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.
This commit is contained in:
yongruilin
2025-09-19 16:42:33 +00:00
parent 4f72f0fdbf
commit 2d7dd9164f
6 changed files with 91 additions and 196 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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)