...

Source file src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go

Documentation: k8s.io/apiextensions-apiserver/pkg/apiserver/validation

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package validation
    18  
    19  import (
    20  	"encoding/json"
    21  	"strings"
    22  
    23  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    24  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    25  	"k8s.io/apiextensions-apiserver/pkg/features"
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  	"k8s.io/apiserver/pkg/cel/common"
    28  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    29  	openapierrors "k8s.io/kube-openapi/pkg/validation/errors"
    30  	"k8s.io/kube-openapi/pkg/validation/spec"
    31  	"k8s.io/kube-openapi/pkg/validation/strfmt"
    32  	"k8s.io/kube-openapi/pkg/validation/validate"
    33  )
    34  
    35  type SchemaValidator interface {
    36  	SchemaCreateValidator
    37  	ValidateUpdate(new, old interface{}, options ...ValidationOption) *validate.Result
    38  }
    39  
    40  type SchemaCreateValidator interface {
    41  	Validate(value interface{}, options ...ValidationOption) *validate.Result
    42  }
    43  
    44  type ValidationOptions struct {
    45  	// Whether errors from unchanged portions of the schema should be ratcheted
    46  	// This field is ignored for Validate
    47  	Ratcheting bool
    48  
    49  	// Correlation between old and new arguments.
    50  	// If set, this is expected to be the correlation between the `new` and
    51  	// `old` arguments to ValidateUpdate, and values for `new` and `old` will
    52  	// be taken from the correlation.
    53  	//
    54  	// This field is ignored for Validate
    55  	//
    56  	// Used for ratcheting, but left as a separate field since it may be used
    57  	// for other purposes in the future.
    58  	CorrelatedObject *common.CorrelatedObject
    59  }
    60  
    61  type ValidationOption func(*ValidationOptions)
    62  
    63  func NewValidationOptions(opts ...ValidationOption) ValidationOptions {
    64  	options := ValidationOptions{}
    65  	for _, opt := range opts {
    66  		opt(&options)
    67  	}
    68  	return options
    69  }
    70  
    71  func WithRatcheting(correlation *common.CorrelatedObject) ValidationOption {
    72  	return func(options *ValidationOptions) {
    73  		options.Ratcheting = true
    74  		options.CorrelatedObject = correlation
    75  	}
    76  }
    77  
    78  // basicSchemaValidator wraps a kube-openapi SchemaCreateValidator to
    79  // support ValidateUpdate. It implements ValidateUpdate by simply validating
    80  // the new value via kube-openapi, ignoring the old value
    81  type basicSchemaValidator struct {
    82  	*validate.SchemaValidator
    83  }
    84  
    85  func (s basicSchemaValidator) Validate(new interface{}, options ...ValidationOption) *validate.Result {
    86  	return s.SchemaValidator.Validate(new)
    87  }
    88  
    89  func (s basicSchemaValidator) ValidateUpdate(new, old interface{}, options ...ValidationOption) *validate.Result {
    90  	return s.Validate(new, options...)
    91  }
    92  
    93  // NewSchemaValidator creates an openapi schema validator for the given CRD validation.
    94  //
    95  // If feature `CRDValidationRatcheting` is disabled, this returns validator which
    96  // validates all `Update`s and `Create`s as a `Create` - without considering old value.
    97  //
    98  // If feature `CRDValidationRatcheting` is enabled - the validator returned
    99  // will support ratcheting unchanged correlatable fields across an update.
   100  func NewSchemaValidator(customResourceValidation *apiextensions.JSONSchemaProps) (SchemaValidator, *spec.Schema, error) {
   101  	// Convert CRD schema to openapi schema
   102  	openapiSchema := &spec.Schema{}
   103  	if customResourceValidation != nil {
   104  		// TODO: replace with NewStructural(...).ToGoOpenAPI
   105  		if err := ConvertJSONSchemaPropsWithPostProcess(customResourceValidation, openapiSchema, StripUnsupportedFormatsPostProcess); err != nil {
   106  			return nil, nil, err
   107  		}
   108  	}
   109  	return NewSchemaValidatorFromOpenAPI(openapiSchema), openapiSchema, nil
   110  }
   111  
   112  func NewSchemaValidatorFromOpenAPI(openapiSchema *spec.Schema) SchemaValidator {
   113  	if utilfeature.DefaultFeatureGate.Enabled(features.CRDValidationRatcheting) {
   114  		return NewRatchetingSchemaValidator(openapiSchema, nil, "", strfmt.Default)
   115  	}
   116  	return basicSchemaValidator{validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)}
   117  
   118  }
   119  
   120  // ValidateCustomResourceUpdate validates the transition of Custom Resource from
   121  // `old` to `new` against the schema in the CustomResourceDefinition.
   122  // Both customResource and old represent a JSON data structures.
   123  //
   124  // If feature `CRDValidationRatcheting` is disabled, this behaves identically to
   125  // ValidateCustomResource(customResource).
   126  func ValidateCustomResourceUpdate(fldPath *field.Path, customResource, old interface{}, validator SchemaValidator, options ...ValidationOption) field.ErrorList {
   127  	// Additional feature gate check for sanity
   128  	if !utilfeature.DefaultFeatureGate.Enabled(features.CRDValidationRatcheting) {
   129  		return ValidateCustomResource(nil, customResource, validator)
   130  	} else if validator == nil {
   131  		return nil
   132  	}
   133  
   134  	result := validator.ValidateUpdate(customResource, old, options...)
   135  	if result.IsValid() {
   136  		return nil
   137  	}
   138  
   139  	return kubeOpenAPIResultToFieldErrors(fldPath, result)
   140  }
   141  
   142  // ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
   143  // CustomResource is a JSON data structure.
   144  func ValidateCustomResource(fldPath *field.Path, customResource interface{}, validator SchemaCreateValidator, options ...ValidationOption) field.ErrorList {
   145  	if validator == nil {
   146  		return nil
   147  	}
   148  
   149  	result := validator.Validate(customResource, options...)
   150  	if result.IsValid() {
   151  		return nil
   152  	}
   153  
   154  	return kubeOpenAPIResultToFieldErrors(fldPath, result)
   155  }
   156  
   157  func kubeOpenAPIResultToFieldErrors(fldPath *field.Path, result *validate.Result) field.ErrorList {
   158  	var allErrs field.ErrorList
   159  	for _, err := range result.Errors {
   160  		switch err := err.(type) {
   161  
   162  		case *openapierrors.Validation:
   163  			errPath := fldPath
   164  			if len(err.Name) > 0 && err.Name != "." {
   165  				errPath = errPath.Child(strings.TrimPrefix(err.Name, "."))
   166  			}
   167  
   168  			switch err.Code() {
   169  			case openapierrors.RequiredFailCode:
   170  				allErrs = append(allErrs, field.Required(errPath, ""))
   171  
   172  			case openapierrors.EnumFailCode:
   173  				values := []string{}
   174  				for _, allowedValue := range err.Values {
   175  					if s, ok := allowedValue.(string); ok {
   176  						values = append(values, s)
   177  					} else {
   178  						allowedJSON, _ := json.Marshal(allowedValue)
   179  						values = append(values, string(allowedJSON))
   180  					}
   181  				}
   182  				allErrs = append(allErrs, field.NotSupported(errPath, err.Value, values))
   183  
   184  			case openapierrors.TooLongFailCode:
   185  				value := interface{}("")
   186  				if err.Value != nil {
   187  					value = err.Value
   188  				}
   189  				max := int64(-1)
   190  				if i, ok := err.Valid.(int64); ok {
   191  					max = i
   192  				}
   193  				allErrs = append(allErrs, field.TooLongMaxLength(errPath, value, int(max)))
   194  
   195  			case openapierrors.MaxItemsFailCode:
   196  				actual := int64(-1)
   197  				if i, ok := err.Value.(int64); ok {
   198  					actual = i
   199  				}
   200  				max := int64(-1)
   201  				if i, ok := err.Valid.(int64); ok {
   202  					max = i
   203  				}
   204  				allErrs = append(allErrs, field.TooMany(errPath, int(actual), int(max)))
   205  
   206  			case openapierrors.TooManyPropertiesCode:
   207  				actual := int64(-1)
   208  				if i, ok := err.Value.(int64); ok {
   209  					actual = i
   210  				}
   211  				max := int64(-1)
   212  				if i, ok := err.Valid.(int64); ok {
   213  					max = i
   214  				}
   215  				allErrs = append(allErrs, field.TooMany(errPath, int(actual), int(max)))
   216  
   217  			case openapierrors.InvalidTypeCode:
   218  				value := interface{}("")
   219  				if err.Value != nil {
   220  					value = err.Value
   221  				}
   222  				allErrs = append(allErrs, field.TypeInvalid(errPath, value, err.Error()))
   223  
   224  			default:
   225  				value := interface{}("")
   226  				if err.Value != nil {
   227  					value = err.Value
   228  				}
   229  				allErrs = append(allErrs, field.Invalid(errPath, value, err.Error()))
   230  			}
   231  
   232  		default:
   233  			allErrs = append(allErrs, field.Invalid(fldPath, "", err.Error()))
   234  		}
   235  	}
   236  	return allErrs
   237  }
   238  
   239  // ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema.
   240  func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
   241  	return ConvertJSONSchemaPropsWithPostProcess(in, out, nil)
   242  }
   243  
   244  // PostProcessFunc post-processes one node of a spec.Schema.
   245  type PostProcessFunc func(*spec.Schema) error
   246  
   247  // ConvertJSONSchemaPropsWithPostProcess converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
   248  // and run a post process step on each JSONSchemaProps node. postProcess is never called for nil schemas.
   249  func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, out *spec.Schema, postProcess PostProcessFunc) error {
   250  	if in == nil {
   251  		return nil
   252  	}
   253  
   254  	out.ID = in.ID
   255  	out.Schema = spec.SchemaURL(in.Schema)
   256  	out.Description = in.Description
   257  	if in.Type != "" {
   258  		out.Type = spec.StringOrArray([]string{in.Type})
   259  	}
   260  	if in.XIntOrString {
   261  		out.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true)
   262  		out.Type = spec.StringOrArray{"integer", "string"}
   263  	}
   264  	out.Nullable = in.Nullable
   265  	out.Format = in.Format
   266  	out.Title = in.Title
   267  	out.Maximum = in.Maximum
   268  	out.ExclusiveMaximum = in.ExclusiveMaximum
   269  	out.Minimum = in.Minimum
   270  	out.ExclusiveMinimum = in.ExclusiveMinimum
   271  	out.MaxLength = in.MaxLength
   272  	out.MinLength = in.MinLength
   273  	out.Pattern = in.Pattern
   274  	out.MaxItems = in.MaxItems
   275  	out.MinItems = in.MinItems
   276  	out.UniqueItems = in.UniqueItems
   277  	out.MultipleOf = in.MultipleOf
   278  	out.MaxProperties = in.MaxProperties
   279  	out.MinProperties = in.MinProperties
   280  	out.Required = in.Required
   281  
   282  	if in.Default != nil {
   283  		out.Default = *(in.Default)
   284  	}
   285  	if in.Example != nil {
   286  		out.Example = *(in.Example)
   287  	}
   288  
   289  	if in.Enum != nil {
   290  		out.Enum = make([]interface{}, len(in.Enum))
   291  		for k, v := range in.Enum {
   292  			out.Enum[k] = v
   293  		}
   294  	}
   295  
   296  	if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf, postProcess); err != nil {
   297  		return err
   298  	}
   299  	if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf, postProcess); err != nil {
   300  		return err
   301  	}
   302  	if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf, postProcess); err != nil {
   303  		return err
   304  	}
   305  
   306  	if in.Not != nil {
   307  		in, out := &in.Not, &out.Not
   308  		*out = new(spec.Schema)
   309  		if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
   310  			return err
   311  		}
   312  	}
   313  
   314  	var err error
   315  	out.Properties, err = convertMapOfJSONSchemaProps(in.Properties, postProcess)
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties, postProcess)
   321  	if err != nil {
   322  		return err
   323  	}
   324  
   325  	out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions, postProcess)
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	if in.Ref != nil {
   331  		out.Ref, err = spec.NewRef(*in.Ref)
   332  		if err != nil {
   333  			return err
   334  		}
   335  	}
   336  
   337  	if in.AdditionalProperties != nil {
   338  		in, out := &in.AdditionalProperties, &out.AdditionalProperties
   339  		*out = new(spec.SchemaOrBool)
   340  		if err := convertJSONSchemaPropsorBool(*in, *out, postProcess); err != nil {
   341  			return err
   342  		}
   343  	}
   344  
   345  	if in.AdditionalItems != nil {
   346  		in, out := &in.AdditionalItems, &out.AdditionalItems
   347  		*out = new(spec.SchemaOrBool)
   348  		if err := convertJSONSchemaPropsorBool(*in, *out, postProcess); err != nil {
   349  			return err
   350  		}
   351  	}
   352  
   353  	if in.Items != nil {
   354  		in, out := &in.Items, &out.Items
   355  		*out = new(spec.SchemaOrArray)
   356  		if err := convertJSONSchemaPropsOrArray(*in, *out, postProcess); err != nil {
   357  			return err
   358  		}
   359  	}
   360  
   361  	if in.Dependencies != nil {
   362  		in, out := &in.Dependencies, &out.Dependencies
   363  		*out = make(spec.Dependencies, len(*in))
   364  		for key, val := range *in {
   365  			newVal := new(spec.SchemaOrStringArray)
   366  			if err := convertJSONSchemaPropsOrStringArray(&val, newVal, postProcess); err != nil {
   367  				return err
   368  			}
   369  			(*out)[key] = *newVal
   370  		}
   371  	}
   372  
   373  	if in.ExternalDocs != nil {
   374  		out.ExternalDocs = &spec.ExternalDocumentation{}
   375  		out.ExternalDocs.Description = in.ExternalDocs.Description
   376  		out.ExternalDocs.URL = in.ExternalDocs.URL
   377  	}
   378  
   379  	if postProcess != nil {
   380  		if err := postProcess(out); err != nil {
   381  			return err
   382  		}
   383  	}
   384  
   385  	if in.XPreserveUnknownFields != nil {
   386  		out.VendorExtensible.AddExtension("x-kubernetes-preserve-unknown-fields", *in.XPreserveUnknownFields)
   387  	}
   388  	if in.XEmbeddedResource {
   389  		out.VendorExtensible.AddExtension("x-kubernetes-embedded-resource", true)
   390  	}
   391  	if len(in.XListMapKeys) != 0 {
   392  		out.VendorExtensible.AddExtension("x-kubernetes-list-map-keys", convertSliceToInterfaceSlice(in.XListMapKeys))
   393  	}
   394  	if in.XListType != nil {
   395  		out.VendorExtensible.AddExtension("x-kubernetes-list-type", *in.XListType)
   396  	}
   397  	if in.XMapType != nil {
   398  		out.VendorExtensible.AddExtension("x-kubernetes-map-type", *in.XMapType)
   399  	}
   400  	if len(in.XValidations) != 0 {
   401  		var serializationValidationRules apiextensionsv1.ValidationRules
   402  		if err := apiextensionsv1.Convert_apiextensions_ValidationRules_To_v1_ValidationRules(&in.XValidations, &serializationValidationRules, nil); err != nil {
   403  			return err
   404  		}
   405  		out.VendorExtensible.AddExtension("x-kubernetes-validations", convertSliceToInterfaceSlice(serializationValidationRules))
   406  	}
   407  	return nil
   408  }
   409  
   410  func convertSliceToInterfaceSlice[T any](in []T) []interface{} {
   411  	var res []interface{}
   412  	for _, v := range in {
   413  		res = append(res, v)
   414  	}
   415  	return res
   416  }
   417  
   418  func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema, postProcess PostProcessFunc) error {
   419  	if in != nil {
   420  		for _, jsonSchemaProps := range *in {
   421  			schema := spec.Schema{}
   422  			if err := ConvertJSONSchemaPropsWithPostProcess(&jsonSchemaProps, &schema, postProcess); err != nil {
   423  				return err
   424  			}
   425  			*out = append(*out, schema)
   426  		}
   427  	}
   428  	return nil
   429  }
   430  
   431  func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps, postProcess PostProcessFunc) (map[string]spec.Schema, error) {
   432  	if in == nil {
   433  		return nil, nil
   434  	}
   435  
   436  	out := make(map[string]spec.Schema)
   437  	for k, jsonSchemaProps := range in {
   438  		schema := spec.Schema{}
   439  		if err := ConvertJSONSchemaPropsWithPostProcess(&jsonSchemaProps, &schema, postProcess); err != nil {
   440  			return nil, err
   441  		}
   442  		out[k] = schema
   443  	}
   444  	return out, nil
   445  }
   446  
   447  func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray, postProcess PostProcessFunc) error {
   448  	if in.Schema != nil {
   449  		in, out := &in.Schema, &out.Schema
   450  		*out = new(spec.Schema)
   451  		if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
   452  			return err
   453  		}
   454  	}
   455  	if in.JSONSchemas != nil {
   456  		in, out := &in.JSONSchemas, &out.Schemas
   457  		*out = make([]spec.Schema, len(*in))
   458  		for i := range *in {
   459  			if err := ConvertJSONSchemaPropsWithPostProcess(&(*in)[i], &(*out)[i], postProcess); err != nil {
   460  				return err
   461  			}
   462  		}
   463  	}
   464  	return nil
   465  }
   466  
   467  func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool, postProcess PostProcessFunc) error {
   468  	out.Allows = in.Allows
   469  	if in.Schema != nil {
   470  		in, out := &in.Schema, &out.Schema
   471  		*out = new(spec.Schema)
   472  		if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
   473  			return err
   474  		}
   475  	}
   476  	return nil
   477  }
   478  
   479  func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray, postProcess PostProcessFunc) error {
   480  	out.Property = in.Property
   481  	if in.Schema != nil {
   482  		in, out := &in.Schema, &out.Schema
   483  		*out = new(spec.Schema)
   484  		if err := ConvertJSONSchemaPropsWithPostProcess(*in, *out, postProcess); err != nil {
   485  			return err
   486  		}
   487  	}
   488  	return nil
   489  }
   490  

View as plain text