sigs.k8s.io/cluster-api@v1.7.1/internal/topology/variables/cluster_variable_defaulting.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 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 26 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" 27 structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting" 28 "k8s.io/apimachinery/pkg/util/validation/field" 29 30 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 31 ) 32 33 // DefaultClusterVariables defaults ClusterVariables. 34 func DefaultClusterVariables(values []clusterv1.ClusterVariable, definitions []clusterv1.ClusterClassStatusVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) { 35 return defaultClusterVariables(values, definitions, true, fldPath) 36 } 37 38 // DefaultMachineVariables defaults MachineDeploymentVariables and MachinePoolVariables. 39 func DefaultMachineVariables(values []clusterv1.ClusterVariable, definitions []clusterv1.ClusterClassStatusVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) { 40 return defaultClusterVariables(values, definitions, false, fldPath) 41 } 42 43 // defaultClusterVariables defaults variables. 44 // If they do not exist yet, they are created if createVariables is set. 45 func defaultClusterVariables(values []clusterv1.ClusterVariable, definitions []clusterv1.ClusterClassStatusVariable, createVariables bool, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) { 46 var allErrs field.ErrorList 47 48 // Get a map of ClusterVariable values. This function validates that: 49 // - variables are not defined more than once in Cluster spec. 50 // - variables with the same name do not have a mix of empty and non-empty DefinitionFrom. 51 valuesIndex, err := newValuesIndex(values) 52 if err != nil { 53 var valueStrings []string 54 for _, v := range values { 55 valueStrings = append(valueStrings, fmt.Sprintf("Name: %s DefinitionFrom: %s", v.Name, v.DefinitionFrom)) 56 } 57 return nil, append(allErrs, field.Invalid(fldPath, "["+strings.Join(valueStrings, ",")+"]", fmt.Sprintf("cluster variables not valid: %s", err))) 58 } 59 60 // Get an index for each variable name and definition. 61 defIndex := newDefinitionsIndex(definitions) 62 63 // Get a deterministically ordered list of all variables defined in both the Cluster and the ClusterClass. 64 // Note: If the order is not deterministic variables would be continuously rewritten to the Cluster. 65 allVariables := getAllVariables(values, valuesIndex, definitions) 66 67 // Default all variables. 68 defaultedValues := []clusterv1.ClusterVariable{} 69 for _, variable := range allVariables { 70 // Get the variable definition from the ClusterClass. If the variable is not defined add an error. 71 definition, err := defIndex.get(variable.Name, variable.DefinitionFrom) 72 if err != nil { 73 allErrs = append(allErrs, field.Required(fldPath, err.Error())) 74 continue 75 } 76 77 // Get the current value of the variable if it is defined in the Cluster spec. 78 currentValue := getCurrentValue(variable, valuesIndex) 79 80 // Default the variable. 81 defaultedValue, errs := defaultValue(currentValue, definition, fldPath, createVariables) 82 if len(errs) > 0 { 83 allErrs = append(allErrs, errs...) 84 continue 85 } 86 87 // Continue if there is no defaulted variable. 88 // NOTE: This happens when the variable doesn't exist on the CLuster before and 89 // there is no top-level default value. 90 if defaultedValue == nil { 91 continue 92 } 93 defaultedValues = append(defaultedValues, *defaultedValue) 94 } 95 96 if len(allErrs) > 0 { 97 return nil, allErrs 98 } 99 return defaultedValues, nil 100 } 101 102 // getCurrentValue returns the value of a variable for its definitionFrom, or for an empty definitionFrom if it exists. 103 func getCurrentValue(variable clusterv1.ClusterVariable, valuesMap map[string]map[string]clusterv1.ClusterVariable) *clusterv1.ClusterVariable { 104 // If the value is set in the Cluster spec get the value. 105 if valuesForName, ok := valuesMap[variable.Name]; ok { 106 if value, ok := valuesForName[variable.DefinitionFrom]; ok { 107 return &value 108 } 109 } 110 return nil 111 } 112 113 // defaultValue defaults a clusterVariable based on the default value in the clusterClassVariable. 114 func defaultValue(currentValue *clusterv1.ClusterVariable, definition *statusVariableDefinition, fldPath *field.Path, createVariable bool) (*clusterv1.ClusterVariable, field.ErrorList) { 115 if currentValue == nil { 116 // Return if the variable does not exist yet and createVariable is false. 117 if !createVariable { 118 return nil, nil 119 } 120 // Return if the variable does not exist yet and there is no top-level default value. 121 if definition.Schema.OpenAPIV3Schema.Default == nil { 122 return nil, nil 123 } 124 } 125 126 // Convert schema to Kubernetes APIExtensions schema. 127 apiExtensionsSchema, errs := convertToAPIExtensionsJSONSchemaProps(&definition.Schema.OpenAPIV3Schema, field.NewPath("schema")) 128 if len(errs) > 0 { 129 return nil, field.ErrorList{field.Invalid(fldPath, "", 130 fmt.Sprintf("invalid schema in ClusterClass for variable %q: error to convert schema %v", definition.Name, errs))} 131 } 132 133 var value interface{} 134 // If the variable already exists, parse the current value. 135 if currentValue != nil && len(currentValue.Value.Raw) > 0 { 136 if err := json.Unmarshal(currentValue.Value.Raw, &value); err != nil { 137 return nil, field.ErrorList{field.Invalid(fldPath, "", 138 fmt.Sprintf("failed to unmarshal variable %q value %q: %v", currentValue.Name, string(currentValue.Value.Raw), err))} 139 } 140 } 141 142 // Structural schema defaulting does not work with scalar values, 143 // so we wrap the schema and the variable in objects. 144 // <variable-name>: <variable-value> 145 wrappedVariable := map[string]interface{}{ 146 definition.Name: value, 147 } 148 // type: object 149 // properties: 150 // <variable-name>: <variable-schema> 151 wrappedSchema := &apiextensions.JSONSchemaProps{ 152 Type: "object", 153 Properties: map[string]apiextensions.JSONSchemaProps{ 154 definition.Name: *apiExtensionsSchema, 155 }, 156 } 157 158 // Default the variable via the structural schema library. 159 ss, err := structuralschema.NewStructural(wrappedSchema) 160 if err != nil { 161 return nil, field.ErrorList{field.Invalid(fldPath, "", 162 fmt.Sprintf("failed defaulting variable %q: %v", currentValue.Name, err))} 163 } 164 structuraldefaulting.Default(wrappedVariable, ss) 165 166 // Marshal the defaulted value. 167 defaultedVariableValue, err := json.Marshal(wrappedVariable[definition.Name]) 168 if err != nil { 169 return nil, field.ErrorList{field.Invalid(fldPath, "", 170 fmt.Sprintf("failed to marshal default value of variable %q: %v", definition.Name, err))} 171 } 172 v := &clusterv1.ClusterVariable{ 173 Name: definition.Name, 174 Value: apiextensionsv1.JSON{ 175 Raw: defaultedVariableValue, 176 }, 177 DefinitionFrom: definition.From, 178 } 179 180 return v, nil 181 } 182 183 // getAllVariables returns a correctly ordered list of all variables defined in the ClusterClass and the Cluster. 184 func getAllVariables(values []clusterv1.ClusterVariable, valuesIndex map[string]map[string]clusterv1.ClusterVariable, definitions []clusterv1.ClusterClassStatusVariable) []clusterv1.ClusterVariable { 185 // allVariables is used to get a full correctly ordered list of variables. 186 allVariables := []clusterv1.ClusterVariable{} 187 uniqueVariableDefinitions := map[string]bool{} 188 189 // Add any values that already exist. 190 allVariables = append(allVariables, values...) 191 192 // Add variables from the ClusterClass, which currently don't exist on the Cluster. 193 for _, variable := range definitions { 194 for _, definition := range variable.Definitions { 195 definitionFrom := definition.From 196 197 // 1) If there is a value in the Cluster with this definitionFrom or with an empty definitionFrom this variable does not need to be defaulted. 198 if _, ok := valuesIndex[variable.Name]; ok { 199 if _, ok := valuesIndex[variable.Name][definitionFrom]; ok { 200 continue 201 } 202 if _, ok := valuesIndex[variable.Name][emptyDefinitionFrom]; ok { 203 continue 204 } 205 } 206 207 // 2) If the definition has no conflicts and no variable of the same name is defined in the Cluster set the definitionFrom to emptyDefinitionFrom. 208 if !variable.DefinitionsConflict && len(valuesIndex[variable.Name]) == 0 { 209 definitionFrom = emptyDefinitionFrom 210 } 211 212 // 3) If a variable with this name and definition has been added already, continue. 213 // This prevents adding the same variable multiple times where the variable is defaulted with an emptyDefinitionFrom. 214 if _, ok := uniqueVariableDefinitions[definitionFrom+variable.Name]; ok { 215 continue 216 } 217 218 // Otherwise add the variable to the list. 219 allVariables = append(allVariables, clusterv1.ClusterVariable{ 220 Name: variable.Name, 221 DefinitionFrom: definitionFrom, 222 }) 223 uniqueVariableDefinitions[definitionFrom+variable.Name] = true 224 } 225 } 226 return allVariables 227 }