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  }