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