sigs.k8s.io/cluster-api@v1.7.1/internal/topology/variables/cluster_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  	"encoding/json"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    25  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    26  	structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
    27  	"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
    28  	"k8s.io/apimachinery/pkg/util/validation/field"
    29  
    30  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    31  )
    32  
    33  // ValidateClusterVariables validates ClusterVariables based on the definitions in ClusterClass `.status.variables`.
    34  func ValidateClusterVariables(values []clusterv1.ClusterVariable, definitions []clusterv1.ClusterClassStatusVariable, fldPath *field.Path) field.ErrorList {
    35  	return validateClusterVariables(values, definitions, true, fldPath)
    36  }
    37  
    38  // ValidateMachineVariables validates MachineDeployment and MachinePool variables.
    39  func ValidateMachineVariables(values []clusterv1.ClusterVariable, definitions []clusterv1.ClusterClassStatusVariable, fldPath *field.Path) field.ErrorList {
    40  	return validateClusterVariables(values, definitions, false, fldPath)
    41  }
    42  
    43  // validateClusterVariables validates variable values according to the corresponding definition.
    44  func validateClusterVariables(values []clusterv1.ClusterVariable, definitions []clusterv1.ClusterClassStatusVariable, validateRequired bool, fldPath *field.Path) field.ErrorList {
    45  	var allErrs field.ErrorList
    46  
    47  	// Get a map of ClusterVariable values. This function validates that:
    48  	// - variables are not defined more than once in Cluster spec.
    49  	// - variables with the same name do not have a mix of empty and non-empty DefinitionFrom.
    50  	valuesMap, err := newValuesIndex(values)
    51  	if err != nil {
    52  		var valueStrings []string
    53  		for _, v := range values {
    54  			valueStrings = append(valueStrings, fmt.Sprintf("Name: %s DefinitionFrom: %s", v.Name, v.DefinitionFrom))
    55  		}
    56  		return append(allErrs, field.Invalid(fldPath, "["+strings.Join(valueStrings, ",")+"]", fmt.Sprintf("cluster variables not valid: %s", err)))
    57  	}
    58  
    59  	// Get an index of definitions for each variable name and definition from the ClusterClass variable.
    60  	defIndex := newDefinitionsIndex(definitions)
    61  
    62  	// Required variables definitions must exist as values on the Cluster.
    63  	if validateRequired {
    64  		allErrs = append(allErrs, validateRequiredVariables(valuesMap, defIndex, fldPath)...)
    65  	}
    66  
    67  	for _, value := range values {
    68  		// Values must have an associated definition and must have a non-empty definitionFrom if there are conflicting definitions.
    69  		definition, err := defIndex.get(value.Name, value.DefinitionFrom)
    70  		if err != nil {
    71  			allErrs = append(allErrs, field.Required(fldPath, err.Error())) // TODO: consider if to add ClusterClass name
    72  			continue
    73  		}
    74  
    75  		// Values must be valid according to the schema in their definition.
    76  		allErrs = append(allErrs, ValidateClusterVariable(value.DeepCopy(), &clusterv1.ClusterClassVariable{
    77  			Name:     value.Name,
    78  			Required: definition.Required,
    79  			Schema:   definition.Schema,
    80  		}, fldPath)...)
    81  	}
    82  
    83  	return allErrs
    84  }
    85  
    86  // validateRequiredVariables validates all required variables from the ClusterClass exist in the Cluster.
    87  func validateRequiredVariables(values map[string]map[string]clusterv1.ClusterVariable, definitions definitionsIndex, fldPath *field.Path) field.ErrorList {
    88  	var allErrs field.ErrorList
    89  
    90  	for name, definitionsForName := range definitions {
    91  		for _, def := range definitionsForName {
    92  			// Check the required value for the specific variable definition. If the variable is not required continue.
    93  			if !def.Required {
    94  				continue
    95  			}
    96  
    97  			// If there is no variable with this name defined in the Cluster add an error and continue.
    98  			valuesForName, found := values[name]
    99  			if !found {
   100  				allErrs = append(allErrs, field.Required(fldPath,
   101  					fmt.Sprintf("required variable with name %q must be defined", name))) // TODO: consider if to use "Clusters with ClusterClass %q must have a variable with name %q"
   102  				continue
   103  			}
   104  
   105  			// If there are no definition conflicts and the variable is set with an empty "DefinitionFrom" field return here.
   106  			// This is a valid way for users to define a required value for variables across all variable definitions.
   107  			if _, ok := valuesForName[emptyDefinitionFrom]; ok && !def.Conflicts {
   108  				continue
   109  			}
   110  
   111  			// If the variable is not set for the specific definitionFrom add an error.
   112  			if _, ok := valuesForName[def.From]; !ok {
   113  				allErrs = append(allErrs, field.Required(fldPath,
   114  					fmt.Sprintf("required variable with name %q from %q must be defined", name, def.From))) // TODO: consider if to use "Clusters with ClusterClass %q must have a variable with name %q"
   115  			}
   116  		}
   117  	}
   118  	return allErrs
   119  }
   120  
   121  // ValidateClusterVariable validates a clusterVariable.
   122  func ValidateClusterVariable(value *clusterv1.ClusterVariable, definition *clusterv1.ClusterClassVariable, fldPath *field.Path) field.ErrorList {
   123  	// Parse JSON value.
   124  	var variableValue interface{}
   125  	// Only try to unmarshal the clusterVariable if it is not nil, otherwise the variableValue is nil.
   126  	// Note: A clusterVariable with a nil value is the result of setting the variable value to "null" via YAML.
   127  	if value.Value.Raw != nil {
   128  		if err := json.Unmarshal(value.Value.Raw, &variableValue); err != nil {
   129  			return field.ErrorList{field.Invalid(fldPath.Child("value"), string(value.Value.Raw),
   130  				fmt.Sprintf("variable %q could not be parsed: %v", value.Name, err))}
   131  		}
   132  	}
   133  
   134  	// Convert schema to Kubernetes APIExtensions Schema.
   135  	apiExtensionsSchema, allErrs := convertToAPIExtensionsJSONSchemaProps(&definition.Schema.OpenAPIV3Schema, field.NewPath("schema"))
   136  	if len(allErrs) > 0 {
   137  		return field.ErrorList{field.InternalError(fldPath,
   138  			fmt.Errorf("failed to convert schema definition for variable %q; ClusterClass should be checked: %v", definition.Name, allErrs))} // TODO: consider if to add ClusterClass name
   139  	}
   140  
   141  	// Create validator for schema.
   142  	validator, _, err := validation.NewSchemaValidator(apiExtensionsSchema)
   143  	if err != nil {
   144  		return field.ErrorList{field.InternalError(fldPath,
   145  			fmt.Errorf("failed to create schema validator for variable %q; ClusterClass should be checked: %v", value.Name, err))} // TODO: consider if to add ClusterClass name
   146  	}
   147  
   148  	// Validate variable against the schema.
   149  	// NOTE: We're reusing a library func used in CRD validation.
   150  	if err := validation.ValidateCustomResource(fldPath, variableValue, validator); err != nil {
   151  		return err
   152  	}
   153  
   154  	return validateUnknownFields(fldPath, value, variableValue, apiExtensionsSchema)
   155  }
   156  
   157  // validateUnknownFields validates the given variableValue for unknown fields.
   158  // This func returns an error if there are variable fields in variableValue that are not defined in
   159  // variableSchema and if x-kubernetes-preserve-unknown-fields is not set.
   160  func validateUnknownFields(fldPath *field.Path, clusterVariable *clusterv1.ClusterVariable, variableValue interface{}, variableSchema *apiextensions.JSONSchemaProps) field.ErrorList {
   161  	// Structural schema pruning does not work with scalar values,
   162  	// so we wrap the schema and the variable in objects.
   163  	// <variable-name>: <variable-value>
   164  	wrappedVariable := map[string]interface{}{
   165  		clusterVariable.Name: variableValue,
   166  	}
   167  	// type: object
   168  	// properties:
   169  	//   <variable-name>: <variable-schema>
   170  	wrappedSchema := &apiextensions.JSONSchemaProps{
   171  		Type: "object",
   172  		Properties: map[string]apiextensions.JSONSchemaProps{
   173  			clusterVariable.Name: *variableSchema,
   174  		},
   175  	}
   176  	ss, err := structuralschema.NewStructural(wrappedSchema)
   177  	if err != nil {
   178  		return field.ErrorList{field.Invalid(fldPath, "",
   179  			fmt.Sprintf("failed defaulting variable %q: %v", clusterVariable.Name, err))}
   180  	}
   181  
   182  	// Run Prune to check if it would drop any unknown fields.
   183  	opts := structuralschema.UnknownFieldPathOptions{
   184  		// TrackUnknownFieldPaths has to be true so PruneWithOptions returns the unknown fields.
   185  		TrackUnknownFieldPaths: true,
   186  	}
   187  	prunedUnknownFields := structuralpruning.PruneWithOptions(wrappedVariable, ss, false, opts)
   188  	if len(prunedUnknownFields) > 0 {
   189  		// If prune dropped any unknown fields, return an error.
   190  		// This means that not all variable fields have been defined in the variable schema and
   191  		// x-kubernetes-preserve-unknown-fields was not set.
   192  		return field.ErrorList{
   193  			field.Invalid(fldPath, "",
   194  				fmt.Sprintf("failed validation: %q fields are not specified in the variable schema of variable %q", strings.Join(prunedUnknownFields, ","), clusterVariable.Name)),
   195  		}
   196  	}
   197  
   198  	return nil
   199  }