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