sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/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  	"encoding/json"
    21  	"fmt"
    22  	"math"
    23  	"strings"
    24  
    25  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    26  
    27  	"sigs.k8s.io/controller-tools/pkg/markers"
    28  )
    29  
    30  const (
    31  	validationPrefix = "kubebuilder:validation:"
    32  
    33  	SchemalessName        = "kubebuilder:validation:Schemaless"
    34  	ValidationItemsPrefix = validationPrefix + "items:"
    35  )
    36  
    37  // ValidationMarkers lists all available markers that affect CRD schema generation,
    38  // except for the few that don't make sense as type-level markers (see FieldOnlyMarkers).
    39  // All markers start with `+kubebuilder:validation:`, and continue with their type name.
    40  // A copy is produced of all markers that describes types as well, for making types
    41  // reusable and writing complex validations on slice items.
    42  // At last a copy of all markers with the prefix `+kubebuilder:validation:items:` is
    43  // produced for marking slice fields and types.
    44  var ValidationMarkers = mustMakeAllWithPrefix(validationPrefix, markers.DescribesField,
    45  
    46  	// numeric markers
    47  
    48  	Maximum(0),
    49  	Minimum(0),
    50  	ExclusiveMaximum(false),
    51  	ExclusiveMinimum(false),
    52  	MultipleOf(0),
    53  	MinProperties(0),
    54  	MaxProperties(0),
    55  
    56  	// string markers
    57  
    58  	MaxLength(0),
    59  	MinLength(0),
    60  	Pattern(""),
    61  
    62  	// slice markers
    63  
    64  	MaxItems(0),
    65  	MinItems(0),
    66  	UniqueItems(false),
    67  
    68  	// general markers
    69  
    70  	Enum(nil),
    71  	Format(""),
    72  	Type(""),
    73  	XPreserveUnknownFields{},
    74  	XEmbeddedResource{},
    75  	XIntOrString{},
    76  	XValidation{},
    77  )
    78  
    79  // FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make
    80  // sense on a type, and thus aren't in ValidationMarkers).
    81  var FieldOnlyMarkers = []*definitionWithHelp{
    82  	must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})).
    83  		WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required.")),
    84  	must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})).
    85  		WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional.")),
    86  	must(markers.MakeDefinition("required", markers.DescribesField, struct{}{})).
    87  		WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required.")),
    88  	must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})).
    89  		WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional.")),
    90  
    91  	must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})).
    92  		WithHelp(Nullable{}.Help()),
    93  
    94  	must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})).
    95  		WithHelp(Default{}.Help()),
    96  	must(markers.MakeDefinition("default", markers.DescribesField, KubernetesDefault{})).
    97  		WithHelp(KubernetesDefault{}.Help()),
    98  
    99  	must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})).
   100  		WithHelp(Example{}.Help()),
   101  
   102  	must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})).
   103  		WithHelp(XEmbeddedResource{}.Help()),
   104  
   105  	must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})).
   106  		WithHelp(Schemaless{}.Help()),
   107  }
   108  
   109  // ValidationIshMarkers are field-and-type markers that don't fall under the
   110  // :validation: prefix, and/or don't have a name that directly matches their
   111  // type.
   112  var ValidationIshMarkers = []*definitionWithHelp{
   113  	must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})).
   114  		WithHelp(XPreserveUnknownFields{}.Help()),
   115  	must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})).
   116  		WithHelp(XPreserveUnknownFields{}.Help()),
   117  }
   118  
   119  func init() {
   120  	AllDefinitions = append(AllDefinitions, ValidationMarkers...)
   121  
   122  	for _, def := range ValidationMarkers {
   123  		typDef := def.clone()
   124  		typDef.Target = markers.DescribesType
   125  		AllDefinitions = append(AllDefinitions, typDef)
   126  
   127  		itemsName := ValidationItemsPrefix + strings.TrimPrefix(def.Name, validationPrefix)
   128  
   129  		itemsFieldDef := def.clone()
   130  		itemsFieldDef.Name = itemsName
   131  		itemsFieldDef.Help.Summary = "for array items " + itemsFieldDef.Help.Summary
   132  		AllDefinitions = append(AllDefinitions, itemsFieldDef)
   133  
   134  		itemsTypDef := def.clone()
   135  		itemsTypDef.Name = itemsName
   136  		itemsTypDef.Help.Summary = "for array items " + itemsTypDef.Help.Summary
   137  		itemsTypDef.Target = markers.DescribesType
   138  		AllDefinitions = append(AllDefinitions, itemsTypDef)
   139  	}
   140  
   141  	AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...)
   142  	AllDefinitions = append(AllDefinitions, ValidationIshMarkers...)
   143  }
   144  
   145  // +controllertools:marker:generateHelp:category="CRD validation"
   146  // Maximum specifies the maximum numeric value that this field can have.
   147  type Maximum float64
   148  
   149  func (m Maximum) Value() float64 {
   150  	return float64(m)
   151  }
   152  
   153  // +controllertools:marker:generateHelp:category="CRD validation"
   154  // Minimum specifies the minimum numeric value that this field can have. Negative numbers are supported.
   155  type Minimum float64
   156  
   157  func (m Minimum) Value() float64 {
   158  	return float64(m)
   159  }
   160  
   161  // +controllertools:marker:generateHelp:category="CRD validation"
   162  // ExclusiveMinimum indicates that the minimum is "up to" but not including that value.
   163  type ExclusiveMinimum bool
   164  
   165  // +controllertools:marker:generateHelp:category="CRD validation"
   166  // ExclusiveMaximum indicates that the maximum is "up to" but not including that value.
   167  type ExclusiveMaximum bool
   168  
   169  // +controllertools:marker:generateHelp:category="CRD validation"
   170  // MultipleOf specifies that this field must have a numeric value that's a multiple of this one.
   171  type MultipleOf float64
   172  
   173  func (m MultipleOf) Value() float64 {
   174  	return float64(m)
   175  }
   176  
   177  // +controllertools:marker:generateHelp:category="CRD validation"
   178  // MaxLength specifies the maximum length for this string.
   179  type MaxLength int
   180  
   181  // +controllertools:marker:generateHelp:category="CRD validation"
   182  // MinLength specifies the minimum length for this string.
   183  type MinLength int
   184  
   185  // +controllertools:marker:generateHelp:category="CRD validation"
   186  // Pattern specifies that this string must match the given regular expression.
   187  type Pattern string
   188  
   189  // +controllertools:marker:generateHelp:category="CRD validation"
   190  // MaxItems specifies the maximum length for this list.
   191  type MaxItems int
   192  
   193  // +controllertools:marker:generateHelp:category="CRD validation"
   194  // MinItems specifies the minimum length for this list.
   195  type MinItems int
   196  
   197  // +controllertools:marker:generateHelp:category="CRD validation"
   198  // UniqueItems specifies that all items in this list must be unique.
   199  type UniqueItems bool
   200  
   201  // +controllertools:marker:generateHelp:category="CRD validation"
   202  // MaxProperties restricts the number of keys in an object
   203  type MaxProperties int
   204  
   205  // +controllertools:marker:generateHelp:category="CRD validation"
   206  // MinProperties restricts the number of keys in an object
   207  type MinProperties int
   208  
   209  // +controllertools:marker:generateHelp:category="CRD validation"
   210  // Enum specifies that this (scalar) field is restricted to the *exact* values specified here.
   211  type Enum []interface{}
   212  
   213  // +controllertools:marker:generateHelp:category="CRD validation"
   214  // Format specifies additional "complex" formatting for this field.
   215  //
   216  // For example, a date-time field would be marked as "type: string" and
   217  // "format: date-time".
   218  type Format string
   219  
   220  // +controllertools:marker:generateHelp:category="CRD validation"
   221  // Type overrides the type for this field (which defaults to the equivalent of the Go type).
   222  //
   223  // This generally must be paired with custom serialization.  For example, the
   224  // metav1.Time field would be marked as "type: string" and "format: date-time".
   225  type Type string
   226  
   227  // +controllertools:marker:generateHelp:category="CRD validation"
   228  // Nullable marks this field as allowing the "null" value.
   229  //
   230  // This is often not necessary, but may be helpful with custom serialization.
   231  type Nullable struct{}
   232  
   233  // +controllertools:marker:generateHelp:category="CRD validation"
   234  // Default sets the default value for this field.
   235  //
   236  // A default value will be accepted as any value valid for the
   237  // field. Formatting for common types include: boolean: `true`, string:
   238  // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy:
   239  // "delete"}`). Defaults should be defined in pruned form, and only best-effort
   240  // validation will be performed. Full validation of a default requires
   241  // submission of the containing CRD to an apiserver.
   242  type Default struct {
   243  	Value interface{}
   244  }
   245  
   246  // +controllertools:marker:generateHelp:category="CRD validation"
   247  // Default sets the default value for this field.
   248  //
   249  // A default value will be accepted as any value valid for the field.
   250  // Only JSON-formatted values are accepted. `ref(...)` values are ignored.
   251  // Formatting for common types include: boolean: `true`, string:
   252  // `"Cluster"`, numerical: `1.24`, array: `[1,2]`, object: `{"policy":
   253  // "delete"}`). Defaults should be defined in pruned form, and only best-effort
   254  // validation will be performed. Full validation of a default requires
   255  // submission of the containing CRD to an apiserver.
   256  type KubernetesDefault struct {
   257  	Value interface{}
   258  }
   259  
   260  // +controllertools:marker:generateHelp:category="CRD validation"
   261  // Example sets the example value for this field.
   262  //
   263  // An example value will be accepted as any value valid for the
   264  // field. Formatting for common types include: boolean: `true`, string:
   265  // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy:
   266  // "delete"}`). Examples should be defined in pruned form, and only best-effort
   267  // validation will be performed. Full validation of an example requires
   268  // submission of the containing CRD to an apiserver.
   269  type Example struct {
   270  	Value interface{}
   271  }
   272  
   273  // +controllertools:marker:generateHelp:category="CRD processing"
   274  // PreserveUnknownFields stops the apiserver from pruning fields which are not specified.
   275  //
   276  // By default the apiserver drops unknown fields from the request payload
   277  // during the decoding step. This marker stops the API server from doing so.
   278  // It affects fields recursively, but switches back to normal pruning behaviour
   279  // if nested  properties or additionalProperties are specified in the schema.
   280  // This can either be true or undefined. False
   281  // is forbidden.
   282  //
   283  // NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated
   284  // in favor of the kubebuilder:pruning:PreserveUnknownFields variant.  They function
   285  // identically.
   286  type XPreserveUnknownFields struct{}
   287  
   288  // +controllertools:marker:generateHelp:category="CRD validation"
   289  // EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields.
   290  //
   291  // An embedded resource is a value that has apiVersion, kind and metadata fields.
   292  // They are validated implicitly according to the semantics of the currently
   293  // running apiserver. It is not necessary to add any additional schema for these
   294  // field, yet it is possible. This can be combined with PreserveUnknownFields.
   295  type XEmbeddedResource struct{}
   296  
   297  // +controllertools:marker:generateHelp:category="CRD validation"
   298  // IntOrString marks a fields as an IntOrString.
   299  //
   300  // This is required when applying patterns or other validations to an IntOrString
   301  // field. Knwon information about the type is applied during the collapse phase
   302  // and as such is not normally available during marker application.
   303  type XIntOrString struct{}
   304  
   305  // +controllertools:marker:generateHelp:category="CRD validation"
   306  // Schemaless marks a field as being a schemaless object.
   307  //
   308  // Schemaless objects are not introspected, so you must provide
   309  // any type and validation information yourself. One use for this
   310  // tag is for embedding fields that hold JSONSchema typed objects.
   311  // Because this field disables all type checking, it is recommended
   312  // to be used only as a last resort.
   313  type Schemaless struct{}
   314  
   315  func hasNumericType(schema *apiext.JSONSchemaProps) bool {
   316  	return schema.Type == "integer" || schema.Type == "number"
   317  }
   318  
   319  func isIntegral(value float64) bool {
   320  	return value == math.Trunc(value) && !math.IsNaN(value) && !math.IsInf(value, 0)
   321  }
   322  
   323  // +controllertools:marker:generateHelp:category="CRD validation"
   324  // XValidation marks a field as requiring a value for which a given
   325  // expression evaluates to true.
   326  //
   327  // This marker may be repeated to specify multiple expressions, all of
   328  // which must evaluate to true.
   329  type XValidation struct {
   330  	Rule              string
   331  	Message           string `marker:",optional"`
   332  	MessageExpression string `marker:"messageExpression,optional"`
   333  	Reason            string `marker:"reason,optional"`
   334  	FieldPath         string `marker:"fieldPath,optional"`
   335  }
   336  
   337  func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   338  	if !hasNumericType(schema) {
   339  		return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type)
   340  	}
   341  
   342  	if schema.Type == "integer" && !isIntegral(m.Value()) {
   343  		return fmt.Errorf("cannot apply non-integral maximum validation (%v) to integer value", m.Value())
   344  	}
   345  
   346  	val := m.Value()
   347  	schema.Maximum = &val
   348  	return nil
   349  }
   350  
   351  func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   352  	if !hasNumericType(schema) {
   353  		return fmt.Errorf("must apply minimum to a numeric value, found %s", schema.Type)
   354  	}
   355  
   356  	if schema.Type == "integer" && !isIntegral(m.Value()) {
   357  		return fmt.Errorf("cannot apply non-integral minimum validation (%v) to integer value", m.Value())
   358  	}
   359  
   360  	val := m.Value()
   361  	schema.Minimum = &val
   362  	return nil
   363  }
   364  
   365  func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   366  	if !hasNumericType(schema) {
   367  		return fmt.Errorf("must apply exclusivemaximum to a numeric value, found %s", schema.Type)
   368  	}
   369  	schema.ExclusiveMaximum = bool(m)
   370  	return nil
   371  }
   372  
   373  func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   374  	if !hasNumericType(schema) {
   375  		return fmt.Errorf("must apply exclusiveminimum to a numeric value, found %s", schema.Type)
   376  	}
   377  
   378  	schema.ExclusiveMinimum = bool(m)
   379  	return nil
   380  }
   381  
   382  func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   383  	if !hasNumericType(schema) {
   384  		return fmt.Errorf("must apply multipleof to a numeric value, found %s", schema.Type)
   385  	}
   386  
   387  	if schema.Type == "integer" && !isIntegral(m.Value()) {
   388  		return fmt.Errorf("cannot apply non-integral multipleof validation (%v) to integer value", m.Value())
   389  	}
   390  
   391  	val := m.Value()
   392  	schema.MultipleOf = &val
   393  	return nil
   394  }
   395  
   396  func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   397  	if schema.Type != "string" {
   398  		return fmt.Errorf("must apply maxlength to a string")
   399  	}
   400  	val := int64(m)
   401  	schema.MaxLength = &val
   402  	return nil
   403  }
   404  
   405  func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   406  	if schema.Type != "string" {
   407  		return fmt.Errorf("must apply minlength to a string")
   408  	}
   409  	val := int64(m)
   410  	schema.MinLength = &val
   411  	return nil
   412  }
   413  
   414  func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   415  	// Allow string types or IntOrStrings. An IntOrString will still
   416  	// apply the pattern validation when a string is detected, the pattern
   417  	// will not apply to ints though.
   418  	if schema.Type != "string" && !schema.XIntOrString {
   419  		return fmt.Errorf("must apply pattern to a `string` or `IntOrString`")
   420  	}
   421  	schema.Pattern = string(m)
   422  	return nil
   423  }
   424  
   425  func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   426  	if schema.Type != "array" {
   427  		return fmt.Errorf("must apply maxitem to an array")
   428  	}
   429  	val := int64(m)
   430  	schema.MaxItems = &val
   431  	return nil
   432  }
   433  
   434  func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   435  	if schema.Type != "array" {
   436  		return fmt.Errorf("must apply minitems to an array")
   437  	}
   438  	val := int64(m)
   439  	schema.MinItems = &val
   440  	return nil
   441  }
   442  
   443  func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   444  	if schema.Type != "array" {
   445  		return fmt.Errorf("must apply uniqueitems to an array")
   446  	}
   447  	schema.UniqueItems = bool(m)
   448  	return nil
   449  }
   450  
   451  func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   452  	if schema.Type != "object" {
   453  		return fmt.Errorf("must apply minproperties to an object")
   454  	}
   455  	val := int64(m)
   456  	schema.MinProperties = &val
   457  	return nil
   458  }
   459  
   460  func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   461  	if schema.Type != "object" {
   462  		return fmt.Errorf("must apply maxproperties to an object")
   463  	}
   464  	val := int64(m)
   465  	schema.MaxProperties = &val
   466  	return nil
   467  }
   468  
   469  func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   470  	// TODO(directxman12): this is a bit hacky -- we should
   471  	// probably support AnyType better + using the schema structure
   472  	vals := make([]apiext.JSON, len(m))
   473  	for i, val := range m {
   474  		// TODO(directxman12): check actual type with schema type?
   475  		// if we're expecting a string, marshal the string properly...
   476  		// NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly
   477  		valMarshalled, err := json.Marshal(val)
   478  		if err != nil {
   479  			return err
   480  		}
   481  		vals[i] = apiext.JSON{Raw: valMarshalled}
   482  	}
   483  	schema.Enum = vals
   484  	return nil
   485  }
   486  
   487  func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   488  	schema.Format = string(m)
   489  	return nil
   490  }
   491  
   492  // NB(directxman12): we "typecheck" on target schema properties here,
   493  // which means the "Type" marker *must* be applied first.
   494  // TODO(directxman12): find a less hacky way to do this
   495  // (we could preserve ordering of markers, but that feels bad in its own right).
   496  
   497  func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   498  	schema.Type = string(m)
   499  	return nil
   500  }
   501  
   502  func (m Type) ApplyPriority() ApplyPriority {
   503  	return ApplyPriorityDefault - 1
   504  }
   505  
   506  func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   507  	schema.Nullable = true
   508  	return nil
   509  }
   510  
   511  // Defaults are only valid CRDs created with the v1 API
   512  func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   513  	marshalledDefault, err := json.Marshal(m.Value)
   514  	if err != nil {
   515  		return err
   516  	}
   517  	if schema.Type == "array" && string(marshalledDefault) == "{}" {
   518  		marshalledDefault = []byte("[]")
   519  	}
   520  	schema.Default = &apiext.JSON{Raw: marshalledDefault}
   521  	return nil
   522  }
   523  
   524  func (m Default) ApplyPriority() ApplyPriority {
   525  	// explicitly go after +default markers, so kubebuilder-specific defaults get applied last and stomp
   526  	return 10
   527  }
   528  
   529  func (m *KubernetesDefault) ParseMarker(_ string, _ string, restFields string) error {
   530  	if strings.HasPrefix(strings.TrimSpace(restFields), "ref(") {
   531  		// Skip +default=ref(...) values for now, since we don't have a good way to evaluate go constant values via AST.
   532  		// See https://github.com/kubernetes-sigs/controller-tools/pull/938#issuecomment-2096790018
   533  		return nil
   534  	}
   535  	return json.Unmarshal([]byte(restFields), &m.Value)
   536  }
   537  
   538  // Defaults are only valid CRDs created with the v1 API
   539  func (m KubernetesDefault) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   540  	if m.Value == nil {
   541  		// only apply to the schema if we have a non-nil default value
   542  		return nil
   543  	}
   544  	marshalledDefault, err := json.Marshal(m.Value)
   545  	if err != nil {
   546  		return err
   547  	}
   548  	schema.Default = &apiext.JSON{Raw: marshalledDefault}
   549  	return nil
   550  }
   551  
   552  func (m KubernetesDefault) ApplyPriority() ApplyPriority {
   553  	// explicitly go before +kubebuilder:default markers, so kubebuilder-specific defaults get applied last and stomp
   554  	return 9
   555  }
   556  
   557  func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   558  	marshalledExample, err := json.Marshal(m.Value)
   559  	if err != nil {
   560  		return err
   561  	}
   562  	schema.Example = &apiext.JSON{Raw: marshalledExample}
   563  	return nil
   564  }
   565  
   566  func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   567  	defTrue := true
   568  	schema.XPreserveUnknownFields = &defTrue
   569  	return nil
   570  }
   571  
   572  func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   573  	schema.XEmbeddedResource = true
   574  	return nil
   575  }
   576  
   577  // NB(JoelSpeed): we use this property in other markers here,
   578  // which means the "XIntOrString" marker *must* be applied first.
   579  
   580  func (m XIntOrString) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   581  	schema.XIntOrString = true
   582  	return nil
   583  }
   584  
   585  func (m XIntOrString) ApplyPriority() ApplyPriority {
   586  	return ApplyPriorityDefault - 1
   587  }
   588  
   589  func (m XValidation) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   590  	var reason *apiext.FieldValueErrorReason
   591  	if m.Reason != "" {
   592  		switch m.Reason {
   593  		case string(apiext.FieldValueRequired), string(apiext.FieldValueInvalid), string(apiext.FieldValueForbidden), string(apiext.FieldValueDuplicate):
   594  			reason = (*apiext.FieldValueErrorReason)(&m.Reason)
   595  		default:
   596  			return fmt.Errorf("invalid reason %s, valid values are %s, %s, %s and %s", m.Reason, apiext.FieldValueRequired, apiext.FieldValueInvalid, apiext.FieldValueForbidden, apiext.FieldValueDuplicate)
   597  		}
   598  	}
   599  
   600  	schema.XValidations = append(schema.XValidations, apiext.ValidationRule{
   601  		Rule:              m.Rule,
   602  		Message:           m.Message,
   603  		MessageExpression: m.MessageExpression,
   604  		Reason:            reason,
   605  		FieldPath:         m.FieldPath,
   606  	})
   607  	return nil
   608  }