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