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