| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- /*
- Copyright 2019 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package markers
- import (
- "encoding/json"
- "fmt"
- "math"
- apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
- "sigs.k8s.io/controller-tools/pkg/markers"
- )
- const (
- SchemalessName = "kubebuilder:validation:Schemaless"
- )
- // ValidationMarkers lists all available markers that affect CRD schema generation,
- // except for the few that don't make sense as type-level markers (see FieldOnlyMarkers).
- // All markers start with `+kubebuilder:validation:`, and continue with their type name.
- // A copy is produced of all markers that describes types as well, for making types
- // reusable and writing complex validations on slice items.
- var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", markers.DescribesField,
- // numeric markers
- Maximum(0),
- Minimum(0),
- ExclusiveMaximum(false),
- ExclusiveMinimum(false),
- MultipleOf(0),
- MinProperties(0),
- MaxProperties(0),
- // string markers
- MaxLength(0),
- MinLength(0),
- Pattern(""),
- // slice markers
- MaxItems(0),
- MinItems(0),
- UniqueItems(false),
- // general markers
- Enum(nil),
- Format(""),
- Type(""),
- XPreserveUnknownFields{},
- XEmbeddedResource{},
- XIntOrString{},
- XValidation{},
- )
- // FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make
- // sense on a type, and thus aren't in ValidationMarkers).
- var FieldOnlyMarkers = []*definitionWithHelp{
- must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})).
- WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required, if fields are optional by default.")),
- must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})).
- WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")),
- must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})).
- WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")),
- must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})).
- WithHelp(Nullable{}.Help()),
- must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})).
- WithHelp(Default{}.Help()),
- must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})).
- WithHelp(Example{}.Help()),
- must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})).
- WithHelp(XEmbeddedResource{}.Help()),
- must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})).
- WithHelp(Schemaless{}.Help()),
- }
- // ValidationIshMarkers are field-and-type markers that don't fall under the
- // :validation: prefix, and/or don't have a name that directly matches their
- // type.
- var ValidationIshMarkers = []*definitionWithHelp{
- must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})).
- WithHelp(XPreserveUnknownFields{}.Help()),
- must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})).
- WithHelp(XPreserveUnknownFields{}.Help()),
- }
- func init() {
- AllDefinitions = append(AllDefinitions, ValidationMarkers...)
- for _, def := range ValidationMarkers {
- newDef := *def.Definition
- // copy both parts so we don't change the definition
- typDef := definitionWithHelp{
- Definition: &newDef,
- Help: def.Help,
- }
- typDef.Target = markers.DescribesType
- AllDefinitions = append(AllDefinitions, &typDef)
- }
- AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...)
- AllDefinitions = append(AllDefinitions, ValidationIshMarkers...)
- }
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Maximum specifies the maximum numeric value that this field can have.
- type Maximum float64
- func (m Maximum) Value() float64 {
- return float64(m)
- }
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Minimum specifies the minimum numeric value that this field can have. Negative numbers are supported.
- type Minimum float64
- func (m Minimum) Value() float64 {
- return float64(m)
- }
- // +controllertools:marker:generateHelp:category="CRD validation"
- // ExclusiveMinimum indicates that the minimum is "up to" but not including that value.
- type ExclusiveMinimum bool
- // +controllertools:marker:generateHelp:category="CRD validation"
- // ExclusiveMaximum indicates that the maximum is "up to" but not including that value.
- type ExclusiveMaximum bool
- // +controllertools:marker:generateHelp:category="CRD validation"
- // MultipleOf specifies that this field must have a numeric value that's a multiple of this one.
- type MultipleOf float64
- func (m MultipleOf) Value() float64 {
- return float64(m)
- }
- // +controllertools:marker:generateHelp:category="CRD validation"
- // MaxLength specifies the maximum length for this string.
- type MaxLength int
- // +controllertools:marker:generateHelp:category="CRD validation"
- // MinLength specifies the minimum length for this string.
- type MinLength int
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Pattern specifies that this string must match the given regular expression.
- type Pattern string
- // +controllertools:marker:generateHelp:category="CRD validation"
- // MaxItems specifies the maximum length for this list.
- type MaxItems int
- // +controllertools:marker:generateHelp:category="CRD validation"
- // MinItems specifies the minimum length for this list.
- type MinItems int
- // +controllertools:marker:generateHelp:category="CRD validation"
- // UniqueItems specifies that all items in this list must be unique.
- type UniqueItems bool
- // +controllertools:marker:generateHelp:category="CRD validation"
- // MaxProperties restricts the number of keys in an object
- type MaxProperties int
- // +controllertools:marker:generateHelp:category="CRD validation"
- // MinProperties restricts the number of keys in an object
- type MinProperties int
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Enum specifies that this (scalar) field is restricted to the *exact* values specified here.
- type Enum []interface{}
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Format specifies additional "complex" formatting for this field.
- //
- // For example, a date-time field would be marked as "type: string" and
- // "format: date-time".
- type Format string
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Type overrides the type for this field (which defaults to the equivalent of the Go type).
- //
- // This generally must be paired with custom serialization. For example, the
- // metav1.Time field would be marked as "type: string" and "format: date-time".
- type Type string
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Nullable marks this field as allowing the "null" value.
- //
- // This is often not necessary, but may be helpful with custom serialization.
- type Nullable struct{}
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Default sets the default value for this field.
- //
- // A default value will be accepted as any value valid for the
- // field. Formatting for common types include: boolean: `true`, string:
- // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy:
- // "delete"}`). Defaults should be defined in pruned form, and only best-effort
- // validation will be performed. Full validation of a default requires
- // submission of the containing CRD to an apiserver.
- type Default struct {
- Value interface{}
- }
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Example sets the example value for this field.
- //
- // An example value will be accepted as any value valid for the
- // field. Formatting for common types include: boolean: `true`, string:
- // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy:
- // "delete"}`). Examples should be defined in pruned form, and only best-effort
- // validation will be performed. Full validation of an example requires
- // submission of the containing CRD to an apiserver.
- type Example struct {
- Value interface{}
- }
- // +controllertools:marker:generateHelp:category="CRD processing"
- // PreserveUnknownFields stops the apiserver from pruning fields which are not specified.
- //
- // By default the apiserver drops unknown fields from the request payload
- // during the decoding step. This marker stops the API server from doing so.
- // It affects fields recursively, but switches back to normal pruning behaviour
- // if nested properties or additionalProperties are specified in the schema.
- // This can either be true or undefined. False
- // is forbidden.
- //
- // NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated
- // in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function
- // identically.
- type XPreserveUnknownFields struct{}
- // +controllertools:marker:generateHelp:category="CRD validation"
- // EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields.
- //
- // An embedded resource is a value that has apiVersion, kind and metadata fields.
- // They are validated implicitly according to the semantics of the currently
- // running apiserver. It is not necessary to add any additional schema for these
- // field, yet it is possible. This can be combined with PreserveUnknownFields.
- type XEmbeddedResource struct{}
- // +controllertools:marker:generateHelp:category="CRD validation"
- // IntOrString marks a fields as an IntOrString.
- //
- // This is required when applying patterns or other validations to an IntOrString
- // field. Knwon information about the type is applied during the collapse phase
- // and as such is not normally available during marker application.
- type XIntOrString struct{}
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Schemaless marks a field as being a schemaless object.
- //
- // Schemaless objects are not introspected, so you must provide
- // any type and validation information yourself. One use for this
- // tag is for embedding fields that hold JSONSchema typed objects.
- // Because this field disables all type checking, it is recommended
- // to be used only as a last resort.
- type Schemaless struct{}
- func hasNumericType(schema *apiext.JSONSchemaProps) bool {
- return schema.Type == "integer" || schema.Type == "number"
- }
- func isIntegral(value float64) bool {
- return value == math.Trunc(value) && !math.IsNaN(value) && !math.IsInf(value, 0)
- }
- // +controllertools:marker:generateHelp:category="CRD validation"
- // XValidation marks a field as requiring a value for which a given
- // expression evaluates to true.
- //
- // This marker may be repeated to specify multiple expressions, all of
- // which must evaluate to true.
- type XValidation struct {
- Rule string
- Message string `marker:",optional"`
- }
- func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if !hasNumericType(schema) {
- return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type)
- }
- if schema.Type == "integer" && !isIntegral(m.Value()) {
- return fmt.Errorf("cannot apply non-integral maximum validation (%v) to integer value", m.Value())
- }
- val := m.Value()
- schema.Maximum = &val
- return nil
- }
- func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if !hasNumericType(schema) {
- return fmt.Errorf("must apply minimum to a numeric value, found %s", schema.Type)
- }
- if schema.Type == "integer" && !isIntegral(m.Value()) {
- return fmt.Errorf("cannot apply non-integral minimum validation (%v) to integer value", m.Value())
- }
- val := m.Value()
- schema.Minimum = &val
- return nil
- }
- func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if !hasNumericType(schema) {
- return fmt.Errorf("must apply exclusivemaximum to a numeric value, found %s", schema.Type)
- }
- schema.ExclusiveMaximum = bool(m)
- return nil
- }
- func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if !hasNumericType(schema) {
- return fmt.Errorf("must apply exclusiveminimum to a numeric value, found %s", schema.Type)
- }
- schema.ExclusiveMinimum = bool(m)
- return nil
- }
- func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if !hasNumericType(schema) {
- return fmt.Errorf("must apply multipleof to a numeric value, found %s", schema.Type)
- }
- if schema.Type == "integer" && !isIntegral(m.Value()) {
- return fmt.Errorf("cannot apply non-integral multipleof validation (%v) to integer value", m.Value())
- }
- val := m.Value()
- schema.MultipleOf = &val
- return nil
- }
- func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "string" {
- return fmt.Errorf("must apply maxlength to a string")
- }
- val := int64(m)
- schema.MaxLength = &val
- return nil
- }
- func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "string" {
- return fmt.Errorf("must apply minlength to a string")
- }
- val := int64(m)
- schema.MinLength = &val
- return nil
- }
- func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- // Allow string types or IntOrStrings. An IntOrString will still
- // apply the pattern validation when a string is detected, the pattern
- // will not apply to ints though.
- if schema.Type != "string" && !schema.XIntOrString {
- return fmt.Errorf("must apply pattern to a `string` or `IntOrString`")
- }
- schema.Pattern = string(m)
- return nil
- }
- func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "array" {
- return fmt.Errorf("must apply maxitem to an array")
- }
- val := int64(m)
- schema.MaxItems = &val
- return nil
- }
- func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "array" {
- return fmt.Errorf("must apply minitems to an array")
- }
- val := int64(m)
- schema.MinItems = &val
- return nil
- }
- func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "array" {
- return fmt.Errorf("must apply uniqueitems to an array")
- }
- schema.UniqueItems = bool(m)
- return nil
- }
- func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "object" {
- return fmt.Errorf("must apply minproperties to an object")
- }
- val := int64(m)
- schema.MinProperties = &val
- return nil
- }
- func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "object" {
- return fmt.Errorf("must apply maxproperties to an object")
- }
- val := int64(m)
- schema.MaxProperties = &val
- return nil
- }
- func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- // TODO(directxman12): this is a bit hacky -- we should
- // probably support AnyType better + using the schema structure
- vals := make([]apiext.JSON, len(m))
- for i, val := range m {
- // TODO(directxman12): check actual type with schema type?
- // if we're expecting a string, marshal the string properly...
- // NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly
- valMarshalled, err := json.Marshal(val)
- if err != nil {
- return err
- }
- vals[i] = apiext.JSON{Raw: valMarshalled}
- }
- schema.Enum = vals
- return nil
- }
- func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- schema.Format = string(m)
- return nil
- }
- // NB(directxman12): we "typecheck" on target schema properties here,
- // which means the "Type" marker *must* be applied first.
- // TODO(directxman12): find a less hacky way to do this
- // (we could preserve ordering of markers, but that feels bad in its own right).
- func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- schema.Type = string(m)
- return nil
- }
- func (m Type) ApplyFirst() {}
- func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- schema.Nullable = true
- return nil
- }
- // Defaults are only valid CRDs created with the v1 API
- func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- marshalledDefault, err := json.Marshal(m.Value)
- if err != nil {
- return err
- }
- if schema.Type == "array" && string(marshalledDefault) == "{}" {
- marshalledDefault = []byte("[]")
- }
- schema.Default = &apiext.JSON{Raw: marshalledDefault}
- return nil
- }
- func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- marshalledExample, err := json.Marshal(m.Value)
- if err != nil {
- return err
- }
- schema.Example = &apiext.JSON{Raw: marshalledExample}
- return nil
- }
- func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- defTrue := true
- schema.XPreserveUnknownFields = &defTrue
- return nil
- }
- func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- schema.XEmbeddedResource = true
- return nil
- }
- // NB(JoelSpeed): we use this property in other markers here,
- // which means the "XIntOrString" marker *must* be applied first.
- func (m XIntOrString) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- schema.XIntOrString = true
- return nil
- }
- func (m XIntOrString) ApplyFirst() {}
- func (m XValidation) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- schema.XValidations = append(schema.XValidations, apiext.ValidationRule{
- Rule: m.Rule,
- Message: m.Message,
- })
- return nil
- }
|