diff --git a/pkg/registry/certificates/certificates/strategy.go b/pkg/registry/certificates/certificates/strategy.go index d910548a91d..6954100415b 100644 --- a/pkg/registry/certificates/certificates/strategy.go +++ b/pkg/registry/certificates/certificates/strategy.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "k8s.io/apimachinery/pkg/api/operation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" @@ -29,11 +30,9 @@ import ( "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/storage/names" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/certificates" "k8s.io/kubernetes/pkg/apis/certificates/validation" - "k8s.io/kubernetes/pkg/features" "sigs.k8s.io/structured-merge-diff/v6/fieldpath" ) @@ -116,25 +115,7 @@ func (csrStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object func (csrStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { csr := obj.(*certificates.CertificateSigningRequest) allErrs := validation.ValidateCertificateSigningRequestCreate(csr) - - // If DeclarativeValidation feature gate is enabled, also run declarative validation - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - - const validationIdentifier = "csr_create" - // Run declarative validation with panic recovery - declarativeErrs := rest.ValidateDeclaratively(ctx, legacyscheme.Scheme, csr, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - - // Compare imperative and declarative errors and log + emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, allErrs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - allErrs = append(allErrs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return allErrs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, csr, nil, allErrs, operation.Create) } // WarningsOnCreate returns warnings for the creation of the given object. @@ -148,25 +129,7 @@ func (csrStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) oldCSR := old.(*certificates.CertificateSigningRequest) newCSR := obj.(*certificates.CertificateSigningRequest) errs := validation.ValidateCertificateSigningRequestUpdate(newCSR, oldCSR) - // If DeclarativeValidation feature gate is enabled, also run declarative validation - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - - const validationIdentifier = "csr_update" - // Run declarative update validation with panic recovery - declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newCSR, oldCSR, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - - // Compare imperative and declarative errors and emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - - return errs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newCSR, oldCSR, errs, operation.Update) } // WarningsOnUpdate returns warnings for the given update. @@ -282,23 +245,7 @@ func (csrStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Ob newCSR := obj.(*certificates.CertificateSigningRequest) oldCSR := old.(*certificates.CertificateSigningRequest) errs := validation.ValidateCertificateSigningRequestStatusUpdate(newCSR, oldCSR) - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - - const validationIdentifier = "csr_status_update" - // Run declarative update validation with panic recovery - declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newCSR, oldCSR, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - - // Compare imperative and declarative errors and emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return errs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newCSR, oldCSR, errs, operation.Update) } // WarningsOnUpdate returns warnings for the given update. @@ -353,23 +300,7 @@ func (csrApprovalStrategy) ValidateUpdate(ctx context.Context, obj, old runtime. newCSR := obj.(*certificates.CertificateSigningRequest) oldCSR := old.(*certificates.CertificateSigningRequest) errs := validation.ValidateCertificateSigningRequestApprovalUpdate(newCSR, oldCSR) - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - - const validationIdentifier = "csr_approval_update" - // Run declarative update validation with panic recovery - declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newCSR, oldCSR, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - - // Compare imperative and declarative errors and emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return errs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newCSR, oldCSR, errs, operation.Update) } // WarningsOnUpdate returns warnings for the given update. diff --git a/pkg/registry/core/replicationcontroller/storage/storage.go b/pkg/registry/core/replicationcontroller/storage/storage.go index 2d1e99e5719..bee5ffb8c28 100644 --- a/pkg/registry/core/replicationcontroller/storage/storage.go +++ b/pkg/registry/core/replicationcontroller/storage/storage.go @@ -23,6 +23,7 @@ import ( "fmt" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/operation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -32,7 +33,6 @@ import ( "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/autoscaling" @@ -40,7 +40,6 @@ import ( "k8s.io/kubernetes/pkg/apis/autoscaling/validation" api "k8s.io/kubernetes/pkg/apis/core" extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" - "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/printers" printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" printerstorage "k8s.io/kubernetes/pkg/printers/storage" @@ -315,25 +314,7 @@ func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runti } errs := validation.ValidateScale(scale) - - // If DeclarativeValidation feature gate is enabled, also run declarative validation - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - - const validationIdentifier = "rc_scale" - // Run declarative validation with panic recovery - declarativeErrs := rest.ValidateUpdateDeclaratively( - ctx, legacyscheme.Scheme, scale, oldScale, rest.WithTakeover(takeover), rest.WithSubresourceMapper(i.scaleGVKMapper), rest.WithValidationIdentifier(validationIdentifier)) - - // Compare imperative and declarative errors and log + emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } + errs = rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, scale, oldScale, errs, operation.Update, rest.WithSubresourceMapper(i.scaleGVKMapper)) if len(errs) > 0 { return nil, errors.NewInvalid(autoscaling.Kind("Scale"), replicationcontroller.Name, errs) diff --git a/pkg/registry/core/replicationcontroller/strategy.go b/pkg/registry/core/replicationcontroller/strategy.go index b86c7c13cde..f1ff1cf53b7 100644 --- a/pkg/registry/core/replicationcontroller/strategy.go +++ b/pkg/registry/core/replicationcontroller/strategy.go @@ -28,6 +28,7 @@ import ( corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -39,13 +40,11 @@ import ( "k8s.io/apiserver/pkg/registry/rest" apistorage "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/pod" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core/helper" corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" - "k8s.io/kubernetes/pkg/features" ) // rcStrategy implements verification logic for Replication Controllers. @@ -129,25 +128,7 @@ func (rcStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorL // Run imperative validation allErrs := corevalidation.ValidateReplicationController(controller, opts) - - // If DeclarativeValidation feature gate is enabled, also run declarative validation - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - const validationIdentifier = "rc_create" - - // Run declarative validation with panic recovery - declarativeErrs := rest.ValidateDeclaratively(ctx, legacyscheme.Scheme, controller, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - - // Compare imperative and declarative errors and log + emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, allErrs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - allErrs = append(allErrs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return allErrs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, controller, nil, allErrs, operation.Create) } // WarningsOnCreate returns warnings for the creation of the given object. @@ -199,25 +180,7 @@ func (rcStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) f } } - // If DeclarativeValidation feature gate is enabled, also run declarative validation - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - const validationIdentifier = "rc_update" - - // Run declarative update validation with panic recovery - declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newRc, oldRc, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - - // Compare imperative and declarative errors and emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - - return errs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newRc, oldRc, errs, operation.Update) } // WarningsOnUpdate returns warnings for the given update. diff --git a/pkg/registry/resource/deviceclass/strategy.go b/pkg/registry/resource/deviceclass/strategy.go index ba32cb199fd..8abe8d061c8 100644 --- a/pkg/registry/resource/deviceclass/strategy.go +++ b/pkg/registry/resource/deviceclass/strategy.go @@ -20,6 +20,7 @@ import ( "context" apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/registry/rest" @@ -53,16 +54,7 @@ func (deviceClassStrategy) Validate(ctx context.Context, obj runtime.Object) fie deviceClass := obj.(*resource.DeviceClass) errorList := validation.ValidateDeviceClass(deviceClass) - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - declarativeErrs := rest.ValidateDeclaratively(ctx, legacyscheme.Scheme, deviceClass, rest.WithTakeover(takeover)) - validationIdentifier := "deviceclass_create" - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errorList, declarativeErrs, takeover, validationIdentifier) - if takeover { - errorList = append(errorList.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return errorList + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, deviceClass, nil, errorList, operation.Create) } func (deviceClassStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { @@ -95,16 +87,7 @@ func (deviceClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime. errorList := validation.ValidateDeviceClass(newClass) errorList = append(errorList, validation.ValidateDeviceClassUpdate(newClass, oldClass)...) - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newClass, oldClass, rest.WithTakeover(takeover)) - validationIdentifier := "deviceclass_update" - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errorList, declarativeErrs, takeover, validationIdentifier) - if takeover { - errorList = append(errorList.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return errorList + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newClass, oldClass, errorList, operation.Update) } func (deviceClassStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { diff --git a/pkg/registry/resource/resourceclaim/strategy.go b/pkg/registry/resource/resourceclaim/strategy.go index 58bee514cf7..4da4321ff3a 100644 --- a/pkg/registry/resource/resourceclaim/strategy.go +++ b/pkg/registry/resource/resourceclaim/strategy.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + "k8s.io/apimachinery/pkg/api/operation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" @@ -99,17 +100,7 @@ func (s *resourceclaimStrategy) Validate(ctx context.Context, obj runtime.Object allErrs := resourceutils.AuthorizedForAdmin(ctx, claim.Spec.Devices.Requests, claim.Namespace, s.nsClient) allErrs = append(allErrs, validation.ValidateResourceClaim(claim)...) - - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - const validationIdentifier = "resourceclaim_create" - declarativeErrs := rest.ValidateDeclaratively(ctx, legacyscheme.Scheme, claim, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, allErrs, declarativeErrs, takeover, validationIdentifier) - if takeover { - allErrs = append(allErrs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return allErrs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, claim, nil, allErrs, operation.Create) } func (*resourceclaimStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { @@ -137,17 +128,7 @@ func (s *resourceclaimStrategy) ValidateUpdate(ctx context.Context, obj, old run // AuthorizedForAdmin isn't needed here because the spec is immutable. errorList := validation.ValidateResourceClaim(newClaim) errorList = append(errorList, validation.ValidateResourceClaimUpdate(newClaim, oldClaim)...) - - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - const validationIdentifier = "resourceclaim_update" - declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newClaim, oldClaim, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errorList, declarativeErrs, takeover, validationIdentifier) - if takeover { - errorList = append(errorList.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return errorList + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newClaim, oldClaim, errorList, operation.Update) } func (*resourceclaimStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { @@ -210,24 +191,7 @@ func (r *resourceclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, o } errs := resourceutils.AuthorizedForAdminStatus(ctx, newAllocationResult, oldAllocationResult, newClaim.Namespace, r.nsClient) errs = append(errs, validation.ValidateResourceClaimStatusUpdate(newClaim, oldClaim)...) - - // If DeclarativeValidation feature gate is enabled, also run declarative validation - if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { - // Determine if takeover is enabled - takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) - - // Run declarative update validation with panic recovery - declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newClaim, oldClaim, rest.WithTakeover(takeover)) - // Compare imperative and declarative errors and emit metric if there's a mismatch - const validationIdentifier = "resourceclaim_status_update" - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) - - // Only apply declarative errors if takeover is enabled - if takeover { - errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) - } - } - return errs + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newClaim, oldClaim, errs, operation.Update) } // WarningsOnUpdate returns warnings for the given update. 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 112377a2d8a..a59ee37d584 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go @@ -23,13 +23,14 @@ import ( "strings" "k8s.io/apimachinery/pkg/api/operation" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + utilfeature "k8s.io/apiserver/pkg/util/feature" validationmetrics "k8s.io/apiserver/pkg/validation" "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/features" ) // ValidationConfig defines how a declarative validation request may be configured. @@ -330,6 +331,78 @@ func panicSafeValidateFunc( } } +func metricIdentifier(ctx context.Context, obj runtime.Object, opType operation.Type) (string, error) { + var identifier string + var err error + + identifier = "unknown_resource" + // Use kind for identifier. + if obj != nil { + gvk := obj.GetObjectKind().GroupVersionKind() + if gvk.Kind != "" { + identifier = strings.ToLower(gvk.Kind) + } + } + + // Use requestInfo for subresource. + requestInfo, found := genericapirequest.RequestInfoFrom(ctx) + if !found { + err = fmt.Errorf("could not find requestInfo in context") + } else if len(requestInfo.Subresource) > 0 { + // subresource can be a path, so replace '/' with '_' + identifier += "_" + strings.ReplaceAll(requestInfo.Subresource, "/", "_") + } + + switch opType { + case operation.Create: + identifier += "_create" + case operation.Update: + identifier += "_update" + default: + if err == nil { + err = fmt.Errorf("unknown operation type: %v", opType) + } + identifier += "_unknown_op" + } + return identifier, err +} + +// ValidateDeclarativelyWithMigrationChecks is a helper function that encapsulates the logic for running declarative validation. +// It checks if the DeclarativeValidation feature gate is enabled, generates a validation identifier, +// runs declarative validation, compares the results with imperative validation, and merges the errors if takeover is enabled. +func ValidateDeclarativelyWithMigrationChecks(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, errs field.ErrorList, opType operation.Type, configOpts ...ValidationConfig) field.ErrorList { + if !utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { + return errs + } + + takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) + + validationIdentifier, err := metricIdentifier(ctx, obj, opType) + if err != nil { + // Log the error, but continue with the best-effort identifier. + klog.FromContext(ctx).Error(err, "failed to generate complete validation identifier for declarative validation") + } + + // Directly create the config and call the core validation logic. + cfg := &validationConfigOption{opType: opType} + opts := []ValidationConfig{WithTakeover(takeover), WithValidationIdentifier(validationIdentifier)} + opts = append(opts, configOpts...) + for _, o := range opts { + o(cfg) + } + + // Call the panic-safe wrapper with the real validation function. + declarativeErrs := panicSafeValidateFunc(validateDeclaratively, cfg.takeover, cfg.validationIdentifier)(ctx, scheme, obj, oldObj, cfg) + + CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) + + if takeover { + errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) + } + + return errs +} + // RecordDuplicateValidationErrors increments a metric and log the error when duplicate validation errors are found. func RecordDuplicateValidationErrors(ctx context.Context, qualifiedKind schema.GroupKind, errs field.ErrorList) { logger := klog.FromContext(ctx)