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