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