diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go index 5a62b95d822..9e9013fd737 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go @@ -76,24 +76,24 @@ func WithNormalizationRules(rules []field.NormalizationRule) ValidationConfig { } } -// WithDeclarativeNative marks the validation configuration to indicate that it includes -// declarative validations that are defined *only* declaratively, lacking corresponding imperative validation. -// When set, declarative validation is always executed regardless of feature gates. Errors marked as -// declarative-native are separated from the full set and returned alongside imperative errors. -func WithDeclarativeNative() ValidationConfig { +// WithDeclarativeEnforcement marks the validation configuration to indicate that it includes +// declarative validations that should follow the fine-grained Validation Lifecycle. +// When set, declarative validation is always executed regardless of feature gates. +// Authority is determined by individual tag prefixes (+k8s:alpha, +k8s:beta) and the +// DeclarativeValidationBeta safety switch. +func WithDeclarativeEnforcement() ValidationConfig { return func(config *validationConfigOption) { - config.containsDeclarativeNative = true + config.declarativeEnforcement = true } } type validationConfigOption struct { - opType operation.Type - options []string - takeover bool - subresourceGVKMapper GroupVersionKindProvider - validationIdentifier string - normalizationRules []field.NormalizationRule - containsDeclarativeNative bool + opType operation.Type + options []string + subresourceGVKMapper GroupVersionKindProvider + validationIdentifier string + normalizationRules []field.NormalizationRule + declarativeEnforcement bool } // validateDeclaratively validates obj and oldObj against declarative @@ -345,12 +345,18 @@ func metricIdentifier(ctx context.Context, scheme *runtime.Scheme, obj runtime.O return identifier, errs } -// ValidateDeclarativelyWithMigrationChecks runs declarative validation, and conditionally compares results -// with imperative validation and merges errors based on the feature gate and `takeover` flag. -// It proceeds if either the DeclarativeValidation feature gate is enabled or `containsDeclarativeNative` is set. +// ValidateDeclarativelyWithMigrationChecks executes declarative validation and implements the Validation Lifecycle strategy. +// It manages the transition from handwritten (HV) to declarative (DV) validation by controlling enforcement: +// - Standard: Enforced if declarativeEnforcement is set. HV counterparts are expected to be deleted from source. +// - Beta: Enforced if declarativeEnforcement is set AND DeclarativeValidationBeta feature gate is enabled. +// When enforced, corresponding HV errors are filtered out. Otherwise, DV is shadowed. +// - Alpha: Always shadowed; HV remains authoritative. +// +// Mismatches between HV and DV are logged if the DeclarativeValidation gate is enabled. +// Mismatch checking is limited to Alpha and Beta stages when explicit enforcement is active. func ValidateDeclarativelyWithMigrationChecks(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, errs field.ErrorList, opType operation.Type, configOpts ...ValidationConfig) field.ErrorList { declarativeValidationEnabled := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) + betaEnabled := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationBeta) validationIdentifier, err := metricIdentifier(ctx, scheme, obj, opType) if err != nil { @@ -361,50 +367,86 @@ func ValidateDeclarativelyWithMigrationChecks(ctx context.Context, scheme *runti // Directly create the config and call the core validation logic. cfg := &validationConfigOption{ opType: opType, - takeover: takeover, validationIdentifier: validationIdentifier, } for _, opt := range configOpts { opt(cfg) } - // Short-circuit if neither DeclarativeValidation is enabled nor the object contains declarative native validation. - if !(declarativeValidationEnabled || cfg.containsDeclarativeNative) { + // Short-circuit if neither DeclarativeValidation is enabled nor the object is explicitly configured for declarative enforcement. + if !declarativeValidationEnabled && !cfg.declarativeEnforcement { return errs } // Call the panic-safe wrapper with the real validation function. - declarativeErrs := panicSafeValidateFunc(validateDeclaratively, cfg.takeover || cfg.containsDeclarativeNative, cfg.validationIdentifier)(ctx, scheme, obj, oldObj, cfg) - - mirroredDVErrors := field.ErrorList{} - dvNativeErrors := field.ErrorList{} - - // When declarative native validation is present, we need to separate declarative native errors - // from mirrored declarative errors. This is to avoid comparing declarative native errors (which - // have no imperative equivalent) with handwritten imperative errors. - if cfg.containsDeclarativeNative { - for _, err := range declarativeErrs { - if err.DeclarativeNative { - dvNativeErrors = append(dvNativeErrors, err) - } else if err.Type == field.ErrorTypeInternal { - // Internal errors should fail both types of validation. - dvNativeErrors = append(dvNativeErrors, err) - mirroredDVErrors = append(mirroredDVErrors, err) - } else { - mirroredDVErrors = append(mirroredDVErrors, err) - } - } - } else { - mirroredDVErrors = declarativeErrs - } + // We should fail if validation is enforced. + declarativeErrs := panicSafeValidateFunc(validateDeclaratively, cfg.declarativeEnforcement, cfg.validationIdentifier)(ctx, scheme, obj, oldObj, cfg) if declarativeValidationEnabled { - compareDeclarativeErrorsAndEmitMismatches(ctx, errs, mirroredDVErrors, takeover, validationIdentifier, cfg.normalizationRules) - if takeover { - errs = append(errs.RemoveCoveredByDeclarative(), mirroredDVErrors...) + // Log mismatches. + // When explicit strategy is used (declarativeEnforcement), Standard errors are authoritative + // and may not have handwritten counterparts (e.g., in new APIs). + // We only mismatch check Alpha and Beta errors in this mode. + mismatchCandidateErrs := declarativeErrs + if cfg.declarativeEnforcement { + mismatchCandidateErrs = nil + for _, err := range declarativeErrs { + level := err.ValidationStabilityLevel.String() + if level == "alpha" || level == "beta" { + mismatchCandidateErrs = append(mismatchCandidateErrs, err) + } + } } + + // We pass betaEnabled (and enforcement) as the takeover flag to avoid changing logic elsewhere for now. + compareDeclarativeErrorsAndEmitMismatches(ctx, errs, mismatchCandidateErrs, cfg.declarativeEnforcement && betaEnabled, validationIdentifier, cfg.normalizationRules) } - errs = append(errs, dvNativeErrors...) + + if !cfg.declarativeEnforcement { + // If enforcement is not enabled, we shadow declarative errors with hand-written ones, so we return early here. + return errs + } + + // Filter HV errors + // We remove HV errors that are covered by declarative validation AND are enforced. + errs = errs.Filter(func(e error) bool { + var fe *field.Error + if !errors.As(e, &fe) || !fe.CoveredByDeclarative { + return false + } + + // Explicit Strategy + level := fe.ValidationStabilityLevel.String() + if level == "beta" { + // Beta validations are enforced only if the Beta feature gate is enabled. + return betaEnabled + } + // For Standard validations, we keep the handwritten error for now to avoid losing coverage + // before it is deleted from source. Alpha validations are always shadowed (kept). + return false + }) + + // Append Enforced DV errors + for _, dvErr := range declarativeErrs { + // Internal errors should always fail validation. + if dvErr.Type == field.ErrorTypeInternal { + errs = append(errs, dvErr) + continue + } + + level := dvErr.ValidationStabilityLevel.String() + if level == "beta" { + if betaEnabled { + errs = append(errs, dvErr) + } + continue + } + if level == "alpha" { + continue // Always shadowed + } + errs = append(errs, dvErr) // Standard + } + return errs } diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go index c389bc19d8d..ef145dd94a3 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go @@ -175,7 +175,8 @@ func TestValidateDeclaratively(t *testing.T) { } else { cfg.opType = operation.Update } - results := panicSafeValidateFunc(validateDeclaratively, cfg.takeover, cfg.validationIdentifier)(ctx, scheme, tc.object, tc.oldObject, cfg) + // takeover is not used here, passing false for shouldFail + results := panicSafeValidateFunc(validateDeclaratively, false, cfg.validationIdentifier)(ctx, scheme, tc.object, tc.oldObject, cfg) matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() matcher.Test(t, tc.expected, results) }) @@ -480,11 +481,11 @@ func TestWithRecover(t *testing.T) { obj := &runtime.Unknown{} testCases := []struct { - name string - validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList - takeoverEnabled bool - wantErrs field.ErrorList - expectLogRegex string + name string + validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList + enforcementEnabled bool + wantErrs field.ErrorList + expectLogRegex string }{ { name: "no panic", @@ -493,28 +494,28 @@ func TestWithRecover(t *testing.T) { field.Invalid(field.NewPath("field"), "value", "reason"), } }, - takeoverEnabled: false, + enforcementEnabled: false, wantErrs: field.ErrorList{ field.Invalid(field.NewPath("field"), "value", "reason"), }, expectLogRegex: "", }, { - name: "panic with takeover disabled", + name: "panic with enforcement disabled", validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { panic("test panic") }, - takeoverEnabled: false, - wantErrs: nil, + enforcementEnabled: false, + wantErrs: nil, // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] expectLogRegex: "E.*panic during declarative validation: test panic", }, { - name: "panic with takeover enabled", + name: "panic with enforcement enabled", validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { panic("test panic") }, - takeoverEnabled: true, + enforcementEnabled: true, wantErrs: field.ErrorList{ field.InternalError(nil, fmt.Errorf("panic during declarative validation: test panic")), }, @@ -525,9 +526,9 @@ func TestWithRecover(t *testing.T) { validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { return nil }, - takeoverEnabled: false, - wantErrs: nil, - expectLogRegex: "", + enforcementEnabled: false, + wantErrs: nil, + expectLogRegex: "", }, } @@ -538,9 +539,9 @@ func TestWithRecover(t *testing.T) { klog.LogToStderr(false) defer klog.LogToStderr(true) - // Pass the takeover flag to panicSafeValidateFunc instead of relying on the feature gate - wrapped := panicSafeValidateFunc(tc.validateFn, tc.takeoverEnabled, "test_validationIdentifier") - gotErrs := wrapped(ctx, scheme, obj, nil, &validationConfigOption{opType: operation.Create, options: options, takeover: tc.takeoverEnabled}) + // Pass the enforcement flag to panicSafeValidateFunc + wrapped := panicSafeValidateFunc(tc.validateFn, tc.enforcementEnabled, "test_validationIdentifier") + gotErrs := wrapped(ctx, scheme, obj, nil, &validationConfigOption{opType: operation.Create, options: options}) klog.Flush() logOutput := buf.String() @@ -574,11 +575,11 @@ func TestWithRecoverUpdate(t *testing.T) { oldObj := &runtime.Unknown{} testCases := []struct { - name string - validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList - takeoverEnabled bool - wantErrs field.ErrorList - expectLogRegex string + name string + validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList + enforcementEnabled bool + wantErrs field.ErrorList + expectLogRegex string }{ { name: "no panic", @@ -587,28 +588,28 @@ func TestWithRecoverUpdate(t *testing.T) { field.Invalid(field.NewPath("field"), "value", "reason"), } }, - takeoverEnabled: false, + enforcementEnabled: false, wantErrs: field.ErrorList{ field.Invalid(field.NewPath("field"), "value", "reason"), }, expectLogRegex: "", }, { - name: "panic with takeover disabled", + name: "panic with enforcement disabled", validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { panic("test update panic") }, - takeoverEnabled: false, - wantErrs: nil, + enforcementEnabled: false, + wantErrs: nil, // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] expectLogRegex: "E.*panic during declarative validation: test update panic", }, { - name: "panic with takeover enabled", + name: "panic with enforcement enabled", validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { panic("test update panic") }, - takeoverEnabled: true, + enforcementEnabled: true, wantErrs: field.ErrorList{ field.InternalError(nil, fmt.Errorf("panic during declarative validation: test update panic")), }, @@ -619,9 +620,9 @@ func TestWithRecoverUpdate(t *testing.T) { validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { return nil }, - takeoverEnabled: false, - wantErrs: nil, - expectLogRegex: "", + enforcementEnabled: false, + wantErrs: nil, + expectLogRegex: "", }, } @@ -632,9 +633,9 @@ func TestWithRecoverUpdate(t *testing.T) { klog.LogToStderr(false) defer klog.LogToStderr(true) - // Pass the takeover flag to panicSafeValidateUpdateFunc instead of relying on the feature gate - wrapped := panicSafeValidateFunc(tc.validateFn, tc.takeoverEnabled, "test_validationIdentifier") - gotErrs := wrapped(ctx, scheme, obj, oldObj, &validationConfigOption{opType: operation.Update, options: options, takeover: tc.takeoverEnabled}) + // Pass the enforcement flag to panicSafeValidateUpdateFunc + wrapped := panicSafeValidateFunc(tc.validateFn, tc.enforcementEnabled, "test_validationIdentifier") + gotErrs := wrapped(ctx, scheme, obj, oldObj, &validationConfigOption{opType: operation.Update, options: options}) klog.Flush() logOutput := buf.String() @@ -829,76 +830,105 @@ func TestMetricIdentifier(t *testing.T) { } func TestValidateDeclarativelyWithMigrationChecks(t *testing.T) { - // Create errors for testing - errImperative := field.ErrorList{ - field.Invalid(field.NewPath("spec", "restartPolicy"), "imp", "imperative error"), - field.Forbidden(field.NewPath("spec", "restartPolicy"), "imperative error covered by declarative").MarkCoveredByDeclarative(), - } - errMirroredDeclarative := field.Invalid(field.NewPath("spec", "restartPolicy"), "dec", "declarative error") - errDeclarativeNative := field.Invalid(field.NewPath("spec", "replicas"), "decOnly", "declarative native error").MarkDeclarativeNative() + // Standard Lifecycle (Enforced by default in explicit strategy) + // Standard HV error marked as covered. In a fully migrated state, this should be deleted from source. + // We include it here to verify it persists (duplicate) if not deleted, rather than being implicitly filtered. + errHVStandardCovered := field.Forbidden(field.NewPath("spec", "standard"), "imperative standard").MarkCoveredByDeclarative() + errDVStandard := field.Forbidden(field.NewPath("spec", "standard"), "decStandard") + + // Additional Declarative (No HV counterpart) + errDVAdditional := field.Invalid(field.NewPath("spec", "additional"), "decAdditional", "declarative additional") + + // Beta Lifecycle (Gated by DeclarativeValidationBeta) + errHVBetaCovered := field.Forbidden(field.NewPath("spec", "beta"), "imperative beta").MarkCoveredByDeclarative().MarkBeta() + errDVBeta := field.Invalid(field.NewPath("spec", "beta"), "decBeta", "declarative beta").MarkBeta() + + // Alpha Lifecycle (Shadowed) + // Alpha rules should NOT mark HV as covered, so HV remains authoritative. + errHVAlpha := field.Forbidden(field.NewPath("spec", "alpha"), "imperative alpha") + errDVAlpha := field.Invalid(field.NewPath("spec", "alpha"), "decAlpha", "declarative alpha").MarkAlpha() testCases := []struct { - name string - dvFeatureEnabled bool - takeoverEnabled bool - containsDeclarativeNative bool - imperativeErrors field.ErrorList - declarativeErrors field.ErrorList - expectedErrors field.ErrorList - shouldPanic bool + name string + dvFeatureEnabled bool + declarativeEnforcement bool + betaGateEnabled bool + imperativeErrors field.ErrorList + declarativeErrors field.ErrorList + expectedErrors field.ErrorList + shouldPanic bool }{ { - name: "Feature Disabled, Not DeclarativeNative -> Skips declarative", - imperativeErrors: errImperative, - declarativeErrors: field.ErrorList{errMirroredDeclarative}, - expectedErrors: errImperative, + name: "Feature Disabled, Not Enforced -> Skips declarative, Returns HV", + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard}, + expectedErrors: field.ErrorList{errHVStandardCovered}, }, { - name: "Feature Disabled, contains DeclarativeNative -> Returns DeclarativeNative errors, ignores regular declarative errors", - containsDeclarativeNative: true, - imperativeErrors: errImperative, - declarativeErrors: field.ErrorList{errMirroredDeclarative, errDeclarativeNative}, - expectedErrors: append(errImperative, errDeclarativeNative), + name: "Feature Disabled, Enforced -> Enforces Standard (HV kept+DV returned, duplicate expected if HV not deleted)", + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard, errDVAdditional}, + expectedErrors: field.ErrorList{errHVStandardCovered, errDVStandard, errDVAdditional}, }, { - name: "Feature Enabled, Not DeclarativeNative -> Returns imperative (no takeover)", + name: "Feature Enabled, Not Enforced -> Returns imperative (Shadow Mode)", dvFeatureEnabled: true, - imperativeErrors: errImperative, - declarativeErrors: field.ErrorList{errMirroredDeclarative}, - expectedErrors: errImperative, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard}, + expectedErrors: field.ErrorList{errHVStandardCovered}, }, { - name: "Feature Enabled, contains DeclarativeNative -> Returns imperative + DeclarativeNative", - dvFeatureEnabled: true, - containsDeclarativeNative: true, - imperativeErrors: errImperative, - declarativeErrors: field.ErrorList{errMirroredDeclarative, errDeclarativeNative}, - expectedErrors: append(errImperative, errDeclarativeNative), + name: "Feature Enabled, Enforced -> Enforces Standard (HV kept+DV returned, duplicate expected if HV not deleted)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard, errDVAdditional}, + expectedErrors: field.ErrorList{errHVStandardCovered, errDVStandard, errDVAdditional}, }, { - name: "Feature Enabled, takeover enabled, contains DeclarativeNative -> Returns non mirrored imperative + declarative + DeclarativeNative", - dvFeatureEnabled: true, - containsDeclarativeNative: true, - takeoverEnabled: true, - imperativeErrors: errImperative, - declarativeErrors: field.ErrorList{errMirroredDeclarative, errDeclarativeNative}, - expectedErrors: field.ErrorList{errImperative[0], errMirroredDeclarative, errDeclarativeNative}, + name: "Feature Disabled, Enforced, Panics -> Returns InternalError", + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + shouldPanic: true, + // Standard HV is kept. Panic error appended. + expectedErrors: append(field.ErrorList{errHVStandardCovered}, field.InternalError(nil, fmt.Errorf("panic during declarative validation: test panic"))), }, { - name: "Feature Disabled, contains DeclarativeNative, Panics -> Returns InternalError", - containsDeclarativeNative: true, - imperativeErrors: errImperative, - shouldPanic: true, - expectedErrors: append(errImperative, field.InternalError(nil, fmt.Errorf("panic during declarative validation: test panic"))), + name: "Feature Enabled, Enforced, InternalError -> Returns InternalError", + dvFeatureEnabled: true, + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{field.InternalError(nil, fmt.Errorf("internal error"))}, + // Standard HV kept. Internal error appended. + expectedErrors: field.ErrorList{errHVStandardCovered, field.InternalError(nil, fmt.Errorf("internal error"))}, }, { - name: "Feature Enabled, takeover enabled, contains DeclarativeNative, InternalError -> Returns non mirrored imperative + InternalError", - dvFeatureEnabled: true, - containsDeclarativeNative: true, - takeoverEnabled: true, - imperativeErrors: errImperative, - declarativeErrors: field.ErrorList{field.InternalError(nil, fmt.Errorf("internal error"))}, - expectedErrors: field.ErrorList{errImperative[0], field.InternalError(nil, fmt.Errorf("internal error")), field.InternalError(nil, fmt.Errorf("internal error"))}, + name: "Enforced, Beta Gate Enabled -> Enforces Beta (HV removed, DV returned)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + betaGateEnabled: true, + imperativeErrors: field.ErrorList{errHVBetaCovered}, + declarativeErrors: field.ErrorList{errDVBeta}, + expectedErrors: field.ErrorList{errDVBeta}, + }, + { + name: "Enforced, Beta Gate Disabled -> Shadows Beta (HV kept, DV hidden)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + betaGateEnabled: false, + imperativeErrors: field.ErrorList{errHVBetaCovered}, + declarativeErrors: field.ErrorList{errDVBeta}, + expectedErrors: field.ErrorList{errHVBetaCovered}, + }, + { + name: "Enforced, Alpha -> Shadows Alpha (HV kept, DV hidden)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + betaGateEnabled: true, + imperativeErrors: field.ErrorList{errHVAlpha}, + declarativeErrors: field.ErrorList{errDVAlpha}, + expectedErrors: field.ErrorList{errHVAlpha}, }, } @@ -907,9 +937,11 @@ func TestValidateDeclarativelyWithMigrationChecks(t *testing.T) { // Set feature gate if !tc.dvFeatureEnabled { featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.35")) + } else { + // Only set Beta gate if we are not emulating an older version where it doesn't exist + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationBeta, tc.betaGateEnabled) } featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, tc.dvFeatureEnabled) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, tc.takeoverEnabled) // Setup scheme for this run localScheme := runtime.NewScheme() @@ -938,8 +970,8 @@ func TestValidateDeclarativelyWithMigrationChecks(t *testing.T) { copy(inputErrs, tc.imperativeErrors) opts := []ValidationConfig{} - if tc.containsDeclarativeNative { - opts = append(opts, WithDeclarativeNative()) + if tc.declarativeEnforcement { + opts = append(opts, WithDeclarativeEnforcement()) } gotErrs := ValidateDeclarativelyWithMigrationChecks(ctx, localScheme, obj, nil, inputErrs, operation.Create, opts...)