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