diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go index 0609d8eb66e..a079931dd59 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting.go @@ -54,12 +54,12 @@ func NewRatchetingSchemaValidator(schema *spec.Schema, rootSchema interface{}, r } } -func (r *RatchetingSchemaValidator) Validate(new interface{}) *validate.Result { +func (r *RatchetingSchemaValidator) Validate(new interface{}, options ...ValidationOption) *validate.Result { sv := validate.NewSchemaValidator(r.schema, r.root, r.path, r.knownFormats, r.options...) return sv.Validate(new) } -func (r *RatchetingSchemaValidator) ValidateUpdate(new, old interface{}) *validate.Result { +func (r *RatchetingSchemaValidator) ValidateUpdate(new, old interface{}, options ...ValidationOption) *validate.Result { return newRatchetingValueValidator( common.NewCorrelatedObject(new, old, &celopenapi.Schema{Schema: r.schemaArgs.schema}), r.schemaArgs, diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting_test.go index 078bb548cdc..c136617218c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/ratcheting_test.go @@ -58,8 +58,8 @@ var largeIntSchema *spec.Schema = &spec.Schema{ func TestScalarRatcheting(t *testing.T) { validator := validation.NewRatchetingSchemaValidator(mediumIntSchema, nil, "", strfmt.Default) - require.True(t, validator.ValidateUpdate(1, 1).IsValid()) - require.False(t, validator.ValidateUpdate(1, 2).IsValid()) + require.True(t, validator.ValidateUpdate(1, 1, validation.WithRatcheting(nil)).IsValid()) + require.False(t, validator.ValidateUpdate(1, 2, validation.WithRatcheting(nil)).IsValid()) } var objectSchema *spec.Schema = &spec.Schema{ @@ -90,18 +90,18 @@ func TestObjectScalarFieldsRatcheting(t *testing.T) { "small": 500, }, map[string]interface{}{ "small": 500, - }).IsValid()) + }, validation.WithRatcheting(nil)).IsValid()) assert.True(t, validator.ValidateUpdate(map[string]interface{}{ "small": 501, }, map[string]interface{}{ "small": 501, "medium": 500, - }).IsValid()) + }, validation.WithRatcheting(nil)).IsValid()) assert.False(t, validator.ValidateUpdate(map[string]interface{}{ "small": 500, }, map[string]interface{}{ "small": 501, - }).IsValid()) + }, validation.WithRatcheting(nil)).IsValid()) } // Shows schemas with object fields which themselves are ratcheted can be ratcheted @@ -113,7 +113,7 @@ func TestObjectObjectFieldsRatcheting(t *testing.T) { }}, map[string]interface{}{ "nested": map[string]interface{}{ "small": 500, - }}).IsValid()) + }}, validation.WithRatcheting(nil)).IsValid()) assert.True(t, validator.ValidateUpdate(map[string]interface{}{ "nested": map[string]interface{}{ "small": 501, @@ -121,14 +121,14 @@ func TestObjectObjectFieldsRatcheting(t *testing.T) { "nested": map[string]interface{}{ "small": 501, "medium": 500, - }}).IsValid()) + }}, validation.WithRatcheting(nil)).IsValid()) assert.False(t, validator.ValidateUpdate(map[string]interface{}{ "nested": map[string]interface{}{ "small": 500, }}, map[string]interface{}{ "nested": map[string]interface{}{ "small": 501, - }}).IsValid()) + }}, validation.WithRatcheting(nil)).IsValid()) } func ptr[T any](v T) *T { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go index e0042356ac0..3fc9965fb9a 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go @@ -24,6 +24,7 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/cel/common" utilfeature "k8s.io/apiserver/pkg/util/feature" openapierrors "k8s.io/kube-openapi/pkg/validation/errors" "k8s.io/kube-openapi/pkg/validation/spec" @@ -33,22 +34,60 @@ import ( type SchemaValidator interface { SchemaCreateValidator - ValidateUpdate(new, old interface{}) *validate.Result + ValidateUpdate(new, old interface{}, options ...ValidationOption) *validate.Result } type SchemaCreateValidator interface { - Validate(value interface{}) *validate.Result + Validate(value interface{}, options ...ValidationOption) *validate.Result +} + +type ValidationOptions struct { + // Whether errors from unchanged portions of the schema should be ratcheted + // This field is ignored for Validate + Ratcheting bool + + // Correlation between old and new arguments. + // If set, this is expected to be the correlation between the `new` and + // `old` arguments to ValidateUpdate, and values for `new` and `old` will + // be taken from the correlation. + // + // This field is ignored for Validate + // + // Used for ratcheting, but left as a separate field since it may be used + // for other purposes in the future. + CorrelatedObject *common.CorrelatedObject +} + +type ValidationOption func(*ValidationOptions) + +func NewValidationOptions(opts ...ValidationOption) ValidationOptions { + options := ValidationOptions{} + for _, opt := range opts { + opt(&options) + } + return options +} + +func WithRatcheting(correlation *common.CorrelatedObject) ValidationOption { + return func(options *ValidationOptions) { + options.Ratcheting = true + options.CorrelatedObject = correlation + } } // basicSchemaValidator wraps a kube-openapi SchemaCreateValidator to // support ValidateUpdate. It implements ValidateUpdate by simply validating -// the new value via kube-openapi, ignoring the old value. +// the new value via kube-openapi, ignoring the old value type basicSchemaValidator struct { *validate.SchemaValidator } -func (s basicSchemaValidator) ValidateUpdate(new, old interface{}) *validate.Result { - return s.Validate(new) +func (s basicSchemaValidator) Validate(new interface{}, options ...ValidationOption) *validate.Result { + return s.SchemaValidator.Validate(new) +} + +func (s basicSchemaValidator) ValidateUpdate(new, old interface{}, options ...ValidationOption) *validate.Result { + return s.Validate(new, options...) } // NewSchemaValidator creates an openapi schema validator for the given CRD validation. @@ -80,7 +119,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.JSONSchemaProps) // // If feature `CRDValidationRatcheting` is disabled, this behaves identically to // ValidateCustomResource(customResource). -func ValidateCustomResourceUpdate(fldPath *field.Path, customResource, old interface{}, validator SchemaValidator) field.ErrorList { +func ValidateCustomResourceUpdate(fldPath *field.Path, customResource, old interface{}, validator SchemaValidator, options ...ValidationOption) field.ErrorList { // Additional feature gate check for sanity if !utilfeature.DefaultFeatureGate.Enabled(features.CRDValidationRatcheting) { return ValidateCustomResource(nil, customResource, validator) @@ -88,7 +127,7 @@ func ValidateCustomResourceUpdate(fldPath *field.Path, customResource, old inter return nil } - result := validator.ValidateUpdate(customResource, old) + result := validator.ValidateUpdate(customResource, old, options...) if result.IsValid() { return nil } @@ -98,12 +137,12 @@ func ValidateCustomResourceUpdate(fldPath *field.Path, customResource, old inter // ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition. // CustomResource is a JSON data structure. -func ValidateCustomResource(fldPath *field.Path, customResource interface{}, validator SchemaCreateValidator) field.ErrorList { +func ValidateCustomResource(fldPath *field.Path, customResource interface{}, validator SchemaCreateValidator, options ...ValidationOption) field.ErrorList { if validator == nil { return nil } - result := validator.Validate(customResource) + result := validator.Validate(customResource, options...) if result.IsValid() { return nil } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go index a1a15108d6a..adcb5c22024 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/validator.go @@ -58,7 +58,7 @@ func (a customResourceValidator) Validate(ctx context.Context, obj *unstructured return allErrs } -func (a customResourceValidator) ValidateUpdate(ctx context.Context, obj, old *unstructured.Unstructured, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList { +func (a customResourceValidator) ValidateUpdate(ctx context.Context, obj, old *unstructured.Unstructured, scale *apiextensions.CustomResourceSubresourceScale, options ...apiextensionsvalidation.ValidationOption) field.ErrorList { if errs := a.ValidateTypeMeta(ctx, obj); len(errs) > 0 { return errs } @@ -66,7 +66,7 @@ func (a customResourceValidator) ValidateUpdate(ctx context.Context, obj, old *u var allErrs field.ErrorList allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(obj, old, field.NewPath("metadata"))...) - allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResourceUpdate(nil, obj.UnstructuredContent(), old.UnstructuredContent(), a.schemaValidator)...) + allErrs = append(allErrs, apiextensionsvalidation.ValidateCustomResourceUpdate(nil, obj.UnstructuredContent(), old.UnstructuredContent(), a.schemaValidator, options...)...) allErrs = append(allErrs, a.ValidateScaleSpec(ctx, obj, scale)...) allErrs = append(allErrs, a.ValidateScaleStatus(ctx, obj, scale)...)