github.com/regadas/controller-tools@v0.5.1-0.20210408091555-18885b17ff7b/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 "fmt" 21 22 "encoding/json" 23 24 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 25 26 "github.com/regadas/controller-tools/pkg/markers" 27 ) 28 29 const ( 30 SchemalessName = "kubebuilder:validation:Schemaless" 31 ) 32 33 // ValidationMarkers lists all available markers that affect CRD schema generation, 34 // except for the few that don't make sense as type-level markers (see FieldOnlyMarkers). 35 // All markers start with `+kubebuilder:validation:`, and continue with their type name. 36 // A copy is produced of all markers that describes types as well, for making types 37 // reusable and writing complex validations on slice items. 38 var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", markers.DescribesField, 39 40 // integer markers 41 42 Maximum(0), 43 Minimum(0), 44 ExclusiveMaximum(false), 45 ExclusiveMinimum(false), 46 MultipleOf(0), 47 MinProperties(0), 48 MaxProperties(0), 49 50 // string markers 51 52 MaxLength(0), 53 MinLength(0), 54 Pattern(""), 55 56 // slice markers 57 58 MaxItems(0), 59 MinItems(0), 60 UniqueItems(false), 61 62 // general markers 63 64 Enum(nil), 65 Format(""), 66 Type(""), 67 XPreserveUnknownFields{}, 68 XEmbeddedResource{}, 69 ) 70 71 // FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make 72 // sense on a type, and thus aren't in ValidationMarkers). 73 var FieldOnlyMarkers = []*definitionWithHelp{ 74 must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})). 75 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required, if fields are optional by default.")), 76 must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})). 77 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")), 78 must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})). 79 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")), 80 81 must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})). 82 WithHelp(Nullable{}.Help()), 83 84 must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})). 85 WithHelp(Default{}.Help()), 86 87 must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})). 88 WithHelp(XEmbeddedResource{}.Help()), 89 90 must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})). 91 WithHelp(Schemaless{}.Help()), 92 } 93 94 // ValidationIshMarkers are field-and-type markers that don't fall under the 95 // :validation: prefix, and/or don't have a name that directly matches their 96 // type. 97 var ValidationIshMarkers = []*definitionWithHelp{ 98 must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})). 99 WithHelp(XPreserveUnknownFields{}.Help()), 100 must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})). 101 WithHelp(XPreserveUnknownFields{}.Help()), 102 } 103 104 func init() { 105 AllDefinitions = append(AllDefinitions, ValidationMarkers...) 106 107 for _, def := range ValidationMarkers { 108 newDef := *def.Definition 109 // copy both parts so we don't change the definition 110 typDef := definitionWithHelp{ 111 Definition: &newDef, 112 Help: def.Help, 113 } 114 typDef.Target = markers.DescribesType 115 AllDefinitions = append(AllDefinitions, &typDef) 116 } 117 118 AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...) 119 AllDefinitions = append(AllDefinitions, ValidationIshMarkers...) 120 } 121 122 // +controllertools:marker:generateHelp:category="CRD validation" 123 // Maximum specifies the maximum numeric value that this field can have. 124 type Maximum int 125 126 // +controllertools:marker:generateHelp:category="CRD validation" 127 // Minimum specifies the minimum numeric value that this field can have. Negative integers are supported. 128 type Minimum int 129 130 // +controllertools:marker:generateHelp:category="CRD validation" 131 // ExclusiveMinimum indicates that the minimum is "up to" but not including that value. 132 type ExclusiveMinimum bool 133 134 // +controllertools:marker:generateHelp:category="CRD validation" 135 // ExclusiveMaximum indicates that the maximum is "up to" but not including that value. 136 type ExclusiveMaximum bool 137 138 // +controllertools:marker:generateHelp:category="CRD validation" 139 // MultipleOf specifies that this field must have a numeric value that's a multiple of this one. 140 type MultipleOf int 141 142 // +controllertools:marker:generateHelp:category="CRD validation" 143 // MaxLength specifies the maximum length for this string. 144 type MaxLength int 145 146 // +controllertools:marker:generateHelp:category="CRD validation" 147 // MinLength specifies the minimum length for this string. 148 type MinLength int 149 150 // +controllertools:marker:generateHelp:category="CRD validation" 151 // Pattern specifies that this string must match the given regular expression. 152 type Pattern string 153 154 // +controllertools:marker:generateHelp:category="CRD validation" 155 // MaxItems specifies the maximum length for this list. 156 type MaxItems int 157 158 // +controllertools:marker:generateHelp:category="CRD validation" 159 // MinItems specifies the minimun length for this list. 160 type MinItems int 161 162 // +controllertools:marker:generateHelp:category="CRD validation" 163 // UniqueItems specifies that all items in this list must be unique. 164 type UniqueItems bool 165 166 // +controllertools:marker:generateHelp:category="CRD validation" 167 // MaxProperties restricts the number of keys in an object 168 type MaxProperties int 169 170 // +controllertools:marker:generateHelp:category="CRD validation" 171 // MinProperties restricts the number of keys in an object 172 type MinProperties int 173 174 // +controllertools:marker:generateHelp:category="CRD validation" 175 // Enum specifies that this (scalar) field is restricted to the *exact* values specified here. 176 type Enum []interface{} 177 178 // +controllertools:marker:generateHelp:category="CRD validation" 179 // Format specifies additional "complex" formatting for this field. 180 // 181 // For example, a date-time field would be marked as "type: string" and 182 // "format: date-time". 183 type Format string 184 185 // +controllertools:marker:generateHelp:category="CRD validation" 186 // Type overrides the type for this field (which defaults to the equivalent of the Go type). 187 // 188 // This generally must be paired with custom serialization. For example, the 189 // metav1.Time field would be marked as "type: string" and "format: date-time". 190 type Type string 191 192 // +controllertools:marker:generateHelp:category="CRD validation" 193 // Nullable marks this field as allowing the "null" value. 194 // 195 // This is often not necessary, but may be helpful with custom serialization. 196 type Nullable struct{} 197 198 // +controllertools:marker:generateHelp:category="CRD validation" 199 // Default sets the default value for this field. 200 // 201 // A default value will be accepted as any value valid for the 202 // field. Formatting for common types include: boolean: `true`, string: 203 // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: 204 // "delete"}`). Defaults should be defined in pruned form, and only best-effort 205 // validation will be performed. Full validation of a default requires 206 // submission of the containing CRD to an apiserver. 207 type Default struct { 208 Value interface{} 209 } 210 211 // +controllertools:marker:generateHelp:category="CRD processing" 212 // PreserveUnknownFields stops the apiserver from pruning fields which are not specified. 213 // 214 // By default the apiserver drops unknown fields from the request payload 215 // during the decoding step. This marker stops the API server from doing so. 216 // It affects fields recursively, but switches back to normal pruning behaviour 217 // if nested properties or additionalProperties are specified in the schema. 218 // This can either be true or undefined. False 219 // is forbidden. 220 // 221 // NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated 222 // in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function 223 // identically. 224 type XPreserveUnknownFields struct{} 225 226 // +controllertools:marker:generateHelp:category="CRD validation" 227 // EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields. 228 // 229 // An embedded resource is a value that has apiVersion, kind and metadata fields. 230 // They are validated implicitly according to the semantics of the currently 231 // running apiserver. It is not necessary to add any additional schema for these 232 // field, yet it is possible. This can be combined with PreserveUnknownFields. 233 type XEmbeddedResource struct{} 234 235 // +controllertools:marker:generateHelp:category="CRD validation" 236 // Schemaless marks a field as being a schemaless object. 237 // 238 // Schemaless objects are not introspected, so you must provide 239 // any type and validation information yourself. One use for this 240 // tag is for embedding fields that hold JSONSchema typed objects. 241 // Because this field disables all type checking, it is recommended 242 // to be used only as a last resort. 243 type Schemaless struct{} 244 245 func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 246 if schema.Type != "integer" { 247 return fmt.Errorf("must apply maximum to an integer") 248 } 249 val := float64(m) 250 schema.Maximum = &val 251 return nil 252 } 253 func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 254 if schema.Type != "integer" { 255 return fmt.Errorf("must apply minimum to an integer") 256 } 257 val := float64(m) 258 schema.Minimum = &val 259 return nil 260 } 261 func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 262 if schema.Type != "integer" { 263 return fmt.Errorf("must apply exclusivemaximum to an integer") 264 } 265 schema.ExclusiveMaximum = bool(m) 266 return nil 267 } 268 func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 269 if schema.Type != "integer" { 270 return fmt.Errorf("must apply exclusiveminimum to an integer") 271 } 272 schema.ExclusiveMinimum = bool(m) 273 return nil 274 } 275 func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 276 if schema.Type != "integer" { 277 return fmt.Errorf("must apply multipleof to an integer") 278 } 279 val := float64(m) 280 schema.MultipleOf = &val 281 return nil 282 } 283 284 func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 285 if schema.Type != "string" { 286 return fmt.Errorf("must apply maxlength to a string") 287 } 288 val := int64(m) 289 schema.MaxLength = &val 290 return nil 291 } 292 func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 293 if schema.Type != "string" { 294 return fmt.Errorf("must apply minlength to a string") 295 } 296 val := int64(m) 297 schema.MinLength = &val 298 return nil 299 } 300 func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 301 if schema.Type != "string" { 302 return fmt.Errorf("must apply pattern to a string") 303 } 304 schema.Pattern = string(m) 305 return nil 306 } 307 308 func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 309 if schema.Type != "array" { 310 return fmt.Errorf("must apply maxitem to an array") 311 } 312 val := int64(m) 313 schema.MaxItems = &val 314 return nil 315 } 316 func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 317 if schema.Type != "array" { 318 return fmt.Errorf("must apply minitems to an array") 319 } 320 val := int64(m) 321 schema.MinItems = &val 322 return nil 323 } 324 func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 325 if schema.Type != "array" { 326 return fmt.Errorf("must apply uniqueitems to an array") 327 } 328 schema.UniqueItems = bool(m) 329 return nil 330 } 331 332 func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 333 if schema.Type != "object" { 334 return fmt.Errorf("must apply minproperties to an object") 335 } 336 val := int64(m) 337 schema.MinProperties = &val 338 return nil 339 } 340 341 func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 342 if schema.Type != "object" { 343 return fmt.Errorf("must apply maxproperties to an object") 344 } 345 val := int64(m) 346 schema.MaxProperties = &val 347 return nil 348 } 349 350 func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 351 // TODO(directxman12): this is a bit hacky -- we should 352 // probably support AnyType better + using the schema structure 353 vals := make([]apiext.JSON, len(m)) 354 for i, val := range m { 355 // TODO(directxman12): check actual type with schema type? 356 // if we're expecting a string, marshal the string properly... 357 // NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly 358 valMarshalled, err := json.Marshal(val) 359 if err != nil { 360 return err 361 } 362 vals[i] = apiext.JSON{Raw: valMarshalled} 363 } 364 schema.Enum = vals 365 return nil 366 } 367 func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 368 schema.Format = string(m) 369 return nil 370 } 371 372 // NB(directxman12): we "typecheck" on target schema properties here, 373 // which means the "Type" marker *must* be applied first. 374 // TODO(directxman12): find a less hacky way to do this 375 // (we could preserve ordering of markers, but that feels bad in its own right). 376 377 func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 378 schema.Type = string(m) 379 return nil 380 } 381 382 func (m Type) ApplyFirst() {} 383 384 func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 385 schema.Nullable = true 386 return nil 387 } 388 389 // Defaults are only valid CRDs created with the v1 API 390 func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 391 marshalledDefault, err := json.Marshal(m.Value) 392 if err != nil { 393 return err 394 } 395 schema.Default = &apiext.JSON{Raw: marshalledDefault} 396 return nil 397 } 398 399 func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 400 defTrue := true 401 schema.XPreserveUnknownFields = &defTrue 402 return nil 403 } 404 405 func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 406 schema.XEmbeddedResource = true 407 return nil 408 }