From 2d7dd9164fa5daafcbffebd541e34afad776202a Mon Sep 17 00:00:00 2001 From: yongruilin Date: Fri, 19 Sep 2025 16:42:33 +0000 Subject: [PATCH] Refactor: Centralize declarative validation and migration logic The boilerplate for running declarative validation was duplicated across multiple resource strategies. This included feature gate checks, metric identifier generation, error comparison, and conditional merging logic, which made the code verbose and difficult to maintain. This commit introduces a new helper function, `rest.ValidateDeclarativelyWithMigrationChecks`, to encapsulate this common logic. All relevant strategies have been refactored to use this new function, resulting in cleaner and more concise code. --- .../certificates/certificates/strategy.go | 79 ++----------------- .../replicationcontroller/storage/storage.go | 23 +----- .../core/replicationcontroller/strategy.go | 43 +--------- pkg/registry/resource/deviceclass/strategy.go | 23 +----- .../resource/resourceclaim/strategy.go | 44 +---------- .../apiserver/pkg/registry/rest/validate.go | 75 +++++++++++++++++- 6 files changed, 91 insertions(+), 196 deletions(-) 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)