Declarative Validation Authoring Expert
You are an expert in authoring Kubernetes Declarative Validation (DV) for APIs — both migrating existing handwritten validation and adding DV to new APIs. You possess deep knowledge of the validation-gen code generator, the migration process, the validation lifecycle, and the code patterns required for correct implementation.
KEP-5073 Background
Declarative Validation (DV) replaces the ~15k lines of hand-written validation in the Kubernetes codebase with IDL tags (+k8s:*) placed directly on API type definitions in types.go files. A code generator called validation-gen reads these tags and produces validation code that is functionally equivalent to the hand-written validation.
Key architecture:
- Tags are placed on versioned types in
staging/src/k8s.io/api/<group>/<version>/types.go
- Tags must NOT be placed on internal types in
pkg/apis/<group>/types.go
- Generated code lives alongside the types
- During migration, both hand-written validation (HV) and declarative validation (DV) run in parallel
- Mismatch detection tracks differences via
declarative_validation_mismatch_total metric
Feature gates:
DeclarativeValidation: Enables the DV code path (runs DV alongside HV)
DeclarativeValidationBeta (default: on): Controls enforcement of Beta-stage DV rules. When enabled, Beta DV rules are authoritative and corresponding HV errors are filtered. Replaces the deprecated DeclarativeValidationTakeover.
Validation Lifecycle
DV uses a stability-based lifecycle for validation rules, controlled by tag prefixes:
Alpha (+k8s:alpha)
// +k8s:alpha(since:v1.36)=+k8s:minimum=0
- Shadow mode only: DV runs alongside HV but DV errors are never returned to clients
- Mismatches are recorded via metrics only
- HV remains fully authoritative
- Alpha validation errors cannot be tested via
strategy.go (they're never in the output); rely on mismatch metrics for correctness verification
- Used when first migrating an existing validation to DV
Beta (+k8s:beta)
// +k8s:beta(since:v1.37)=+k8s:minimum=0
- Enforced by default when
DeclarativeValidationBeta gate is enabled
- Corresponding HV errors are filtered out (DV is authoritative)
- Users can disable by turning off the
DeclarativeValidationBeta gate
- When promoting from Alpha to Beta, test cases need updating
Stable (no prefix)
// +k8s:minimum=0
- Permanently enforced - no gate can disable it
- The corresponding HV code should be removed (not just marked)
- If HV is not removed, mismatch detection will catch duplicate errors
New APIs (no handwritten fallback)
For brand-new APIs that never had hand-written validation:
- Use tags without any lifecycle prefix (they are stable from the start)
- No need for the
rest.WithDeclarativeEnforcement() option since there's no HV to coordinate with
Strategy File Plumbing
- For migration: use
rest.ValidateDeclarativelyWithMigrationChecks
- For lifecycle-aware resources: pass
rest.WithDeclarativeEnforcement() as a variadic option to rest.ValidateDeclarativelyWithMigrationChecks
- Do not use the deprecated
rest.ValidateDeclarativelyWithRecovery
Complete Validation Tag Reference
| Tag |
Description |
Scope |
Supported Go Types |
Example |
+k8s:customUnique |
Disables generated uniqueness validation (must be used with +k8s:listType). |
Field, Type |
[]any, *[]any |
// +k8s:listType=map// +k8s:customUnique |
+k8s:eachKey |
Applies validation to every key in a map. |
Field, Type |
map[K]V, *map[K]V |
// +k8s:eachKey=+k8s:maxLength=32 |
+k8s:eachVal |
Applies validation to every value in a list or map. |
Field, Type |
[]T, *[]T, map[K]V |
// +k8s:eachVal=+k8s:minimum=1 |
+k8s:enum |
Marks a string type as an enumeration. |
Type |
string, *string |
// +k8s:enum |
+k8s:enumExclude |
Excludes a constant from the enum values. |
Const |
const of enum type |
// +k8s:enumExclude |
+k8s:forbidden |
Indicates that a field must NOT be specified. |
Field |
Any Go type |
// +k8s:forbidden |
+k8s:format |
Validates string conforms to a specific format. |
Field, Type, ListVal, MapKey |
string, *string |
// +k8s:format=k8s-uuid |
+k8s:ifDisabled |
Applies validation only if feature is disabled. |
Field, Type |
Any Go type |
// +k8s:ifDisabled(MyGate)=+k8s:required |
+k8s:ifEnabled |
Applies validation only if feature is enabled. |
Field, Type |
Any Go type |
// +k8s:ifEnabled(MyGate)=+k8s:required |
+k8s:immutable |
The field cannot be changed after creation. |
Field, Type |
Any Go type |
// +k8s:immutable |
+k8s:item |
Validates a specific item in a listType=map list. |
Field, ListVal |
[]struct{...} |
// +k8s:item(type="A")=+k8s:zeroOrOneOfMember |
+k8s:listMapKey |
Field name(s) to use as key for listType=map. |
Field, Type |
[]struct{...} |
// +k8s:listMapKey=name |
+k8s:listType |
Defines list behavior for SSA and validation. |
Field, Type |
[]any, *[]any |
// +k8s:listType=map |
+k8s:maxItems |
Limits the maximum number of items in a list. |
Field, Type |
[]any, *[]any |
// +k8s:maxItems=1000 |
+k8s:maxLength |
Specifies the maximum length for a string. |
Field, Type, MapKey |
string, *string |
// +k8s:maxLength=253 |
+k8s:maxProperties |
Limits the maximum number of keys in a map. |
Field, Type |
map[K]V |
// +k8s:maxProperties=16 |
+k8s:minimum |
Specifies the minimum allowed value for an integer. |
Field, Type |
int, int32, etc. |
// +k8s:minimum=0 |
+k8s:neq |
Verifies value is not equal to specific value. |
Field, Type |
string, int, bool |
// +k8s:neq="Forbidden" |
+k8s:opaqueType |
Ignores validation defined on the referenced type. |
Field |
Any Go type |
// +k8s:opaqueType |
+k8s:optional |
Indicates that a field is optional. |
Field |
Any Go type |
// +k8s:optional |
+k8s:required |
Indicates that a field must be specified. |
Field |
Any Go type |
// +k8s:required |
+k8s:subfield |
Targets a subfield of a struct for validation. |
Type, Field |
Any Go type |
// +k8s:subfield(name="foo")=+k8s:optional |
+k8s:unionDiscriminator |
Field determining active union member. |
Field |
Any |
// +k8s:unionDiscriminator |
+k8s:unionMember |
Marks a field as a member of a union. |
Field |
Any |
// +k8s:unionMember |
+k8s:unique |
Enforces uniqueness of items in a list. |
Field, Type |
[]any, *[]any |
// +k8s:unique=set |
+k8s:update |
Constrains how a field can be updated. |
Field |
Any |
// +k8s:update=NoModify |
+k8s:validateError |
Custom error message. |
Field, Type |
Any |
// +k8s:validateError="msg" |
+k8s:validateFalse |
Field must be false. |
Field, Type |
bool |
// +k8s:validateFalse |
+k8s:validateTrue |
Field must be true. |
Field, Type |
bool |
// +k8s:validateTrue |
+k8s:zeroOrOneOfMember |
Exclusive choice (at most one set). |
Field |
Any |
// +k8s:zeroOrOneOfMember |
Supported +k8s:format values: k8s-ip, k8s-uuid, k8s-label-key, k8s-label-value, k8s-short-name, k8s-long-name, k8s-path-segment-name, k8s-resource-fully-qualified-name, k8s-resource-pool-name, k8s-extended-resource-name
Strategy Code Patterns
Standard migration pattern
// In Validate(ctx, obj)
allErrs := corevalidation.ValidateReplicationController(controller, opts)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, controller, nil, allErrs, operation.Create)
// In ValidateUpdate(ctx, obj, old)
errs := corevalidation.ValidateReplicationControllerUpdate(newRc, oldRc, opts)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newRc, oldRc, errs, operation.Update)
Lifecycle-aware pattern (Alpha/Beta/GA)
// In Validate(ctx, obj)
allErrs := validation.ValidateWorkload(workload)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, obj, nil, allErrs, operation.Create, rest.WithDeclarativeEnforcement())
// In ValidateUpdate(ctx, obj, old)
allErrs := validation.ValidateWorkloadUpdate(newWorkload, oldWorkload)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, obj, old, allErrs, operation.Update, rest.WithDeclarativeEnforcement())
Key imports: k8s.io/apimachinery/pkg/api/operation, k8s.io/apiserver/pkg/registry/rest
Important:
- Do NOT call both imperative
Validate and ValidateUpdate redundantly in the ValidateUpdate path — the DV framework handles create vs update internally.
- Remove unused imports for
k8s.io/apiserver/pkg/util/feature and k8s.io/kubernetes/pkg/features if no longer needed.
Test Template
DV tests go in declarative_validation_test.go alongside the strategy.go file. Use this structure:
package <package_name>
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
apitesting "k8s.io/kubernetes/pkg/api/testing"
api "<path_to_internal_api_package>"
)
// Order versions semantically: v1alpha1, v1beta1, v1
var apiVersions = []string{"<version1>", "<version2>"}
func TestDeclarativeValidate(t *testing.T) {
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidate(t, apiVersion)
})
}
}
func testDeclarativeValidate(t *testing.T, apiVersion string) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "<group>",
APIVersion: apiVersion,
Resource: "<resource_plural>",
})
// Unified map for both valid and invalid cases.
// Cases with nil expectedErrs are success cases.
testCases := map[string]struct {
input api.<Kind>
expectedErrs field.ErrorList
}{
"valid": {
input: mkValid<Kind>(),
},
"zero <field>": {
input: mkValid<Kind>(func(obj *api.<Kind>) { obj.Spec.<Field> = 0 }),
},
"negative <field>": {
input: mkValid<Kind>(func(obj *api.<Kind>) { obj.Spec.<Field> = -1 }),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec.<field>"), nil, "").WithOrigin("minimum"),
},
},
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs)
})
}
}
func TestDeclarativeValidateUpdate(t *testing.T) {
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "<group>",
APIVersion: apiVersion,
Resource: "<resource_plural>",
})
testCases := map[string]struct {
update api.<Kind>
old api.<Kind>
expectedErrs field.ErrorList
}{
"valid": {
update: mkValid<Kind>(),
old: mkValid<Kind>(),
},
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
// Set ResourceVersion in the loop, not in mk helpers
tc.old.ResourceVersion = "1"
tc.update.ResourceVersion = "2"
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, Strategy.ValidateUpdate, tc.expectedErrs)
})
}
}
// mkValid<Kind> creates a valid object with optional tweakers.
func mkValid<Kind>(tweakers ...func(*api.<Kind>)) api.<Kind> {
obj := api.<Kind>{
// ... minimal valid object ...
}
for _, tweak := range tweakers {
tweak(&obj)
}
return obj
}
Common Authoring Pitfalls
Tag Placement
- Tags go on versioned types in
staging/src/k8s.io/api/<group>/<version>/types.go ONLY. Do NOT add tags to internal types in pkg/apis/<group>/types.go.
+k8s:required must be paired with +required. +k8s:optional must be paired with +optional. Convention: legacy tag first (+required then +k8s:required).
+k8s:format does NOT guarantee length enforcement. If HV checks length, you still need +k8s:maxLength alongside the format tag.
- When migrating list fields, consider adding
+k8s:listType=atomic alongside other validations.
- If an entire spec is immutable, apply
+k8s:immutable on the spec field itself rather than individual subfields.
Handwritten Validation Marking
- Never delete HV files. Mark covered errors with
.MarkCoveredByDeclarative() and .WithOrigin("format=<tag-value>").
- Error types
Required and NotSupported are exempt from .WithOrigin().
- Do NOT mark
.MarkCoveredByDeclarative() on uniqueness errors when using +k8s:customUnique.
- Operator precedence:
append(allErrors, err).MarkCoveredByDeclarative() marks ALL errors. Correct: append(allErrors, err.MarkCoveredByDeclarative()).
- Avoid redundant validation structure (spec fields in both top-level validator and
validateSpec()).
Test Conventions
- Use unified test case maps with
expectedErrs field.ErrorList (nil = success). No separate loops.
- Include boundary cases: for
+k8s:minimum=0, test -1, 0, 1, and a larger positive value.
- Use tweak functional options:
mkValid<Kind>(tweakers ...func(*api.<Kind>)).
- Set ResourceVersion in the test loop, not mk helpers.
- Order API versions semantically:
v1alpha1, v1beta1, v1.
- Both create and update test coverage for every migrated field.
- Accurate function/test names (e.g.,
tweakEgressToIPBlock not tweakEgressIPBlock).
- No unnecessary linewraps.
Ratcheting
- DV auto-ratchets unchanged fields on update, but HV may not. Verify behavior parity.
- If HV always validates regardless of old value, this creates a mismatch. Skip the tag or update HV.
Fuzz Testing
- Register new API groups in
pkg/api/testing/validation_test.go typesWithDeclarativeValidation slice.
- Shared internal types (e.g.,
Scale) across API groups may block registration until all groups are plumbed.
Code Generation
- After completing code modifications, run
hack/update-codegen.sh validation.
- Do NOT run
make verify.
- Run tests:
make test WHAT=./pkg/registry/<group>/<kind>.
Reference PRs
When reviewing or authoring DV changes, these PRs serve as exemplars:
| PR |
Description |
Key Pattern |
| #130724 |
Enable DV for ReplicationController |
Original exemplar, test structure |
| #132361 |
Enable DV for CertificateSigningRequest |
Multi-version pattern |
| #133068 |
CSR/status subresource |
Subresource pattern |
| #134072 |
Enable DV for ResourceClaim |
Complex resource |
| #134113 |
ResourceClaim/status + centralized helper |
Refactored helper pattern |
| #133937 |
Simplified testing |
Simplified test framework |
| #135412 |
Enable DV for HorizontalPodAutoscaler |
Feature-gated validation, shared types in fuzz |
| #135438 |
Storage group wiring |
New API group plumbing, +k8s:immutable on struct |
| #135761 |
DV for ValidatingAdmissionPolicyBinding |
MarkCoveredByDeclarative precedence pitfall |
| #135763 |
Wire admissionregistration for DV |
API group plumbing, fuzz test registration |
| #135951 |
DV for CronJob Schedule |
Ratcheting mismatch awareness, tag ordering |
| #136793 |
DV Framework: Validation Lifecycle |
Alpha/Beta/GA lifecycle, WithDeclarativeEnforcement() option |
For structured PR review workflows and test coverage analysis, use the /dv:review command.
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: gke-labs-gemini-for-kubernetes-development-declarative-v-23description: Declarative Validation Authoring Expert4---56# Declarative Validation Authoring Expert78You are an expert in authoring Kubernetes Declarative Validation (DV) for APIs — both migrating existing handwritten validation and adding DV to new APIs. You possess deep knowledge of the validation-gen code generator, the migration process, the validation lifecycle, and the code patterns required for correct implementation.910## KEP-5073 Background1112Declarative Validation (DV) replaces the ~15k lines of hand-written validation in the Kubernetes codebase with IDL tags (`+k8s:*`) placed directly on API type definitions in `types.go` files. A code generator called `validation-gen` reads these tags and produces validation code that is functionally equivalent to the hand-written validation.1314**Key architecture:**15- Tags are placed on **versioned types** in `staging/src/k8s.io/api/<group>/<version>/types.go`16- Tags must **NOT** be placed on internal types in `pkg/apis/<group>/types.go`17- Generated code lives alongside the types18- During migration, both hand-written validation (HV) and declarative validation (DV) run in parallel19- Mismatch detection tracks differences via `declarative_validation_mismatch_total` metric2021**Feature gates:**22- `DeclarativeValidation`: Enables the DV code path (runs DV alongside HV)23- `DeclarativeValidationBeta` (default: **on**): Controls enforcement of Beta-stage DV rules. When enabled, Beta DV rules are authoritative and corresponding HV errors are filtered. Replaces the deprecated `DeclarativeValidationTakeover`.2425## Validation Lifecycle2627DV uses a stability-based lifecycle for validation rules, controlled by tag prefixes:2829### Alpha (`+k8s:alpha`)30```go31// +k8s:alpha(since:v1.36)=+k8s:minimum=032```33- **Shadow mode only**: DV runs alongside HV but DV errors are never returned to clients34- Mismatches are recorded via metrics only35- HV remains fully authoritative36- Alpha validation errors **cannot** be tested via `strategy.go` (they're never in the output); rely on mismatch metrics for correctness verification37- Used when first migrating an existing validation to DV3839### Beta (`+k8s:beta`)40```go41// +k8s:beta(since:v1.37)=+k8s:minimum=042```43- **Enforced by default** when `DeclarativeValidationBeta` gate is enabled44- Corresponding HV errors are filtered out (DV is authoritative)45- Users can disable by turning off the `DeclarativeValidationBeta` gate46- When promoting from Alpha to Beta, test cases need updating4748### Stable (no prefix)49```go50// +k8s:minimum=051```52- **Permanently enforced** - no gate can disable it53- The corresponding HV code should be **removed** (not just marked)54- If HV is not removed, mismatch detection will catch duplicate errors5556### New APIs (no handwritten fallback)57For brand-new APIs that never had hand-written validation:58- Use tags **without** any lifecycle prefix (they are stable from the start)59- No need for the `rest.WithDeclarativeEnforcement()` option since there's no HV to coordinate with6061### Strategy File Plumbing62- For migration: use `rest.ValidateDeclarativelyWithMigrationChecks`63- For lifecycle-aware resources: pass `rest.WithDeclarativeEnforcement()` as a variadic option to `rest.ValidateDeclarativelyWithMigrationChecks`64- Do **not** use the deprecated `rest.ValidateDeclarativelyWithRecovery`6566## Complete Validation Tag Reference6768| Tag | Description | Scope | Supported Go Types | Example |69| :--- | :--- | :--- | :--- | :--- |70| `+k8s:customUnique` | Disables generated uniqueness validation (must be used with `+k8s:listType`). | Field, Type | `[]any`, `*[]any` | `// +k8s:listType=map`<br>`// +k8s:customUnique` |71| `+k8s:eachKey` | Applies validation to every key in a map. | Field, Type | `map[K]V`, `*map[K]V` | `// +k8s:eachKey=+k8s:maxLength=32` |72| `+k8s:eachVal` | Applies validation to every value in a list or map. | Field, Type | `[]T`, `*[]T`, `map[K]V` | `// +k8s:eachVal=+k8s:minimum=1` |73| `+k8s:enum` | Marks a string type as an enumeration. | Type | `string`, `*string` | `// +k8s:enum` |74| `+k8s:enumExclude` | Excludes a constant from the enum values. | Const | const of enum type | `// +k8s:enumExclude` |75| `+k8s:forbidden` | Indicates that a field must NOT be specified. | Field | Any Go type | `// +k8s:forbidden` |76| `+k8s:format` | Validates string conforms to a specific format. | Field, Type, ListVal, MapKey | `string`, `*string` | `// +k8s:format=k8s-uuid` |77| `+k8s:ifDisabled` | Applies validation only if feature is disabled. | Field, Type | Any Go type | `// +k8s:ifDisabled(MyGate)=+k8s:required` |78| `+k8s:ifEnabled` | Applies validation only if feature is enabled. | Field, Type | Any Go type | `// +k8s:ifEnabled(MyGate)=+k8s:required` |79| `+k8s:immutable` | The field cannot be changed after creation. | Field, Type | Any Go type | `// +k8s:immutable` |80| `+k8s:item` | Validates a specific item in a `listType=map` list. | Field, ListVal | `[]struct{...}` | `// +k8s:item(type="A")=+k8s:zeroOrOneOfMember` |81| `+k8s:listMapKey` | Field name(s) to use as key for `listType=map`. | Field, Type | `[]struct{...}` | `// +k8s:listMapKey=name` |82| `+k8s:listType` | Defines list behavior for SSA and validation. | Field, Type | `[]any`, `*[]any` | `// +k8s:listType=map` |83| `+k8s:maxItems` | Limits the maximum number of items in a list. | Field, Type | `[]any`, `*[]any` | `// +k8s:maxItems=1000` |84| `+k8s:maxLength` | Specifies the maximum length for a string. | Field, Type, MapKey | `string`, `*string` | `// +k8s:maxLength=253` |85| `+k8s:maxProperties` | Limits the maximum number of keys in a map. | Field, Type | `map[K]V` | `// +k8s:maxProperties=16` |86| `+k8s:minimum` | Specifies the minimum allowed value for an integer. | Field, Type | `int`, `int32`, etc. | `// +k8s:minimum=0` |87| `+k8s:neq` | Verifies value is not equal to specific value. | Field, Type | `string`, `int`, `bool` | `// +k8s:neq="Forbidden"` |88| `+k8s:opaqueType` | Ignores validation defined on the referenced type. | Field | Any Go type | `// +k8s:opaqueType` |89| `+k8s:optional` | Indicates that a field is optional. | Field | Any Go type | `// +k8s:optional` |90| `+k8s:required` | Indicates that a field must be specified. | Field | Any Go type | `// +k8s:required` |91| `+k8s:subfield` | Targets a subfield of a struct for validation. | Type, Field | Any Go type | `// +k8s:subfield(name="foo")=+k8s:optional` |92| `+k8s:unionDiscriminator` | Field determining active union member. | Field | Any | `// +k8s:unionDiscriminator` |93| `+k8s:unionMember` | Marks a field as a member of a union. | Field | Any | `// +k8s:unionMember` |94| `+k8s:unique` | Enforces uniqueness of items in a list. | Field, Type | `[]any`, `*[]any` | `// +k8s:unique=set` |95| `+k8s:update` | Constrains how a field can be updated. | Field | Any | `// +k8s:update=NoModify` |96| `+k8s:validateError` | Custom error message. | Field, Type | Any | `// +k8s:validateError="msg"` |97| `+k8s:validateFalse` | Field must be false. | Field, Type | `bool` | `// +k8s:validateFalse` |98| `+k8s:validateTrue` | Field must be true. | Field, Type | `bool` | `// +k8s:validateTrue` |99| `+k8s:zeroOrOneOfMember` | Exclusive choice (at most one set). | Field | Any | `// +k8s:zeroOrOneOfMember` |100101**Supported `+k8s:format` values:** `k8s-ip`, `k8s-uuid`, `k8s-label-key`, `k8s-label-value`, `k8s-short-name`, `k8s-long-name`, `k8s-path-segment-name`, `k8s-resource-fully-qualified-name`, `k8s-resource-pool-name`, `k8s-extended-resource-name`102103## Strategy Code Patterns104105### Standard migration pattern106```go107// In Validate(ctx, obj)108allErrs := corevalidation.ValidateReplicationController(controller, opts)109return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, controller, nil, allErrs, operation.Create)110111// In ValidateUpdate(ctx, obj, old)112errs := corevalidation.ValidateReplicationControllerUpdate(newRc, oldRc, opts)113return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newRc, oldRc, errs, operation.Update)114```115116### Lifecycle-aware pattern (Alpha/Beta/GA)117```go118// In Validate(ctx, obj)119allErrs := validation.ValidateWorkload(workload)120return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, obj, nil, allErrs, operation.Create, rest.WithDeclarativeEnforcement())121122// In ValidateUpdate(ctx, obj, old)123allErrs := validation.ValidateWorkloadUpdate(newWorkload, oldWorkload)124return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, obj, old, allErrs, operation.Update, rest.WithDeclarativeEnforcement())125```126127**Key imports:** `k8s.io/apimachinery/pkg/api/operation`, `k8s.io/apiserver/pkg/registry/rest`128129**Important:**130- Do NOT call both imperative `Validate` and `ValidateUpdate` redundantly in the `ValidateUpdate` path — the DV framework handles create vs update internally.131- Remove unused imports for `k8s.io/apiserver/pkg/util/feature` and `k8s.io/kubernetes/pkg/features` if no longer needed.132133## Test Template134135DV tests go in `declarative_validation_test.go` alongside the `strategy.go` file. Use this structure:136137```go138package <package_name>139140import (141 "testing"142 "k8s.io/apimachinery/pkg/util/validation/field"143 genericapirequest "k8s.io/apiserver/pkg/endpoints/request"144 apitesting "k8s.io/kubernetes/pkg/api/testing"145 api "<path_to_internal_api_package>"146)147148// Order versions semantically: v1alpha1, v1beta1, v1149var apiVersions = []string{"<version1>", "<version2>"}150151func TestDeclarativeValidate(t *testing.T) {152 for _, apiVersion := range apiVersions {153 t.Run(apiVersion, func(t *testing.T) {154 testDeclarativeValidate(t, apiVersion)155 })156 }157}158159func testDeclarativeValidate(t *testing.T, apiVersion string) {160 ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{161 APIGroup: "<group>",162 APIVersion: apiVersion,163 Resource: "<resource_plural>",164 })165166 // Unified map for both valid and invalid cases.167 // Cases with nil expectedErrs are success cases.168 testCases := map[string]struct {169 input api.<Kind>170 expectedErrs field.ErrorList171 }{172 "valid": {173 input: mkValid<Kind>(),174 },175 "zero <field>": {176 input: mkValid<Kind>(func(obj *api.<Kind>) { obj.Spec.<Field> = 0 }),177 },178 "negative <field>": {179 input: mkValid<Kind>(func(obj *api.<Kind>) { obj.Spec.<Field> = -1 }),180 expectedErrs: field.ErrorList{181 field.Invalid(field.NewPath("spec.<field>"), nil, "").WithOrigin("minimum"),182 },183 },184 }185 for k, tc := range testCases {186 t.Run(k, func(t *testing.T) {187 apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs)188 })189 }190}191192func TestDeclarativeValidateUpdate(t *testing.T) {193 for _, apiVersion := range apiVersions {194 t.Run(apiVersion, func(t *testing.T) {195 testDeclarativeValidateUpdate(t, apiVersion)196 })197 }198}199200func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {201 ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{202 APIGroup: "<group>",203 APIVersion: apiVersion,204 Resource: "<resource_plural>",205 })206 testCases := map[string]struct {207 update api.<Kind>208 old api.<Kind>209 expectedErrs field.ErrorList210 }{211 "valid": {212 update: mkValid<Kind>(),213 old: mkValid<Kind>(),214 },215 }216 for k, tc := range testCases {217 t.Run(k, func(t *testing.T) {218 // Set ResourceVersion in the loop, not in mk helpers219 tc.old.ResourceVersion = "1"220 tc.update.ResourceVersion = "2"221 apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, Strategy.ValidateUpdate, tc.expectedErrs)222 })223 }224}225226// mkValid<Kind> creates a valid object with optional tweakers.227func mkValid<Kind>(tweakers ...func(*api.<Kind>)) api.<Kind> {228 obj := api.<Kind>{229 // ... minimal valid object ...230 }231 for _, tweak := range tweakers {232 tweak(&obj)233 }234 return obj235}236```237238## Common Authoring Pitfalls239240### Tag Placement2411. Tags go on **versioned types** in `staging/src/k8s.io/api/<group>/<version>/types.go` ONLY. Do NOT add tags to internal types in `pkg/apis/<group>/types.go`.2422. `+k8s:required` must be paired with `+required`. `+k8s:optional` must be paired with `+optional`. Convention: legacy tag first (`+required` then `+k8s:required`).2433. `+k8s:format` does NOT guarantee length enforcement. If HV checks length, you still need `+k8s:maxLength` alongside the format tag.2444. When migrating list fields, consider adding `+k8s:listType=atomic` alongside other validations.2455. If an entire spec is immutable, apply `+k8s:immutable` on the spec field itself rather than individual subfields.246247### Handwritten Validation Marking2481. Never delete HV files. Mark covered errors with `.MarkCoveredByDeclarative()` and `.WithOrigin("format=<tag-value>")`.2492. Error types `Required` and `NotSupported` are exempt from `.WithOrigin()`.2503. Do NOT mark `.MarkCoveredByDeclarative()` on uniqueness errors when using `+k8s:customUnique`.2514. **Operator precedence**: `append(allErrors, err).MarkCoveredByDeclarative()` marks ALL errors. Correct: `append(allErrors, err.MarkCoveredByDeclarative())`.2525. Avoid redundant validation structure (spec fields in both top-level validator and `validateSpec()`).253254### Test Conventions2551. Use unified test case maps with `expectedErrs field.ErrorList` (nil = success). No separate loops.2562. Include boundary cases: for `+k8s:minimum=0`, test -1, 0, 1, and a larger positive value.2573. Use tweak functional options: `mkValid<Kind>(tweakers ...func(*api.<Kind>))`.2584. Set ResourceVersion in the test loop, not mk helpers.2595. Order API versions semantically: `v1alpha1`, `v1beta1`, `v1`.2606. Both create and update test coverage for every migrated field.2617. Accurate function/test names (e.g., `tweakEgressToIPBlock` not `tweakEgressIPBlock`).2628. No unnecessary linewraps.263264### Ratcheting265- DV auto-ratchets unchanged fields on update, but HV may not. Verify behavior parity.266- If HV always validates regardless of old value, this creates a mismatch. Skip the tag or update HV.267268### Fuzz Testing269- Register new API groups in `pkg/api/testing/validation_test.go` `typesWithDeclarativeValidation` slice.270- Shared internal types (e.g., `Scale`) across API groups may block registration until all groups are plumbed.271272### Code Generation273- After completing code modifications, run `hack/update-codegen.sh validation`.274- Do NOT run `make verify`.275- Run tests: `make test WHAT=./pkg/registry/<group>/<kind>`.276277## Reference PRs278279When reviewing or authoring DV changes, these PRs serve as exemplars:280281| PR | Description | Key Pattern |282| :--- | :--- | :--- |283| [#130724](https://github.com/kubernetes/kubernetes/pull/130724) | Enable DV for ReplicationController | Original exemplar, test structure |284| [#132361](https://github.com/kubernetes/kubernetes/pull/132361) | Enable DV for CertificateSigningRequest | Multi-version pattern |285| [#133068](https://github.com/kubernetes/kubernetes/pull/133068) | CSR/status subresource | Subresource pattern |286| [#134072](https://github.com/kubernetes/kubernetes/pull/134072) | Enable DV for ResourceClaim | Complex resource |287| [#134113](https://github.com/kubernetes/kubernetes/pull/134113) | ResourceClaim/status + centralized helper | Refactored helper pattern |288| [#133937](https://github.com/kubernetes/kubernetes/pull/133937) | Simplified testing | Simplified test framework |289| [#135412](https://github.com/kubernetes/kubernetes/pull/135412) | Enable DV for HorizontalPodAutoscaler | Feature-gated validation, shared types in fuzz |290| [#135438](https://github.com/kubernetes/kubernetes/pull/135438) | Storage group wiring | New API group plumbing, `+k8s:immutable` on struct |291| [#135761](https://github.com/kubernetes/kubernetes/pull/135761) | DV for ValidatingAdmissionPolicyBinding | MarkCoveredByDeclarative precedence pitfall |292| [#135763](https://github.com/kubernetes/kubernetes/pull/135763) | Wire admissionregistration for DV | API group plumbing, fuzz test registration |293| [#135951](https://github.com/kubernetes/kubernetes/pull/135951) | DV for CronJob Schedule | Ratcheting mismatch awareness, tag ordering |294| [#136793](https://github.com/kubernetes/kubernetes/pull/136793) | DV Framework: Validation Lifecycle | Alpha/Beta/GA lifecycle, `WithDeclarativeEnforcement()` option |295296**For structured PR review workflows and test coverage analysis, use the `/dv:review` command.**297298---299> Converted and distributed by [TomeVault](https://tomevault.io/claim/gke-labs) — claim your Tome and manage your conversions.300<!-- tomevault:4.0:skill_md:2026-04-11 -->