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