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