1
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
46
47 Ratcheting bool
48
49
50
51
52
53
54
55
56
57
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
79
80
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
94
95
96
97
98
99
100 func NewSchemaValidator(customResourceValidation *apiextensions.JSONSchemaProps) (SchemaValidator, *spec.Schema, error) {
101
102 openapiSchema := &spec.Schema{}
103 if customResourceValidation != nil {
104
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
121
122
123
124
125
126 func ValidateCustomResourceUpdate(fldPath *field.Path, customResource, old interface{}, validator SchemaValidator, options ...ValidationOption) field.ErrorList {
127
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
143
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
240 func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
241 return ConvertJSONSchemaPropsWithPostProcess(in, out, nil)
242 }
243
244
245 type PostProcessFunc func(*spec.Schema) error
246
247
248
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