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 }