sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/crd/markers/validation.go (about) 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 markers 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "math" 23 "strings" 24 25 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 26 27 "sigs.k8s.io/controller-tools/pkg/markers" 28 ) 29 30 const ( 31 validationPrefix = "kubebuilder:validation:" 32 33 SchemalessName = "kubebuilder:validation:Schemaless" 34 ValidationItemsPrefix = validationPrefix + "items:" 35 ) 36 37 // ValidationMarkers lists all available markers that affect CRD schema generation, 38 // except for the few that don't make sense as type-level markers (see FieldOnlyMarkers). 39 // All markers start with `+kubebuilder:validation:`, and continue with their type name. 40 // A copy is produced of all markers that describes types as well, for making types 41 // reusable and writing complex validations on slice items. 42 // At last a copy of all markers with the prefix `+kubebuilder:validation:items:` is 43 // produced for marking slice fields and types. 44 var ValidationMarkers = mustMakeAllWithPrefix(validationPrefix, markers.DescribesField, 45 46 // numeric markers 47 48 Maximum(0), 49 Minimum(0), 50 ExclusiveMaximum(false), 51 ExclusiveMinimum(false), 52 MultipleOf(0), 53 MinProperties(0), 54 MaxProperties(0), 55 56 // string markers 57 58 MaxLength(0), 59 MinLength(0), 60 Pattern(""), 61 62 // slice markers 63 64 MaxItems(0), 65 MinItems(0), 66 UniqueItems(false), 67 68 // general markers 69 70 Enum(nil), 71 Format(""), 72 Type(""), 73 XPreserveUnknownFields{}, 74 XEmbeddedResource{}, 75 XIntOrString{}, 76 XValidation{}, 77 ) 78 79 // FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make 80 // sense on a type, and thus aren't in ValidationMarkers). 81 var FieldOnlyMarkers = []*definitionWithHelp{ 82 must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})). 83 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required.")), 84 must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})). 85 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional.")), 86 must(markers.MakeDefinition("required", markers.DescribesField, struct{}{})). 87 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required.")), 88 must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})). 89 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional.")), 90 91 must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})). 92 WithHelp(Nullable{}.Help()), 93 94 must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})). 95 WithHelp(Default{}.Help()), 96 must(markers.MakeDefinition("default", markers.DescribesField, KubernetesDefault{})). 97 WithHelp(KubernetesDefault{}.Help()), 98 99 must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})). 100 WithHelp(Example{}.Help()), 101 102 must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})). 103 WithHelp(XEmbeddedResource{}.Help()), 104 105 must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})). 106 WithHelp(Schemaless{}.Help()), 107 } 108 109 // ValidationIshMarkers are field-and-type markers that don't fall under the 110 // :validation: prefix, and/or don't have a name that directly matches their 111 // type. 112 var ValidationIshMarkers = []*definitionWithHelp{ 113 must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})). 114 WithHelp(XPreserveUnknownFields{}.Help()), 115 must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})). 116 WithHelp(XPreserveUnknownFields{}.Help()), 117 } 118 119 func init() { 120 AllDefinitions = append(AllDefinitions, ValidationMarkers...) 121 122 for _, def := range ValidationMarkers { 123 typDef := def.clone() 124 typDef.Target = markers.DescribesType 125 AllDefinitions = append(AllDefinitions, typDef) 126 127 itemsName := ValidationItemsPrefix + strings.TrimPrefix(def.Name, validationPrefix) 128 129 itemsFieldDef := def.clone() 130 itemsFieldDef.Name = itemsName 131 itemsFieldDef.Help.Summary = "for array items " + itemsFieldDef.Help.Summary 132 AllDefinitions = append(AllDefinitions, itemsFieldDef) 133 134 itemsTypDef := def.clone() 135 itemsTypDef.Name = itemsName 136 itemsTypDef.Help.Summary = "for array items " + itemsTypDef.Help.Summary 137 itemsTypDef.Target = markers.DescribesType 138 AllDefinitions = append(AllDefinitions, itemsTypDef) 139 } 140 141 AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...) 142 AllDefinitions = append(AllDefinitions, ValidationIshMarkers...) 143 } 144 145 // +controllertools:marker:generateHelp:category="CRD validation" 146 // Maximum specifies the maximum numeric value that this field can have. 147 type Maximum float64 148 149 func (m Maximum) Value() float64 { 150 return float64(m) 151 } 152 153 // +controllertools:marker:generateHelp:category="CRD validation" 154 // Minimum specifies the minimum numeric value that this field can have. Negative numbers are supported. 155 type Minimum float64 156 157 func (m Minimum) Value() float64 { 158 return float64(m) 159 } 160 161 // +controllertools:marker:generateHelp:category="CRD validation" 162 // ExclusiveMinimum indicates that the minimum is "up to" but not including that value. 163 type ExclusiveMinimum bool 164 165 // +controllertools:marker:generateHelp:category="CRD validation" 166 // ExclusiveMaximum indicates that the maximum is "up to" but not including that value. 167 type ExclusiveMaximum bool 168 169 // +controllertools:marker:generateHelp:category="CRD validation" 170 // MultipleOf specifies that this field must have a numeric value that's a multiple of this one. 171 type MultipleOf float64 172 173 func (m MultipleOf) Value() float64 { 174 return float64(m) 175 } 176 177 // +controllertools:marker:generateHelp:category="CRD validation" 178 // MaxLength specifies the maximum length for this string. 179 type MaxLength int 180 181 // +controllertools:marker:generateHelp:category="CRD validation" 182 // MinLength specifies the minimum length for this string. 183 type MinLength int 184 185 // +controllertools:marker:generateHelp:category="CRD validation" 186 // Pattern specifies that this string must match the given regular expression. 187 type Pattern string 188 189 // +controllertools:marker:generateHelp:category="CRD validation" 190 // MaxItems specifies the maximum length for this list. 191 type MaxItems int 192 193 // +controllertools:marker:generateHelp:category="CRD validation" 194 // MinItems specifies the minimum length for this list. 195 type MinItems int 196 197 // +controllertools:marker:generateHelp:category="CRD validation" 198 // UniqueItems specifies that all items in this list must be unique. 199 type UniqueItems bool 200 201 // +controllertools:marker:generateHelp:category="CRD validation" 202 // MaxProperties restricts the number of keys in an object 203 type MaxProperties int 204 205 // +controllertools:marker:generateHelp:category="CRD validation" 206 // MinProperties restricts the number of keys in an object 207 type MinProperties int 208 209 // +controllertools:marker:generateHelp:category="CRD validation" 210 // Enum specifies that this (scalar) field is restricted to the *exact* values specified here. 211 type Enum []interface{} 212 213 // +controllertools:marker:generateHelp:category="CRD validation" 214 // Format specifies additional "complex" formatting for this field. 215 // 216 // For example, a date-time field would be marked as "type: string" and 217 // "format: date-time". 218 type Format string 219 220 // +controllertools:marker:generateHelp:category="CRD validation" 221 // Type overrides the type for this field (which defaults to the equivalent of the Go type). 222 // 223 // This generally must be paired with custom serialization. For example, the 224 // metav1.Time field would be marked as "type: string" and "format: date-time". 225 type Type string 226 227 // +controllertools:marker:generateHelp:category="CRD validation" 228 // Nullable marks this field as allowing the "null" value. 229 // 230 // This is often not necessary, but may be helpful with custom serialization. 231 type Nullable struct{} 232 233 // +controllertools:marker:generateHelp:category="CRD validation" 234 // Default sets the default value for this field. 235 // 236 // A default value will be accepted as any value valid for the 237 // field. Formatting for common types include: boolean: `true`, string: 238 // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: 239 // "delete"}`). Defaults should be defined in pruned form, and only best-effort 240 // validation will be performed. Full validation of a default requires 241 // submission of the containing CRD to an apiserver. 242 type Default struct { 243 Value interface{} 244 } 245 246 // +controllertools:marker:generateHelp:category="CRD validation" 247 // Default sets the default value for this field. 248 // 249 // A default value will be accepted as any value valid for the field. 250 // Only JSON-formatted values are accepted. `ref(...)` values are ignored. 251 // Formatting for common types include: boolean: `true`, string: 252 // `"Cluster"`, numerical: `1.24`, array: `[1,2]`, object: `{"policy": 253 // "delete"}`). Defaults should be defined in pruned form, and only best-effort 254 // validation will be performed. Full validation of a default requires 255 // submission of the containing CRD to an apiserver. 256 type KubernetesDefault struct { 257 Value interface{} 258 } 259 260 // +controllertools:marker:generateHelp:category="CRD validation" 261 // Example sets the example value for this field. 262 // 263 // An example value will be accepted as any value valid for the 264 // field. Formatting for common types include: boolean: `true`, string: 265 // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: 266 // "delete"}`). Examples should be defined in pruned form, and only best-effort 267 // validation will be performed. Full validation of an example requires 268 // submission of the containing CRD to an apiserver. 269 type Example struct { 270 Value interface{} 271 } 272 273 // +controllertools:marker:generateHelp:category="CRD processing" 274 // PreserveUnknownFields stops the apiserver from pruning fields which are not specified. 275 // 276 // By default the apiserver drops unknown fields from the request payload 277 // during the decoding step. This marker stops the API server from doing so. 278 // It affects fields recursively, but switches back to normal pruning behaviour 279 // if nested properties or additionalProperties are specified in the schema. 280 // This can either be true or undefined. False 281 // is forbidden. 282 // 283 // NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated 284 // in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function 285 // identically. 286 type XPreserveUnknownFields struct{} 287 288 // +controllertools:marker:generateHelp:category="CRD validation" 289 // EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields. 290 // 291 // An embedded resource is a value that has apiVersion, kind and metadata fields. 292 // They are validated implicitly according to the semantics of the currently 293 // running apiserver. It is not necessary to add any additional schema for these 294 // field, yet it is possible. This can be combined with PreserveUnknownFields. 295 type XEmbeddedResource struct{} 296 297 // +controllertools:marker:generateHelp:category="CRD validation" 298 // IntOrString marks a fields as an IntOrString. 299 // 300 // This is required when applying patterns or other validations to an IntOrString 301 // field. Knwon information about the type is applied during the collapse phase 302 // and as such is not normally available during marker application. 303 type XIntOrString struct{} 304 305 // +controllertools:marker:generateHelp:category="CRD validation" 306 // Schemaless marks a field as being a schemaless object. 307 // 308 // Schemaless objects are not introspected, so you must provide 309 // any type and validation information yourself. One use for this 310 // tag is for embedding fields that hold JSONSchema typed objects. 311 // Because this field disables all type checking, it is recommended 312 // to be used only as a last resort. 313 type Schemaless struct{} 314 315 func hasNumericType(schema *apiext.JSONSchemaProps) bool { 316 return schema.Type == "integer" || schema.Type == "number" 317 } 318 319 func isIntegral(value float64) bool { 320 return value == math.Trunc(value) && !math.IsNaN(value) && !math.IsInf(value, 0) 321 } 322 323 // +controllertools:marker:generateHelp:category="CRD validation" 324 // XValidation marks a field as requiring a value for which a given 325 // expression evaluates to true. 326 // 327 // This marker may be repeated to specify multiple expressions, all of 328 // which must evaluate to true. 329 type XValidation struct { 330 Rule string 331 Message string `marker:",optional"` 332 MessageExpression string `marker:"messageExpression,optional"` 333 Reason string `marker:"reason,optional"` 334 FieldPath string `marker:"fieldPath,optional"` 335 } 336 337 func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 338 if !hasNumericType(schema) { 339 return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type) 340 } 341 342 if schema.Type == "integer" && !isIntegral(m.Value()) { 343 return fmt.Errorf("cannot apply non-integral maximum validation (%v) to integer value", m.Value()) 344 } 345 346 val := m.Value() 347 schema.Maximum = &val 348 return nil 349 } 350 351 func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 352 if !hasNumericType(schema) { 353 return fmt.Errorf("must apply minimum to a numeric value, found %s", schema.Type) 354 } 355 356 if schema.Type == "integer" && !isIntegral(m.Value()) { 357 return fmt.Errorf("cannot apply non-integral minimum validation (%v) to integer value", m.Value()) 358 } 359 360 val := m.Value() 361 schema.Minimum = &val 362 return nil 363 } 364 365 func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 366 if !hasNumericType(schema) { 367 return fmt.Errorf("must apply exclusivemaximum to a numeric value, found %s", schema.Type) 368 } 369 schema.ExclusiveMaximum = bool(m) 370 return nil 371 } 372 373 func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 374 if !hasNumericType(schema) { 375 return fmt.Errorf("must apply exclusiveminimum to a numeric value, found %s", schema.Type) 376 } 377 378 schema.ExclusiveMinimum = bool(m) 379 return nil 380 } 381 382 func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 383 if !hasNumericType(schema) { 384 return fmt.Errorf("must apply multipleof to a numeric value, found %s", schema.Type) 385 } 386 387 if schema.Type == "integer" && !isIntegral(m.Value()) { 388 return fmt.Errorf("cannot apply non-integral multipleof validation (%v) to integer value", m.Value()) 389 } 390 391 val := m.Value() 392 schema.MultipleOf = &val 393 return nil 394 } 395 396 func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 397 if schema.Type != "string" { 398 return fmt.Errorf("must apply maxlength to a string") 399 } 400 val := int64(m) 401 schema.MaxLength = &val 402 return nil 403 } 404 405 func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 406 if schema.Type != "string" { 407 return fmt.Errorf("must apply minlength to a string") 408 } 409 val := int64(m) 410 schema.MinLength = &val 411 return nil 412 } 413 414 func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 415 // Allow string types or IntOrStrings. An IntOrString will still 416 // apply the pattern validation when a string is detected, the pattern 417 // will not apply to ints though. 418 if schema.Type != "string" && !schema.XIntOrString { 419 return fmt.Errorf("must apply pattern to a `string` or `IntOrString`") 420 } 421 schema.Pattern = string(m) 422 return nil 423 } 424 425 func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 426 if schema.Type != "array" { 427 return fmt.Errorf("must apply maxitem to an array") 428 } 429 val := int64(m) 430 schema.MaxItems = &val 431 return nil 432 } 433 434 func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 435 if schema.Type != "array" { 436 return fmt.Errorf("must apply minitems to an array") 437 } 438 val := int64(m) 439 schema.MinItems = &val 440 return nil 441 } 442 443 func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 444 if schema.Type != "array" { 445 return fmt.Errorf("must apply uniqueitems to an array") 446 } 447 schema.UniqueItems = bool(m) 448 return nil 449 } 450 451 func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 452 if schema.Type != "object" { 453 return fmt.Errorf("must apply minproperties to an object") 454 } 455 val := int64(m) 456 schema.MinProperties = &val 457 return nil 458 } 459 460 func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 461 if schema.Type != "object" { 462 return fmt.Errorf("must apply maxproperties to an object") 463 } 464 val := int64(m) 465 schema.MaxProperties = &val 466 return nil 467 } 468 469 func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 470 // TODO(directxman12): this is a bit hacky -- we should 471 // probably support AnyType better + using the schema structure 472 vals := make([]apiext.JSON, len(m)) 473 for i, val := range m { 474 // TODO(directxman12): check actual type with schema type? 475 // if we're expecting a string, marshal the string properly... 476 // NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly 477 valMarshalled, err := json.Marshal(val) 478 if err != nil { 479 return err 480 } 481 vals[i] = apiext.JSON{Raw: valMarshalled} 482 } 483 schema.Enum = vals 484 return nil 485 } 486 487 func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 488 schema.Format = string(m) 489 return nil 490 } 491 492 // NB(directxman12): we "typecheck" on target schema properties here, 493 // which means the "Type" marker *must* be applied first. 494 // TODO(directxman12): find a less hacky way to do this 495 // (we could preserve ordering of markers, but that feels bad in its own right). 496 497 func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 498 schema.Type = string(m) 499 return nil 500 } 501 502 func (m Type) ApplyPriority() ApplyPriority { 503 return ApplyPriorityDefault - 1 504 } 505 506 func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 507 schema.Nullable = true 508 return nil 509 } 510 511 // Defaults are only valid CRDs created with the v1 API 512 func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 513 marshalledDefault, err := json.Marshal(m.Value) 514 if err != nil { 515 return err 516 } 517 if schema.Type == "array" && string(marshalledDefault) == "{}" { 518 marshalledDefault = []byte("[]") 519 } 520 schema.Default = &apiext.JSON{Raw: marshalledDefault} 521 return nil 522 } 523 524 func (m Default) ApplyPriority() ApplyPriority { 525 // explicitly go after +default markers, so kubebuilder-specific defaults get applied last and stomp 526 return 10 527 } 528 529 func (m *KubernetesDefault) ParseMarker(_ string, _ string, restFields string) error { 530 if strings.HasPrefix(strings.TrimSpace(restFields), "ref(") { 531 // Skip +default=ref(...) values for now, since we don't have a good way to evaluate go constant values via AST. 532 // See https://github.com/kubernetes-sigs/controller-tools/pull/938#issuecomment-2096790018 533 return nil 534 } 535 return json.Unmarshal([]byte(restFields), &m.Value) 536 } 537 538 // Defaults are only valid CRDs created with the v1 API 539 func (m KubernetesDefault) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 540 if m.Value == nil { 541 // only apply to the schema if we have a non-nil default value 542 return nil 543 } 544 marshalledDefault, err := json.Marshal(m.Value) 545 if err != nil { 546 return err 547 } 548 schema.Default = &apiext.JSON{Raw: marshalledDefault} 549 return nil 550 } 551 552 func (m KubernetesDefault) ApplyPriority() ApplyPriority { 553 // explicitly go before +kubebuilder:default markers, so kubebuilder-specific defaults get applied last and stomp 554 return 9 555 } 556 557 func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 558 marshalledExample, err := json.Marshal(m.Value) 559 if err != nil { 560 return err 561 } 562 schema.Example = &apiext.JSON{Raw: marshalledExample} 563 return nil 564 } 565 566 func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 567 defTrue := true 568 schema.XPreserveUnknownFields = &defTrue 569 return nil 570 } 571 572 func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 573 schema.XEmbeddedResource = true 574 return nil 575 } 576 577 // NB(JoelSpeed): we use this property in other markers here, 578 // which means the "XIntOrString" marker *must* be applied first. 579 580 func (m XIntOrString) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 581 schema.XIntOrString = true 582 return nil 583 } 584 585 func (m XIntOrString) ApplyPriority() ApplyPriority { 586 return ApplyPriorityDefault - 1 587 } 588 589 func (m XValidation) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 590 var reason *apiext.FieldValueErrorReason 591 if m.Reason != "" { 592 switch m.Reason { 593 case string(apiext.FieldValueRequired), string(apiext.FieldValueInvalid), string(apiext.FieldValueForbidden), string(apiext.FieldValueDuplicate): 594 reason = (*apiext.FieldValueErrorReason)(&m.Reason) 595 default: 596 return fmt.Errorf("invalid reason %s, valid values are %s, %s, %s and %s", m.Reason, apiext.FieldValueRequired, apiext.FieldValueInvalid, apiext.FieldValueForbidden, apiext.FieldValueDuplicate) 597 } 598 } 599 600 schema.XValidations = append(schema.XValidations, apiext.ValidationRule{ 601 Rule: m.Rule, 602 Message: m.Message, 603 MessageExpression: m.MessageExpression, 604 Reason: reason, 605 FieldPath: m.FieldPath, 606 }) 607 return nil 608 }