k8s.io/apiserver@v0.31.1/pkg/cel/common/schemas.go (about)

     1  /*
     2  Copyright 2022 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 common
    18  
    19  import (
    20  	"time"
    21  
    22  	"github.com/google/cel-go/cel"
    23  	"github.com/google/cel-go/common/types"
    24  
    25  	apiservercel "k8s.io/apiserver/pkg/cel"
    26  	"k8s.io/kube-openapi/pkg/validation/spec"
    27  )
    28  
    29  const maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
    30  
    31  // SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
    32  // structural schema should not be exposed in CEL expressions.
    33  // Set isResourceRoot to true for the root of a custom resource or embedded resource.
    34  //
    35  // Schemas with XPreserveUnknownFields not exposed unless they are objects. Array and "maps" schemas
    36  // are not exposed if their items or additionalProperties schemas are not exposed. Object Properties are not exposed
    37  // if their schema is not exposed.
    38  //
    39  // The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
    40  func SchemaDeclType(s Schema, isResourceRoot bool) *apiservercel.DeclType {
    41  	if s == nil {
    42  		return nil
    43  	}
    44  	if s.IsXIntOrString() {
    45  		// schemas using XIntOrString are not required to have a type.
    46  
    47  		// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
    48  		// In CEL, the type is represented as dynamic value, which can be thought of as a union type of all types.
    49  		// All type checking for XIntOrString is deferred to runtime, so all access to values of this type must
    50  		// be guarded with a type check, e.g.:
    51  		//
    52  		// To require that the string representation be a percentage:
    53  		//  `type(intOrStringField) == string && intOrStringField.matches(r'(\d+(\.\d+)?%)')`
    54  		// To validate requirements on both the int and string representation:
    55  		//  `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5
    56  		//
    57  		dyn := apiservercel.NewSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1) // smallest value for a serialized x-kubernetes-int-or-string is 0
    58  		// handle x-kubernetes-int-or-string by returning the max length/min serialized size of the largest possible string
    59  		dyn.MaxElements = maxRequestSizeBytes - 2
    60  		return dyn
    61  	}
    62  
    63  	// We ignore XPreserveUnknownFields since we don't support validation rules on
    64  	// data that we don't have schema information for.
    65  
    66  	if isResourceRoot {
    67  		// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible to validator rules
    68  		// at the root of resources, even if not specified in the schema.
    69  		// This includes the root of a custom resource and the root of XEmbeddedResource objects.
    70  		s = s.WithTypeAndObjectMeta()
    71  	}
    72  
    73  	switch s.Type() {
    74  	case "array":
    75  		if s.Items() != nil {
    76  			itemsType := SchemaDeclType(s.Items(), s.Items().IsXEmbeddedResource())
    77  			if itemsType == nil {
    78  				return nil
    79  			}
    80  			var maxItems int64
    81  			if s.MaxItems() != nil {
    82  				maxItems = zeroIfNegative(*s.MaxItems())
    83  			} else {
    84  				maxItems = estimateMaxArrayItemsFromMinSize(itemsType.MinSerializedSize)
    85  			}
    86  			return apiservercel.NewListType(itemsType, maxItems)
    87  		}
    88  		return nil
    89  	case "object":
    90  		if s.AdditionalProperties() != nil && s.AdditionalProperties().Schema() != nil {
    91  			propsType := SchemaDeclType(s.AdditionalProperties().Schema(), s.AdditionalProperties().Schema().IsXEmbeddedResource())
    92  			if propsType != nil {
    93  				var maxProperties int64
    94  				if s.MaxProperties() != nil {
    95  					maxProperties = zeroIfNegative(*s.MaxProperties())
    96  				} else {
    97  					maxProperties = estimateMaxAdditionalPropertiesFromMinSize(propsType.MinSerializedSize)
    98  				}
    99  				return apiservercel.NewMapType(apiservercel.StringType, propsType, maxProperties)
   100  			}
   101  			return nil
   102  		}
   103  		fields := make(map[string]*apiservercel.DeclField, len(s.Properties()))
   104  
   105  		required := map[string]bool{}
   106  		if s.Required() != nil {
   107  			for _, f := range s.Required() {
   108  				required[f] = true
   109  			}
   110  		}
   111  		// an object will always be serialized at least as {}, so account for that
   112  		minSerializedSize := int64(2)
   113  		for name, prop := range s.Properties() {
   114  			var enumValues []interface{}
   115  			if prop.Enum() != nil {
   116  				for _, e := range prop.Enum() {
   117  					enumValues = append(enumValues, e)
   118  				}
   119  			}
   120  			if fieldType := SchemaDeclType(prop, prop.IsXEmbeddedResource()); fieldType != nil {
   121  				if propName, ok := apiservercel.Escape(name); ok {
   122  					fields[propName] = apiservercel.NewDeclField(propName, fieldType, required[name], enumValues, prop.Default())
   123  				}
   124  				// the min serialized size for an object is 2 (for {}) plus the min size of all its required
   125  				// properties
   126  				// only include required properties without a default value; default values are filled in
   127  				// server-side
   128  				if required[name] && prop.Default() == nil {
   129  					minSerializedSize += int64(len(name)) + fieldType.MinSerializedSize + 4
   130  				}
   131  			}
   132  		}
   133  		objType := apiservercel.NewObjectType("object", fields)
   134  		objType.MinSerializedSize = minSerializedSize
   135  		return objType
   136  	case "string":
   137  		switch s.Format() {
   138  		case "byte":
   139  			byteWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), apiservercel.MinStringSize)
   140  			if s.MaxLength() != nil {
   141  				byteWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength())
   142  			} else {
   143  				byteWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
   144  			}
   145  			return byteWithMaxLength
   146  		case "duration":
   147  			durationWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, int64(apiservercel.MinDurationSizeJSON))
   148  			durationWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
   149  			return durationWithMaxLength
   150  		case "date":
   151  			timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.JSONDateSize))
   152  			timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
   153  			return timestampWithMaxLength
   154  		case "date-time":
   155  			timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.MinDatetimeSizeJSON))
   156  			timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
   157  			return timestampWithMaxLength
   158  		}
   159  
   160  		strWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("string", cel.StringType, types.String(""), apiservercel.MinStringSize)
   161  		if s.MaxLength() != nil {
   162  			// multiply the user-provided max length by 4 in the case of an otherwise-untyped string
   163  			// we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points,
   164  			// but we need to reason about length for things like request size, so we use bytes in this code (and an individual
   165  			// unicode code point can be up to 4 bytes long)
   166  			strWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength()) * 4
   167  		} else {
   168  			if len(s.Enum()) > 0 {
   169  				strWithMaxLength.MaxElements = estimateMaxStringEnumLength(s)
   170  			} else {
   171  				strWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
   172  			}
   173  		}
   174  		return strWithMaxLength
   175  	case "boolean":
   176  		return apiservercel.BoolType
   177  	case "number":
   178  		return apiservercel.DoubleType
   179  	case "integer":
   180  		return apiservercel.IntType
   181  	}
   182  	return nil
   183  }
   184  
   185  func zeroIfNegative(v int64) int64 {
   186  	if v < 0 {
   187  		return 0
   188  	}
   189  	return v
   190  }
   191  
   192  // WithTypeAndObjectMeta ensures the kind, apiVersion and
   193  // metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed.
   194  func WithTypeAndObjectMeta(s *spec.Schema) *spec.Schema {
   195  	if s.Properties != nil &&
   196  		s.Properties["kind"].Type.Contains("string") &&
   197  		s.Properties["apiVersion"].Type.Contains("string") &&
   198  		s.Properties["metadata"].Type.Contains("object") &&
   199  		s.Properties["metadata"].Properties != nil &&
   200  		s.Properties["metadata"].Properties["name"].Type.Contains("string") &&
   201  		s.Properties["metadata"].Properties["generateName"].Type.Contains("string") {
   202  		return s
   203  	}
   204  	result := *s
   205  	props := make(map[string]spec.Schema, len(s.Properties))
   206  	for k, prop := range s.Properties {
   207  		props[k] = prop
   208  	}
   209  	stringType := spec.StringProperty()
   210  	props["kind"] = *stringType
   211  	props["apiVersion"] = *stringType
   212  	props["metadata"] = spec.Schema{
   213  		SchemaProps: spec.SchemaProps{
   214  			Type: []string{"object"},
   215  			Properties: map[string]spec.Schema{
   216  				"name":         *stringType,
   217  				"generateName": *stringType,
   218  			},
   219  		},
   220  	}
   221  	result.Properties = props
   222  
   223  	return &result
   224  }
   225  
   226  // estimateMaxStringLengthPerRequest estimates the maximum string length (in characters)
   227  // of a string compatible with the format requirements in the provided schema.
   228  // must only be called on schemas of type "string" or x-kubernetes-int-or-string: true
   229  func estimateMaxStringLengthPerRequest(s Schema) int64 {
   230  	if s.IsXIntOrString() {
   231  		return maxRequestSizeBytes - 2
   232  	}
   233  	switch s.Format() {
   234  	case "duration":
   235  		return apiservercel.MaxDurationSizeJSON
   236  	case "date":
   237  		return apiservercel.JSONDateSize
   238  	case "date-time":
   239  		return apiservercel.MaxDatetimeSizeJSON
   240  	default:
   241  		// subtract 2 to account for ""
   242  		return maxRequestSizeBytes - 2
   243  	}
   244  }
   245  
   246  // estimateMaxStringLengthPerRequest estimates the maximum string length (in characters)
   247  // that has a set of enum values.
   248  // The result of the estimation is the length of the longest possible value.
   249  func estimateMaxStringEnumLength(s Schema) int64 {
   250  	var maxLength int64
   251  	for _, v := range s.Enum() {
   252  		if s, ok := v.(string); ok && int64(len(s)) > maxLength {
   253  			maxLength = int64(len(s))
   254  		}
   255  	}
   256  	return maxLength
   257  }
   258  
   259  // estimateMaxArrayItemsPerRequest estimates the maximum number of array items with
   260  // the provided minimum serialized size that can fit into a single request.
   261  func estimateMaxArrayItemsFromMinSize(minSize int64) int64 {
   262  	// subtract 2 to account for [ and ]
   263  	return (maxRequestSizeBytes - 2) / (minSize + 1)
   264  }
   265  
   266  // estimateMaxAdditionalPropertiesPerRequest estimates the maximum number of additional properties
   267  // with the provided minimum serialized size that can fit into a single request.
   268  func estimateMaxAdditionalPropertiesFromMinSize(minSize int64) int64 {
   269  	// 2 bytes for key + "" + colon + comma + smallest possible value, realistically the actual keys
   270  	// will all vary in length
   271  	keyValuePairSize := minSize + 6
   272  	// subtract 2 to account for { and }
   273  	return (maxRequestSizeBytes - 2) / keyValuePairSize
   274  }