...

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

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

     1  /*
     2  Copyright 2019 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 schema
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"regexp"
    23  	"sort"
    24  
    25  	"k8s.io/apimachinery/pkg/util/validation/field"
    26  )
    27  
    28  var intOrStringAnyOf = []NestedValueValidation{
    29  	{ForbiddenGenerics: Generic{
    30  		Type: "integer",
    31  	}},
    32  	{ForbiddenGenerics: Generic{
    33  		Type: "string",
    34  	}},
    35  }
    36  
    37  type level int
    38  
    39  const (
    40  	rootLevel level = iota
    41  	itemLevel
    42  	fieldLevel
    43  )
    44  
    45  // ValidateStructural checks that s is a structural schema with the invariants:
    46  //
    47  // * structurality: both `ForbiddenGenerics` and `ForbiddenExtensions` only have zero values, with the two exceptions for IntOrString.
    48  // * RawExtension: for every schema with `x-kubernetes-embedded-resource: true`, `x-kubernetes-preserve-unknown-fields: true` and `type: object` are set
    49  // * IntOrString: for `x-kubernetes-int-or-string: true` either `type` is empty under `anyOf` and `allOf` or the schema structure is one of these:
    50  //
    51  //  1. anyOf:
    52  //     - type: integer
    53  //     - type: string
    54  //  2. allOf:
    55  //     - anyOf:
    56  //     - type: integer
    57  //     - type: string
    58  //     - ... zero or more
    59  //
    60  // * every specified field or array in s is also specified outside of value validation.
    61  // * metadata at the root can only restrict the name and generateName, and not be specified at all in nested contexts.
    62  // * additionalProperties at the root is not allowed.
    63  func ValidateStructural(fldPath *field.Path, s *Structural) field.ErrorList {
    64  	allErrs := field.ErrorList{}
    65  
    66  	allErrs = append(allErrs, validateStructuralInvariants(s, rootLevel, fldPath)...)
    67  	allErrs = append(allErrs, validateStructuralCompleteness(s, fldPath)...)
    68  
    69  	// sort error messages. Otherwise, the errors slice will change every time due to
    70  	// maps in the types and randomized iteration.
    71  	sort.Slice(allErrs, func(i, j int) bool {
    72  		return allErrs[i].Error() < allErrs[j].Error()
    73  	})
    74  
    75  	return allErrs
    76  }
    77  
    78  // validateStructuralInvariants checks the invariants of a structural schema.
    79  func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path) field.ErrorList {
    80  	if s == nil {
    81  		return nil
    82  	}
    83  
    84  	allErrs := field.ErrorList{}
    85  
    86  	if s.Type == "array" && s.Items == nil {
    87  		allErrs = append(allErrs, field.Required(fldPath.Child("items"), "must be specified"))
    88  	}
    89  	allErrs = append(allErrs, validateStructuralInvariants(s.Items, itemLevel, fldPath.Child("items"))...)
    90  
    91  	for k, v := range s.Properties {
    92  		allErrs = append(allErrs, validateStructuralInvariants(&v, fieldLevel, fldPath.Child("properties").Key(k))...)
    93  	}
    94  	allErrs = append(allErrs, validateGeneric(&s.Generic, lvl, fldPath)...)
    95  	allErrs = append(allErrs, validateExtensions(&s.Extensions, fldPath)...)
    96  
    97  	// detect the two IntOrString exceptions:
    98  	// 1) anyOf:
    99  	//    - type: integer
   100  	//    - type: string
   101  	// 2) allOf:
   102  	//    - anyOf:
   103  	//      - type: integer
   104  	//      - type: string
   105  	//    - ... zero or more
   106  	skipAnyOf := isIntOrStringAnyOfPattern(s)
   107  	skipFirstAllOfAnyOf := isIntOrStringAllOfPattern(s)
   108  
   109  	allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...)
   110  
   111  	checkMetadata := (lvl == rootLevel) || s.XEmbeddedResource
   112  
   113  	if s.XEmbeddedResource && s.Type != "object" {
   114  		if len(s.Type) == 0 {
   115  			allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must be object if x-kubernetes-embedded-resource is true"))
   116  		} else {
   117  			allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object if x-kubernetes-embedded-resource is true"))
   118  		}
   119  	} else if len(s.Type) == 0 && !s.Extensions.XIntOrString && !s.Extensions.XPreserveUnknownFields {
   120  		switch lvl {
   121  		case rootLevel:
   122  			allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty at the root"))
   123  		case itemLevel:
   124  			allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified array items"))
   125  		case fieldLevel:
   126  			allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified object fields"))
   127  		}
   128  	}
   129  	if s.XEmbeddedResource && s.AdditionalProperties != nil {
   130  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must not be used if x-kubernetes-embedded-resource is set"))
   131  	}
   132  
   133  	if lvl == rootLevel && len(s.Type) > 0 && s.Type != "object" {
   134  		allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root"))
   135  	}
   136  
   137  	// restrict metadata schemas to name and generateName only
   138  	if kind, found := s.Properties["kind"]; found && checkMetadata {
   139  		if kind.Type != "string" {
   140  			allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key("kind").Child("type"), kind.Type, "must be string"))
   141  		}
   142  	}
   143  	if apiVersion, found := s.Properties["apiVersion"]; found && checkMetadata {
   144  		if apiVersion.Type != "string" {
   145  			allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key("apiVersion").Child("type"), apiVersion.Type, "must be string"))
   146  		}
   147  	}
   148  
   149  	if metadata, found := s.Properties["metadata"]; found {
   150  		allErrs = append(allErrs, validateStructuralMetadataInvariants(&metadata, checkMetadata, lvl, fldPath.Child("properties").Key("metadata"))...)
   151  	}
   152  
   153  	if s.XEmbeddedResource && !s.XPreserveUnknownFields && len(s.Properties) == 0 {
   154  		allErrs = append(allErrs, field.Required(fldPath.Child("properties"), "must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields"))
   155  	}
   156  
   157  	return allErrs
   158  }
   159  
   160  func validateStructuralMetadataInvariants(s *Structural, checkMetadata bool, lvl level, fldPath *field.Path) field.ErrorList {
   161  	allErrs := field.ErrorList{}
   162  
   163  	if checkMetadata && s.Type != "object" {
   164  		allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object"))
   165  	}
   166  
   167  	if lvl == rootLevel {
   168  		// metadata is a shallow copy. We can mutate it.
   169  		_, foundName := s.Properties["name"]
   170  		_, foundGenerateName := s.Properties["generateName"]
   171  		if foundName && foundGenerateName && len(s.Properties) == 2 {
   172  			s.Properties = nil
   173  		} else if (foundName || foundGenerateName) && len(s.Properties) == 1 {
   174  			s.Properties = nil
   175  		}
   176  		s.Type = ""
   177  		s.Default.Object = nil // this is checked in API validation (and also tested)
   178  		if s.ValueValidation == nil {
   179  			s.ValueValidation = &ValueValidation{}
   180  		}
   181  		if !reflect.DeepEqual(*s, Structural{ValueValidation: &ValueValidation{}}) {
   182  			// TODO: this is actually a field.Invalid error, but we cannot do JSON serialization of metadata here to get a proper message
   183  			allErrs = append(allErrs, field.Forbidden(fldPath, "must not specify anything other than name and generateName, but metadata is implicitly specified"))
   184  		}
   185  	}
   186  
   187  	return allErrs
   188  }
   189  
   190  func isIntOrStringAnyOfPattern(s *Structural) bool {
   191  	if s == nil || s.ValueValidation == nil {
   192  		return false
   193  	}
   194  	return len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf)
   195  }
   196  
   197  func isIntOrStringAllOfPattern(s *Structural) bool {
   198  	if s == nil || s.ValueValidation == nil {
   199  		return false
   200  	}
   201  	return len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf)
   202  }
   203  
   204  // validateGeneric checks the generic fields of a structural schema.
   205  func validateGeneric(g *Generic, lvl level, fldPath *field.Path) field.ErrorList {
   206  	if g == nil {
   207  		return nil
   208  	}
   209  
   210  	allErrs := field.ErrorList{}
   211  
   212  	if g.AdditionalProperties != nil {
   213  		if lvl == rootLevel {
   214  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must not be used at the root"))
   215  		}
   216  		if g.AdditionalProperties.Structural != nil {
   217  			allErrs = append(allErrs, validateStructuralInvariants(g.AdditionalProperties.Structural, fieldLevel, fldPath.Child("additionalProperties"))...)
   218  		}
   219  	}
   220  
   221  	return allErrs
   222  }
   223  
   224  // validateExtensions checks Kubernetes vendor extensions of a structural schema.
   225  func validateExtensions(x *Extensions, fldPath *field.Path) field.ErrorList {
   226  	allErrs := field.ErrorList{}
   227  
   228  	if x.XIntOrString && x.XPreserveUnknownFields {
   229  		allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-preserve-unknown-fields"), x.XPreserveUnknownFields, "must be false if x-kubernetes-int-or-string is true"))
   230  	}
   231  	if x.XIntOrString && x.XEmbeddedResource {
   232  		allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-embedded-resource"), x.XEmbeddedResource, "must be false if x-kubernetes-int-or-string is true"))
   233  	}
   234  
   235  	return allErrs
   236  }
   237  
   238  // validateValueValidation checks the value validation in a structural schema.
   239  func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList {
   240  	if v == nil {
   241  		return nil
   242  	}
   243  
   244  	allErrs := field.ErrorList{}
   245  
   246  	if !skipAnyOf {
   247  		for i := range v.AnyOf {
   248  			allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, lvl, fldPath.Child("anyOf").Index(i))...)
   249  		}
   250  	}
   251  
   252  	for i := range v.AllOf {
   253  		skipAnyOf := false
   254  		if skipFirstAllOfAnyOf && i == 0 {
   255  			skipAnyOf = true
   256  		}
   257  		allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, lvl, fldPath.Child("allOf").Index(i))...)
   258  	}
   259  
   260  	for i := range v.OneOf {
   261  		allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, lvl, fldPath.Child("oneOf").Index(i))...)
   262  	}
   263  
   264  	allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, lvl, fldPath.Child("not"))...)
   265  
   266  	if len(v.Pattern) > 0 {
   267  		if _, err := regexp.Compile(v.Pattern); err != nil {
   268  			allErrs = append(allErrs, field.Invalid(fldPath.Child("pattern"), v.Pattern, fmt.Sprintf("must be a valid regular expression, but isn't: %v", err)))
   269  		}
   270  	}
   271  
   272  	return allErrs
   273  }
   274  
   275  // validateNestedValueValidation checks the nested value validation under a logic junctor in a structural schema.
   276  func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList {
   277  	if v == nil {
   278  		return nil
   279  	}
   280  
   281  	allErrs := field.ErrorList{}
   282  
   283  	allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, lvl, fldPath)...)
   284  	allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, lvl, fldPath.Child("items"))...)
   285  
   286  	for k, fld := range v.Properties {
   287  		allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fieldLevel, fldPath.Child("properties").Key(k))...)
   288  	}
   289  
   290  	if len(v.ForbiddenGenerics.Type) > 0 {
   291  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "must be empty to be structural"))
   292  	}
   293  	if v.ForbiddenGenerics.AdditionalProperties != nil {
   294  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must be undefined to be structural"))
   295  	}
   296  	if v.ForbiddenGenerics.Default.Object != nil {
   297  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "must be undefined to be structural"))
   298  	}
   299  	if len(v.ForbiddenGenerics.Title) > 0 {
   300  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("title"), "must be empty to be structural"))
   301  	}
   302  	if len(v.ForbiddenGenerics.Description) > 0 {
   303  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("description"), "must be empty to be structural"))
   304  	}
   305  	if v.ForbiddenGenerics.Nullable {
   306  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("nullable"), "must be false to be structural"))
   307  	}
   308  
   309  	if v.ForbiddenExtensions.XPreserveUnknownFields {
   310  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-preserve-unknown-fields"), "must be false to be structural"))
   311  	}
   312  	if v.ForbiddenExtensions.XEmbeddedResource {
   313  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-embedded-resource"), "must be false to be structural"))
   314  	}
   315  	if v.ForbiddenExtensions.XIntOrString {
   316  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural"))
   317  	}
   318  	if len(v.ForbiddenExtensions.XListMapKeys) > 0 {
   319  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-list-map-keys"), "must be empty to be structural"))
   320  	}
   321  	if v.ForbiddenExtensions.XListType != nil {
   322  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-list-type"), "must be undefined to be structural"))
   323  	}
   324  	if v.ForbiddenExtensions.XMapType != nil {
   325  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-map-type"), "must be undefined to be structural"))
   326  	}
   327  	if len(v.ForbiddenExtensions.XValidations) > 0 {
   328  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-validations"), "must be empty to be structural"))
   329  	}
   330  
   331  	// forbid reasoning about metadata because it can lead to metadata restriction we don't want
   332  	if _, found := v.Properties["metadata"]; found {
   333  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("properties").Key("metadata"), "must not be specified in a nested context"))
   334  	}
   335  
   336  	return allErrs
   337  }
   338  

View as plain text