sigs.k8s.io/cluster-api@v1.6.3/internal/topology/variables/clusterclass_variable_validation.go (about)

     1  /*
     2  Copyright 2021 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 variables
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    25  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    26  	structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
    27  	"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  
    31  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    32  )
    33  
    34  const (
    35  	// builtinsName is the name of the builtin variable.
    36  	builtinsName = "builtin"
    37  )
    38  
    39  // ValidateClusterClassVariables validates clusterClassVariable.
    40  func ValidateClusterClassVariables(ctx context.Context, clusterClassVariables []clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList {
    41  	var allErrs field.ErrorList
    42  
    43  	allErrs = append(allErrs, validateClusterClassVariableNamesUnique(clusterClassVariables, fldPath)...)
    44  
    45  	for i := range clusterClassVariables {
    46  		allErrs = append(allErrs, validateClusterClassVariable(ctx, &clusterClassVariables[i], fldPath.Index(i))...)
    47  	}
    48  
    49  	return allErrs
    50  }
    51  
    52  // validateClusterClassVariableNamesUnique validates that ClusterClass variable names are unique.
    53  func validateClusterClassVariableNamesUnique(clusterClassVariables []clusterv1.ClusterClassVariable, pathPrefix *field.Path) field.ErrorList {
    54  	var allErrs field.ErrorList
    55  
    56  	variableNames := sets.Set[string]{}
    57  	for i, clusterClassVariable := range clusterClassVariables {
    58  		if variableNames.Has(clusterClassVariable.Name) {
    59  			allErrs = append(allErrs,
    60  				field.Invalid(
    61  					pathPrefix.Index(i).Child("name"),
    62  					clusterClassVariable.Name,
    63  					fmt.Sprintf("variable name must be unique. Variable with name %q is defined more than once", clusterClassVariable.Name),
    64  				),
    65  			)
    66  		}
    67  		variableNames.Insert(clusterClassVariable.Name)
    68  	}
    69  
    70  	return allErrs
    71  }
    72  
    73  // validateClusterClassVariable validates a ClusterClassVariable.
    74  func validateClusterClassVariable(ctx context.Context, variable *clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList {
    75  	allErrs := field.ErrorList{}
    76  
    77  	// Validate variable name.
    78  	allErrs = append(allErrs, validateClusterClassVariableName(variable.Name, fldPath.Child("name"))...)
    79  
    80  	// Validate schema.
    81  	allErrs = append(allErrs, validateRootSchema(ctx, variable, fldPath.Child("schema", "openAPIV3Schema"))...)
    82  
    83  	return allErrs
    84  }
    85  
    86  // validateClusterClassVariableName validates a variable name.
    87  func validateClusterClassVariableName(variableName string, fldPath *field.Path) field.ErrorList {
    88  	allErrs := field.ErrorList{}
    89  
    90  	if variableName == "" {
    91  		allErrs = append(allErrs, field.Required(fldPath, "variable name must be defined"))
    92  	}
    93  
    94  	if variableName == builtinsName {
    95  		allErrs = append(allErrs, field.Invalid(fldPath, variableName, fmt.Sprintf("%q is a reserved variable name", builtinsName)))
    96  	}
    97  	if strings.Contains(variableName, ".") {
    98  		allErrs = append(allErrs, field.Invalid(fldPath, variableName, "variable name cannot contain \".\"")) // TODO: consider if to restrict variable names to RFC 1123
    99  	}
   100  
   101  	return allErrs
   102  }
   103  
   104  var validVariableTypes = sets.Set[string]{}.Insert("object", "array", "string", "number", "integer", "boolean")
   105  
   106  // validateRootSchema validates the schema.
   107  func validateRootSchema(ctx context.Context, clusterClassVariable *clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList {
   108  	var allErrs field.ErrorList
   109  
   110  	apiExtensionsSchema, allErrs := convertToAPIExtensionsJSONSchemaProps(&clusterClassVariable.Schema.OpenAPIV3Schema, field.NewPath("schema"))
   111  	if len(allErrs) > 0 {
   112  		return field.ErrorList{field.InternalError(fldPath,
   113  			fmt.Errorf("failed to convert schema definition for variable %q; ClusterClass should be checked: %v", clusterClassVariable.Name, allErrs))} // TODO: consider if to add ClusterClass name
   114  	}
   115  
   116  	// Validate structural schema.
   117  	// Note: structural schema only allows `type: object` on the root level, so we wrap the schema with:
   118  	// type: object
   119  	// properties:
   120  	//   variableSchema: <variable-schema>
   121  	wrappedSchema := &apiextensions.JSONSchemaProps{
   122  		Type: "object",
   123  		Properties: map[string]apiextensions.JSONSchemaProps{
   124  			"variableSchema": *apiExtensionsSchema,
   125  		},
   126  	}
   127  
   128  	// Get the structural schema for the variable.
   129  	ss, err := structuralschema.NewStructural(wrappedSchema)
   130  	if err != nil {
   131  		return append(allErrs, field.Invalid(fldPath.Child("schema"), "", err.Error()))
   132  	}
   133  
   134  	// Validate the schema.
   135  	if validationErrors := structuralschema.ValidateStructural(fldPath.Child("schema"), ss); len(validationErrors) > 0 {
   136  		return append(allErrs, validationErrors...)
   137  	}
   138  
   139  	// Validate defaults in the structural schema.
   140  	validationErrors, err := structuraldefaulting.ValidateDefaults(ctx, fldPath.Child("schema"), ss, true, true)
   141  	if err != nil {
   142  		return append(allErrs, field.Invalid(fldPath.Child("schema"), "", err.Error()))
   143  	}
   144  	if len(validationErrors) > 0 {
   145  		return append(allErrs, validationErrors...)
   146  	}
   147  
   148  	// If the structural schema is valid, ensure a schema validator can be constructed.
   149  	if _, _, err := validation.NewSchemaValidator(apiExtensionsSchema); err != nil {
   150  		return append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("failed to build validator: %v", err)))
   151  	}
   152  
   153  	allErrs = append(allErrs, validateSchema(apiExtensionsSchema, fldPath)...)
   154  	return allErrs
   155  }
   156  
   157  func validateSchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
   158  	var allErrs field.ErrorList
   159  
   160  	// Validate that type is one of the validVariableTypes.
   161  	switch {
   162  	case schema.Type == "":
   163  		return field.ErrorList{field.Required(fldPath.Child("type"), "type cannot be empty")}
   164  	case !validVariableTypes.Has(schema.Type):
   165  		return field.ErrorList{field.NotSupported(fldPath.Child("type"), schema.Type, sets.List(validVariableTypes))}
   166  	}
   167  
   168  	// If the structural schema is valid, ensure a schema validator can be constructed.
   169  	validator, _, err := validation.NewSchemaValidator(schema)
   170  	if err != nil {
   171  		return append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("failed to build validator: %v", err)))
   172  	}
   173  
   174  	if schema.Example != nil {
   175  		if errs := validation.ValidateCustomResource(fldPath, *schema.Example, validator); len(errs) > 0 {
   176  			allErrs = append(allErrs, field.Invalid(fldPath.Child("example"), schema.Example, fmt.Sprintf("invalid value in example: %v", errs)))
   177  		}
   178  	}
   179  
   180  	for i, enum := range schema.Enum {
   181  		if enum != nil {
   182  			if errs := validation.ValidateCustomResource(fldPath, enum, validator); len(errs) > 0 {
   183  				allErrs = append(allErrs, field.Invalid(fldPath.Child("enum").Index(i), enum, fmt.Sprintf("invalid value in enum: %v", errs)))
   184  			}
   185  		}
   186  	}
   187  
   188  	if schema.AdditionalProperties != nil {
   189  		if len(schema.Properties) > 0 {
   190  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive"))
   191  		}
   192  		allErrs = append(allErrs, validateSchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"))...)
   193  	}
   194  
   195  	for propertyName, propertySchema := range schema.Properties {
   196  		p := propertySchema
   197  		allErrs = append(allErrs, validateSchema(&p, fldPath.Child("properties").Key(propertyName))...)
   198  	}
   199  
   200  	if schema.Items != nil {
   201  		allErrs = append(allErrs, validateSchema(schema.Items.Schema, fldPath.Child("items"))...)
   202  	}
   203  
   204  	return allErrs
   205  }