validation.go 15 KB

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