sigs.k8s.io/cluster-api@v1.7.1/internal/topology/variables/utils.go (about) 1 /* 2 Copyright 2023 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 "github.com/pkg/errors" 21 kerrors "k8s.io/apimachinery/pkg/util/errors" 22 23 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 24 ) 25 26 const ( 27 // emptyDefinitionFrom is the definitionFrom value used when none is supplied by the user. 28 emptyDefinitionFrom = "" 29 ) 30 31 // newValuesIndex return a map of ClusterVariable values per name and definitionsFrom. 32 // This function validates that: 33 // - variables are not defined more than once in Cluster spec/ 34 // - variables with the same name do not have a mix of empty and non-empty DefinitionFrom. 35 func newValuesIndex(values []clusterv1.ClusterVariable) (map[string]map[string]clusterv1.ClusterVariable, error) { 36 valuesMap := map[string]map[string]clusterv1.ClusterVariable{} 37 errs := []error{} 38 for _, value := range values { 39 c := value 40 _, ok := valuesMap[c.Name] 41 if !ok { 42 valuesMap[c.Name] = map[string]clusterv1.ClusterVariable{} 43 } 44 // Check that the variable has not been defined more than once with the same definitionFrom. 45 if _, ok := valuesMap[c.Name][c.DefinitionFrom]; ok { 46 errs = append(errs, errors.Errorf("variable %q from %q is defined more than once", c.Name, c.DefinitionFrom)) 47 continue 48 } 49 // Add the variable. 50 valuesMap[c.Name][c.DefinitionFrom] = c 51 } 52 if len(errs) > 0 { 53 return nil, kerrors.NewAggregate(errs) 54 } 55 // Validate that the variables do not have incompatible values in their `definitionFrom` fields. 56 if err := validateValuesDefinitionFrom(valuesMap); err != nil { 57 return nil, err 58 } 59 60 return valuesMap, nil 61 } 62 63 // validateValuesDefinitionFrom validates that variables are not defined with both an empty DefinitionFrom and a 64 // non-empty DefinitionFrom. 65 func validateValuesDefinitionFrom(values map[string]map[string]clusterv1.ClusterVariable) error { 66 var errs []error 67 for name, valuesForName := range values { 68 for _, value := range valuesForName { 69 // Append an error if the value has a non-empty definitionFrom but there's also a value with 70 // an empty DefinitionFrom. 71 if _, ok := valuesForName[emptyDefinitionFrom]; ok && value.DefinitionFrom != emptyDefinitionFrom { 72 errs = append(errs, errors.Errorf("variable %q is defined with a mix of empty and non-empty values for definitionFrom", name)) 73 break // No need to check other values for this variable. 74 } 75 } 76 } 77 if len(errs) > 0 { 78 return kerrors.NewAggregate(errs) 79 } 80 return nil 81 } 82 83 type statusVariableDefinition struct { 84 *clusterv1.ClusterClassStatusVariableDefinition 85 Name string 86 Conflicts bool 87 } 88 89 type definitionsIndex map[string]map[string]*statusVariableDefinition 90 91 // newDefinitionsIndex returns a definitionsIndex with ClusterClassStatusVariable definitions by name and definition.From. 92 // This index has special handling for variables with no definition conflicts in the `get` method. 93 func newDefinitionsIndex(definitions []clusterv1.ClusterClassStatusVariable) definitionsIndex { 94 i := definitionsIndex{} 95 for _, def := range definitions { 96 i.store(def) 97 } 98 return i 99 } 100 func (i definitionsIndex) store(definition clusterv1.ClusterClassStatusVariable) { 101 for _, d := range definition.Definitions { 102 if _, ok := i[definition.Name]; !ok { 103 i[definition.Name] = map[string]*statusVariableDefinition{} 104 } 105 i[definition.Name][d.From] = &statusVariableDefinition{ 106 Name: definition.Name, 107 Conflicts: definition.DefinitionsConflict, 108 ClusterClassStatusVariableDefinition: d.DeepCopy(), 109 } 110 } 111 } 112 113 // get returns a statusVariableDefinition for a given name and definitionFrom. If the definition has no conflicts it can 114 // be retrieved using an emptyDefinitionFrom. 115 // Return an error if the definition is not found or if the definitionFrom is empty and there are conflicts in the definitions. 116 func (i definitionsIndex) get(name, definitionFrom string) (*statusVariableDefinition, error) { 117 // If no variable with this name exists return an error. 118 if _, ok := i[name]; !ok { 119 return nil, errors.Errorf("no definitions found for variable %q", name) 120 } 121 122 // If the definition exists for the specific definitionFrom, return it. 123 if def, ok := i[name][definitionFrom]; ok { 124 return def, nil 125 } 126 127 // If definitionFrom is empty and there are no conflicts return a definition with an emptyDefinitionFrom. 128 if definitionFrom == emptyDefinitionFrom { 129 for _, def := range i[name] { 130 if !def.Conflicts { 131 return &statusVariableDefinition{ 132 Name: def.Name, 133 Conflicts: def.Conflicts, 134 ClusterClassStatusVariableDefinition: &clusterv1.ClusterClassStatusVariableDefinition{ 135 // Return the definition with an empty definitionFrom. This ensures when a user gets 136 // a definition with an emptyDefinitionFrom, the return value also has emptyDefinitionFrom. 137 // This is used in variable defaulting to ensure variables that only need one value for multiple 138 // definitions have an emptyDefinitionFrom. 139 From: emptyDefinitionFrom, 140 Required: def.Required, 141 Schema: def.Schema, 142 }, 143 }, nil 144 } 145 return nil, errors.Errorf("variable %q has conflicting definitions. It requires a non-empty `definitionFrom`", name) 146 } 147 } 148 return nil, errors.Errorf("no definitions found for variable %q from %q", name, definitionFrom) 149 }