github.com/alex123012/deckhouse-controller-tools@v0.0.0-20230510090815-d594daf1af8c/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  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"go/ast"
    24  	"go/parser"
    25  	"go/token"
    26  	"math"
    27  	"strings"
    28  
    29  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    30  	"sigs.k8s.io/yaml"
    31  
    32  	"sigs.k8s.io/controller-tools/pkg/markers"
    33  )
    34  
    35  const (
    36  	SchemalessName = "kubebuilder:validation:Schemaless"
    37  )
    38  
    39  // ValidationMarkers lists all available markers that affect CRD schema generation,
    40  // except for the few that don't make sense as type-level markers (see FieldOnlyMarkers).
    41  // All markers start with `+kubebuilder:validation:`, and continue with their type name.
    42  // A copy is produced of all markers that describes types as well, for making types
    43  // reusable and writing complex validations on slice items.
    44  var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", 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  	OneOf(""),
    74  	XPreserveUnknownFields{},
    75  	XEmbeddedResource{},
    76  	XIntOrString{},
    77  	XValidation{},
    78  )
    79  
    80  // FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make
    81  // sense on a type, and thus aren't in ValidationMarkers).
    82  var FieldOnlyMarkers = []*definitionWithHelp{
    83  	must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})).
    84  		WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required, if fields are optional by default.")),
    85  	must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})).
    86  		WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")),
    87  	must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})).
    88  		WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")),
    89  
    90  	must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})).
    91  		WithHelp(Nullable{}.Help()),
    92  
    93  	must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})).
    94  		WithHelp(Default{}.Help()),
    95  
    96  	must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})).
    97  		WithHelp(Example{}.Help()),
    98  
    99  	must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})).
   100  		WithHelp(XEmbeddedResource{}.Help()),
   101  
   102  	must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})).
   103  		WithHelp(Schemaless{}.Help()),
   104  
   105  	must(markers.MakeAnyTypeDefinition("deckhouse:xdoc:default", markers.DescribesField, DeckhouseDocDefault{})).
   106  		WithHelp(DeckhouseDocDefault{}.Help()),
   107  
   108  	must(markers.MakeAnyTypeDefinition("deckhouse:xdoc:example", markers.DescribesField, DeckhouseDocExample{})).
   109  		WithHelp(DeckhouseDocExample{}.Help()),
   110  }
   111  
   112  // ValidationIshMarkers are field-and-type markers that don't fall under the
   113  // :validation: prefix, and/or don't have a name that directly matches their
   114  // type.
   115  var ValidationIshMarkers = []*definitionWithHelp{
   116  	must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})).
   117  		WithHelp(XPreserveUnknownFields{}.Help()),
   118  	must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})).
   119  		WithHelp(XPreserveUnknownFields{}.Help()),
   120  }
   121  
   122  func init() {
   123  	AllDefinitions = append(AllDefinitions, ValidationMarkers...)
   124  
   125  	for _, def := range ValidationMarkers {
   126  		newDef := *def.Definition
   127  		// copy both parts so we don't change the definition
   128  		typDef := definitionWithHelp{
   129  			Definition: &newDef,
   130  			Help:       def.Help,
   131  		}
   132  		typDef.Target = markers.DescribesType
   133  		AllDefinitions = append(AllDefinitions, &typDef)
   134  	}
   135  
   136  	AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...)
   137  	AllDefinitions = append(AllDefinitions, ValidationIshMarkers...)
   138  }
   139  
   140  // +controllertools:marker:generateHelp:category="CRD validation"
   141  // Maximum specifies the maximum numeric value that this field can have.
   142  type Maximum float64
   143  
   144  func (m Maximum) Value() float64 {
   145  	return float64(m)
   146  }
   147  
   148  // +controllertools:marker:generateHelp:category="CRD validation"
   149  // Minimum specifies the minimum numeric value that this field can have. Negative numbers are supported.
   150  type Minimum float64
   151  
   152  func (m Minimum) Value() float64 {
   153  	return float64(m)
   154  }
   155  
   156  // +controllertools:marker:generateHelp:category="CRD validation"
   157  // ExclusiveMinimum indicates that the minimum is "up to" but not including that value.
   158  type ExclusiveMinimum bool
   159  
   160  // +controllertools:marker:generateHelp:category="CRD validation"
   161  // ExclusiveMaximum indicates that the maximum is "up to" but not including that value.
   162  type ExclusiveMaximum bool
   163  
   164  // +controllertools:marker:generateHelp:category="CRD validation"
   165  // MultipleOf specifies that this field must have a numeric value that's a multiple of this one.
   166  type MultipleOf float64
   167  
   168  func (m MultipleOf) Value() float64 {
   169  	return float64(m)
   170  }
   171  
   172  // +controllertools:marker:generateHelp:category="CRD validation"
   173  // MaxLength specifies the maximum length for this string.
   174  type MaxLength int
   175  
   176  // +controllertools:marker:generateHelp:category="CRD validation"
   177  // MinLength specifies the minimum length for this string.
   178  type MinLength int
   179  
   180  // +controllertools:marker:generateHelp:category="CRD validation"
   181  // Pattern specifies that this string must match the given regular expression.
   182  type Pattern string
   183  
   184  // +controllertools:marker:generateHelp:category="CRD validation"
   185  // MaxItems specifies the maximum length for this list.
   186  type MaxItems int
   187  
   188  // +controllertools:marker:generateHelp:category="CRD validation"
   189  // MinItems specifies the minimum length for this list.
   190  type MinItems int
   191  
   192  // +controllertools:marker:generateHelp:category="CRD validation"
   193  // UniqueItems specifies that all items in this list must be unique.
   194  type UniqueItems bool
   195  
   196  // +controllertools:marker:generateHelp:category="CRD validation"
   197  // MaxProperties restricts the number of keys in an object
   198  type MaxProperties int
   199  
   200  // +controllertools:marker:generateHelp:category="CRD validation"
   201  // MinProperties restricts the number of keys in an object
   202  type MinProperties int
   203  
   204  // +controllertools:marker:generateHelp:category="CRD validation"
   205  // Enum specifies that this (scalar) field is restricted to the *exact* values specified here.
   206  type Enum []interface{}
   207  
   208  // +controllertools:marker:generateHelp:category="CRD validation"
   209  // Format specifies additional "complex" formatting for this field.
   210  //
   211  // For example, a date-time field would be marked as "type: string" and
   212  // "format: date-time".
   213  type Format string
   214  
   215  // +controllertools:marker:generateHelp:category="CRD validation"
   216  // Type overrides the type for this field (which defaults to the equivalent of the Go type).
   217  //
   218  // This generally must be paired with custom serialization.  For example, the
   219  // metav1.Time field would be marked as "type: string" and "format: date-time".
   220  type Type string
   221  
   222  // +controllertools:marker:generateHelp:category="CRD validation"
   223  // Nullable marks this field as allowing the "null" value.
   224  //
   225  // This is often not necessary, but may be helpful with custom serialization.
   226  type Nullable struct{}
   227  
   228  // +controllertools:marker:generateHelp:category="CRD validation"
   229  // Default sets the default value for this field.
   230  //
   231  // A default value will be accepted as any value valid for the
   232  // field. Formatting for common types include: boolean: `true`, string:
   233  // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy:
   234  // "delete"}`). Defaults should be defined in pruned form, and only best-effort
   235  // validation will be performed. Full validation of a default requires
   236  // submission of the containing CRD to an apiserver.
   237  type Default struct {
   238  	Value interface{}
   239  }
   240  
   241  // +controllertools:marker:generateHelp:category="CRD validation"
   242  // Example sets the example value for this field.
   243  //
   244  // An example value will be accepted as any value valid for the
   245  // field. Formatting for common types include: boolean: `true`, string:
   246  // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy:
   247  // "delete"}`). Examples should be defined in pruned form, and only best-effort
   248  // validation will be performed. Full validation of an example requires
   249  // submission of the containing CRD to an apiserver.
   250  type Example struct {
   251  	Value interface{}
   252  }
   253  
   254  // +controllertools:marker:generateHelp:category="CRD processing"
   255  // PreserveUnknownFields stops the apiserver from pruning fields which are not specified.
   256  //
   257  // By default the apiserver drops unknown fields from the request payload
   258  // during the decoding step. This marker stops the API server from doing so.
   259  // It affects fields recursively, but switches back to normal pruning behaviour
   260  // if nested  properties or additionalProperties are specified in the schema.
   261  // This can either be true or undefined. False
   262  // is forbidden.
   263  //
   264  // NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated
   265  // in favor of the kubebuilder:pruning:PreserveUnknownFields variant.  They function
   266  // identically.
   267  type XPreserveUnknownFields struct{}
   268  
   269  // +controllertools:marker:generateHelp:category="CRD validation"
   270  // EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields.
   271  //
   272  // An embedded resource is a value that has apiVersion, kind and metadata fields.
   273  // They are validated implicitly according to the semantics of the currently
   274  // running apiserver. It is not necessary to add any additional schema for these
   275  // field, yet it is possible. This can be combined with PreserveUnknownFields.
   276  type XEmbeddedResource struct{}
   277  
   278  // +controllertools:marker:generateHelp:category="CRD validation"
   279  // IntOrString marks a fields as an IntOrString.
   280  //
   281  // This is required when applying patterns or other validations to an IntOrString
   282  // field. Knwon information about the type is applied during the collapse phase
   283  // and as such is not normally available during marker application.
   284  type XIntOrString struct{}
   285  
   286  // +controllertools:marker:generateHelp:category="CRD validation"
   287  // Schemaless marks a field as being a schemaless object.
   288  //
   289  // Schemaless objects are not introspected, so you must provide
   290  // any type and validation information yourself. One use for this
   291  // tag is for embedding fields that hold JSONSchema typed objects.
   292  // Because this field disables all type checking, it is recommended
   293  // to be used only as a last resort.
   294  type Schemaless struct{}
   295  
   296  func hasNumericType(schema *apiext.JSONSchemaProps) bool {
   297  	return schema.Type == "integer" || schema.Type == "number"
   298  }
   299  
   300  func isIntegral(value float64) bool {
   301  	return value == math.Trunc(value) && !math.IsNaN(value) && !math.IsInf(value, 0)
   302  }
   303  
   304  // +controllertools:marker:generateHelp:category="CRD validation"
   305  // XValidation marks a field as requiring a value for which a given
   306  // expression evaluates to true.
   307  //
   308  // This marker may be repeated to specify multiple expressions, all of
   309  // which must evaluate to true.
   310  type XValidation struct {
   311  	Rule    string
   312  	Message string `marker:",optional"`
   313  }
   314  
   315  func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   316  	if !hasNumericType(schema) {
   317  		return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type)
   318  	}
   319  
   320  	if schema.Type == "integer" && !isIntegral(m.Value()) {
   321  		return fmt.Errorf("cannot apply non-integral maximum validation (%v) to integer value", m.Value())
   322  	}
   323  
   324  	val := m.Value()
   325  	schema.Maximum = &val
   326  	return nil
   327  }
   328  
   329  func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   330  	if !hasNumericType(schema) {
   331  		return fmt.Errorf("must apply minimum to a numeric value, found %s", schema.Type)
   332  	}
   333  
   334  	if schema.Type == "integer" && !isIntegral(m.Value()) {
   335  		return fmt.Errorf("cannot apply non-integral minimum validation (%v) to integer value", m.Value())
   336  	}
   337  
   338  	val := m.Value()
   339  	schema.Minimum = &val
   340  	return nil
   341  }
   342  
   343  func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   344  	if !hasNumericType(schema) {
   345  		return fmt.Errorf("must apply exclusivemaximum to a numeric value, found %s", schema.Type)
   346  	}
   347  	schema.ExclusiveMaximum = bool(m)
   348  	return nil
   349  }
   350  
   351  func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   352  	if !hasNumericType(schema) {
   353  		return fmt.Errorf("must apply exclusiveminimum to a numeric value, found %s", schema.Type)
   354  	}
   355  
   356  	schema.ExclusiveMinimum = bool(m)
   357  	return nil
   358  }
   359  
   360  func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   361  	if !hasNumericType(schema) {
   362  		return fmt.Errorf("must apply multipleof to a numeric value, found %s", schema.Type)
   363  	}
   364  
   365  	if schema.Type == "integer" && !isIntegral(m.Value()) {
   366  		return fmt.Errorf("cannot apply non-integral multipleof validation (%v) to integer value", m.Value())
   367  	}
   368  
   369  	val := m.Value()
   370  	schema.MultipleOf = &val
   371  	return nil
   372  }
   373  
   374  func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   375  	if schema.Type != "string" {
   376  		return fmt.Errorf("must apply maxlength to a string")
   377  	}
   378  	val := int64(m)
   379  	schema.MaxLength = &val
   380  	return nil
   381  }
   382  
   383  func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   384  	if schema.Type != "string" {
   385  		return fmt.Errorf("must apply minlength to a string")
   386  	}
   387  	val := int64(m)
   388  	schema.MinLength = &val
   389  	return nil
   390  }
   391  
   392  func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   393  	// Allow string types or IntOrStrings. An IntOrString will still
   394  	// apply the pattern validation when a string is detected, the pattern
   395  	// will not apply to ints though.
   396  	if schema.Type != "string" && !schema.XIntOrString {
   397  		return fmt.Errorf("must apply pattern to a `string` or `IntOrString`")
   398  	}
   399  	schema.Pattern = string(m)
   400  	return nil
   401  }
   402  
   403  func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   404  	if schema.Type != "array" {
   405  		return fmt.Errorf("must apply maxitem to an array")
   406  	}
   407  	val := int64(m)
   408  	schema.MaxItems = &val
   409  	return nil
   410  }
   411  
   412  func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   413  	if schema.Type != "array" {
   414  		return fmt.Errorf("must apply minitems to an array")
   415  	}
   416  	val := int64(m)
   417  	schema.MinItems = &val
   418  	return nil
   419  }
   420  
   421  func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   422  	if schema.Type != "array" {
   423  		return fmt.Errorf("must apply uniqueitems to an array")
   424  	}
   425  	schema.UniqueItems = bool(m)
   426  	return nil
   427  }
   428  
   429  func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   430  	if schema.Type != "object" {
   431  		return fmt.Errorf("must apply minproperties to an object")
   432  	}
   433  	val := int64(m)
   434  	schema.MinProperties = &val
   435  	return nil
   436  }
   437  
   438  func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   439  	if schema.Type != "object" {
   440  		return fmt.Errorf("must apply maxproperties to an object")
   441  	}
   442  	val := int64(m)
   443  	schema.MaxProperties = &val
   444  	return nil
   445  }
   446  
   447  func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   448  	// TODO(directxman12): this is a bit hacky -- we should
   449  	// probably support AnyType better + using the schema structure
   450  	vals := make([]apiext.JSON, len(m))
   451  	for i, val := range m {
   452  		// TODO(directxman12): check actual type with schema type?
   453  		// if we're expecting a string, marshal the string properly...
   454  		// NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly
   455  		valMarshalled, err := json.Marshal(val)
   456  		if err != nil {
   457  			return err
   458  		}
   459  		vals[i] = apiext.JSON{Raw: valMarshalled}
   460  	}
   461  	schema.Enum = vals
   462  	return nil
   463  }
   464  
   465  func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   466  	schema.Format = string(m)
   467  	return nil
   468  }
   469  
   470  // NB(directxman12): we "typecheck" on target schema properties here,
   471  // which means the "Type" marker *must* be applied first.
   472  // TODO(directxman12): find a less hacky way to do this
   473  // (we could preserve ordering of markers, but that feels bad in its own right).
   474  
   475  func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   476  	schema.Type = string(m)
   477  	return nil
   478  }
   479  
   480  func (m Type) ApplyFirst() {}
   481  
   482  func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   483  	schema.Nullable = true
   484  	return nil
   485  }
   486  
   487  // Defaults are only valid CRDs created with the v1 API
   488  func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   489  	marshalledDefault, err := json.Marshal(m.Value)
   490  	if err != nil {
   491  		return err
   492  	}
   493  	schema.Default = &apiext.JSON{Raw: marshalledDefault}
   494  	return nil
   495  }
   496  
   497  func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   498  	marshalledExample, err := json.Marshal(m.Value)
   499  	if err != nil {
   500  		return err
   501  	}
   502  	schema.Example = &apiext.JSON{Raw: marshalledExample}
   503  	return nil
   504  }
   505  
   506  func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   507  	defTrue := true
   508  	schema.XPreserveUnknownFields = &defTrue
   509  	return nil
   510  }
   511  
   512  func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   513  	schema.XEmbeddedResource = true
   514  	return nil
   515  }
   516  
   517  // NB(JoelSpeed): we use this property in other markers here,
   518  // which means the "XIntOrString" marker *must* be applied first.
   519  
   520  func (m XIntOrString) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   521  	schema.XIntOrString = true
   522  	return nil
   523  }
   524  
   525  func (m XIntOrString) ApplyFirst() {}
   526  
   527  func (m XValidation) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   528  	schema.XValidations = append(schema.XValidations, apiext.ValidationRule{
   529  		Rule:    m.Rule,
   530  		Message: m.Message,
   531  	})
   532  	return nil
   533  }
   534  
   535  // +controllertools:marker:generateHelp:category=CRD
   536  
   537  // DeckhouseDocDefault configures the additional x-doc-default field
   538  // for property with default value for deckhouse documentation.
   539  type DeckhouseDocDefault struct {
   540  	Value interface{}
   541  }
   542  
   543  func (m DeckhouseDocDefault) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   544  	marshalledDocDefault, err := json.Marshal(m.Value)
   545  	if err != nil {
   546  		return err
   547  	}
   548  	schema.XDocDefault = &apiext.JSON{Raw: marshalledDocDefault}
   549  	return nil
   550  }
   551  
   552  // +controllertools:marker:generateHelp:category=CRD
   553  
   554  // DeckhouseDocDefault configures the additional x-doc-example field
   555  // for property with example usage for deckhouse documentation.
   556  type DeckhouseDocExample struct {
   557  	Value interface{}
   558  }
   559  
   560  func (m DeckhouseDocExample) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   561  	marshalledDocExample, err := json.Marshal(m.Value)
   562  	if err != nil {
   563  		return err
   564  	}
   565  	if schema.XDocExample != nil && len(schema.XDocExample.Raw) > 0 {
   566  		fullString := bytes.Join([][]byte{
   567  			escapeString(schema.XDocExample.Raw),
   568  			escapeString(marshalledDocExample),
   569  		}, []byte("\n"))
   570  		marshalFullString, err := json.Marshal(string(fullString))
   571  		if err != nil {
   572  			return err
   573  		}
   574  		marshalledDocExample = marshalFullString
   575  	}
   576  	schema.XDocExample = &apiext.JSON{Raw: marshalledDocExample}
   577  
   578  	return nil
   579  }
   580  
   581  func escapeString(b []byte) []byte {
   582  	return bytes.ReplaceAll(bytes.ReplaceAll(b, []byte("\""), nil), []byte("\\n"), []byte("\n"))
   583  }
   584  
   585  // +controllertools:marker:generateHelp:category=CRD
   586  
   587  // OneOf configures file name and variable name at package level
   588  // that will be used to generate oneOf CRD field.
   589  // for Example, for comment deckhouse:one:of=./cronjob_types.go=OneOfCRD
   590  // generator will search in package root for file ./cronjob_types.go and declared variable "OneOfCRD" in it
   591  //
   592  // file: ./cronjob_types.go:
   593  //
   594  //	package api
   595  //
   596  //	import (
   597  //		metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
   598  //	)
   599  //
   600  //	const OneOfCRD = `
   601  //	- required: [layout]
   602  //	  properties:
   603  //		layout:
   604  //		  enum: [Standard]
   605  //	- required: [layout]
   606  //	  properties:
   607  //		layout:
   608  //		  enum: [WithoutNAT]
   609  //	  masterNodeGroup:
   610  //		properties:
   611  //		  instanceClass:
   612  //			type: object
   613  //			properties:
   614  //			  disableExternalIP:
   615  //				enum: [false]
   616  //
   617  // `
   618  //
   619  // const "OneOfCRD" would be parsed to oneOf field of CRD as it is
   620  type OneOf string
   621  
   622  func (m OneOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
   623  	jsonProps, err := parseJSONSchemaPropsFromMarkerValue(string(m))
   624  	if err != nil {
   625  		return err
   626  	}
   627  	schema.OneOf = append(schema.OneOf, jsonProps...)
   628  	return nil
   629  }
   630  
   631  func parseJSONSchemaPropsFromMarkerValue(m string) ([]apiext.JSONSchemaProps, error) {
   632  	fileName, varName, err := fileNameVarNameFromMarkerValue(m)
   633  	if err != nil {
   634  		return nil, err
   635  	}
   636  
   637  	constant, err := constFromFileNameVarName(fileName, varName)
   638  	if err != nil {
   639  		return nil, err
   640  	}
   641  
   642  	jsonProps, err := parseJSONSchemaPropsFromAstVariable(constant)
   643  	if err != nil {
   644  		return nil, err
   645  	}
   646  	return jsonProps, nil
   647  }
   648  
   649  func fileNameVarNameFromMarkerValue(m string) (string, string, error) {
   650  	fc := strings.SplitN(string(m), "=", 2)
   651  	if len(fc) < 2 {
   652  		return "", "", fmt.Errorf("deckhouse:one:of not in format '<file name>=<variable name>'")
   653  	}
   654  	return fc[0], fc[1], nil
   655  }
   656  
   657  func constFromFileNameVarName(fileName, varName string) (*ast.ValueSpec, error) {
   658  	file, err := parser.ParseFile(token.NewFileSet(), fileName, nil, 0)
   659  	if err != nil {
   660  		return nil, err
   661  	}
   662  	astVar, ok := file.Scope.Objects[varName]
   663  	if !ok {
   664  		return nil, fmt.Errorf("no variable found with name '%s' in file '%s'", varName, fileName)
   665  	}
   666  
   667  	if astVar.Kind.String() != "const" {
   668  		return nil, fmt.Errorf("variable '%s' from file '%s' should be 'const'", varName, fileName)
   669  	}
   670  
   671  	variable, ok := astVar.Decl.(*ast.ValueSpec)
   672  	if !ok {
   673  		return nil, fmt.Errorf("bad const format, should be 'const <var name> = ...' at package level")
   674  	}
   675  	return variable, nil
   676  }
   677  
   678  func parseJSONSchemaPropsFromAstVariable(variable *ast.ValueSpec) ([]apiext.JSONSchemaProps, error) {
   679  	value, ok := variable.Values[0].(*ast.BasicLit)
   680  	if !ok {
   681  		return nil, fmt.Errorf("bad variable format, should be 'var/const <var name> = <string value>' at package level")
   682  	}
   683  
   684  	if value.Kind.String() != "STRING" {
   685  		return nil, fmt.Errorf("variable should be of type `string`")
   686  	}
   687  
   688  	stringValue := strings.Trim(value.Value, "`\"'")
   689  	var props []apiext.JSONSchemaProps
   690  	if err := yaml.Unmarshal([]byte(stringValue), &props); err != nil {
   691  		return nil, err
   692  	}
   693  	return props, nil
   694  }