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 }