| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- /*
- 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 (
- "fmt"
- "encoding/json"
- 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,
- // integer 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{},
- )
- // 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.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 int
- // +controllertools:marker:generateHelp:category="CRD validation"
- // Minimum specifies the minimum numeric value that this field can have. Negative integers are supported.
- type Minimum int
- // +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 int
- // +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 minimun 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 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"
- // 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 (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "integer" {
- return fmt.Errorf("must apply maximum to an integer")
- }
- val := float64(m)
- schema.Maximum = &val
- return nil
- }
- func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "integer" {
- return fmt.Errorf("must apply minimum to an integer")
- }
- val := float64(m)
- schema.Minimum = &val
- return nil
- }
- func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "integer" {
- return fmt.Errorf("must apply exclusivemaximum to an integer")
- }
- schema.ExclusiveMaximum = bool(m)
- return nil
- }
- func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "integer" {
- return fmt.Errorf("must apply exclusiveminimum to an integer")
- }
- schema.ExclusiveMinimum = bool(m)
- return nil
- }
- func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
- if schema.Type != "integer" {
- return fmt.Errorf("must apply multipleof to an integer")
- }
- val := float64(m)
- 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 {
- if schema.Type != "string" {
- return fmt.Errorf("must apply pattern to a string")
- }
- 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
- }
- schema.Default = &apiext.JSON{Raw: marshalledDefault}
- 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
- }
|