From c5fa740bf8ed9a57ee6fc9a1e335dbb748af85b3 Mon Sep 17 00:00:00 2001 From: Sam Gartner Date: Fri, 23 Aug 2024 22:40:28 -0500 Subject: [PATCH] initial commit Add LoadConfigFromFile Move config ops to a seperate file fix typo Update permutations.go Update permutations.go Update configoperations.go Update permutations.go Update configoperations.go Create example_test.go Update example_test.go Update to address PR comments Update example_test.go update with correct locations update directory location update naming --- pkg/config/config.go | 16 +++ pkg/config/operations/operations.go | 95 ++++++++++++++++ .../operations/permutations/example_test.go | 38 +++++++ .../operations/permutations/permutations.go | 106 ++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 pkg/config/operations/operations.go create mode 100644 pkg/config/operations/permutations/example_test.go create mode 100644 pkg/config/operations/permutations/permutations.go diff --git a/pkg/config/config.go b/pkg/config/config.go index f5e2f5ac..c9758722 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -119,3 +119,19 @@ func WriteConfig(key string, config interface{}) error { return nil } + +// LoadConfigFromFile loads an entire yaml file into a map[string]any +func LoadConfigFromFile(filePath string) map[string]any { + allString, err := os.ReadFile(filePath) + if err != nil { + panic(err) + } + + var all map[string]any + err = yaml.Unmarshal(allString, &all) + if err != nil { + panic(err) + } + + return all +} diff --git a/pkg/config/operations/operations.go b/pkg/config/operations/operations.go new file mode 100644 index 00000000..21b48e8e --- /dev/null +++ b/pkg/config/operations/operations.go @@ -0,0 +1,95 @@ +package operations + +import ( + "encoding/json" + "errors" + "fmt" + + "sigs.k8s.io/yaml" +) + +// ReplaceValue recursively traverses keyPath, replacing the specified replaceVal in searchMap. +func ReplaceValue(keyPath []string, replaceVal any, searchMap map[string]any) (map[string]any, error) { + if len(keyPath) <= 1 { + searchMap[keyPath[0]] = replaceVal + + return searchMap, nil + } else { + var err error + + if _, ok := searchMap[keyPath[0]].(map[string]any); ok { + searchMap[keyPath[0]], err = ReplaceValue(keyPath[1:], replaceVal, searchMap[keyPath[0]].(map[string]any)) + if err != nil { + return nil, err + } + } else if _, ok := searchMap[keyPath[0]].([]any); ok { + for i := range searchMap[keyPath[0]].([]any) { + searchMap[keyPath[0]].([]any)[i], err = ReplaceValue(keyPath[1:], replaceVal, searchMap[keyPath[0]].([]any)[i].(map[string]any)) + if err != nil { + return nil, err + } + } + } + } + + return searchMap, nil +} + +// GetValue recursively traverses keyPath, returning the value of the specified keyPath in searchMap. +func GetValue(keyPath []string, searchMap map[string]any) (any, error) { + var err error + var keypathvalues any + if len(keyPath) == 1 { + keypathvalues, ok := searchMap[keyPath[0]] + if !ok { + err = errors.New(fmt.Sprintf("expected key does not exist: %s", keyPath[0])) + } + return keypathvalues, err + } else { + if _, ok := searchMap[keyPath[0]].(map[string]any); ok { + keypathvalues, err = GetValue(keyPath[1:], searchMap[keyPath[0]].(map[string]any)) + if err != nil { + return nil, err + } + } else if _, ok := searchMap[keyPath[0]].([]any); ok { + for i := range searchMap[keyPath[0]].([]any) { + keypathvalues, err = GetValue(keyPath[1:], searchMap[keyPath[0]].([]any)[i].(map[string]any)) + if err != nil { + return nil, err + } + } + } + } + + return keypathvalues, err +} + +// LoadObjectFromMap unmarshals a specific key's value into an object +func LoadObjectFromMap(key string, config map[string]any, object any) { + keyConfig := config[key] + scopedString, err := yaml.Marshal(keyConfig) + if err != nil { + panic(err) + } + + err = yaml.Unmarshal(scopedString, &object) + if err != nil { + panic(err) + } +} + +// DeepCopyMap creates a copy of the map that doesn't have any links to the original map +func DeepCopyMap(config map[string]any) (map[string]any, error) { + marshaledConfig, err := json.Marshal(config) + if err != nil { + return nil, err + } + + unmarshaledConfig := make(map[string]any) + err = json.Unmarshal(marshaledConfig, &unmarshaledConfig) + if err != nil { + return nil, err + } + + return unmarshaledConfig, nil +} diff --git a/pkg/config/operations/permutations/example_test.go b/pkg/config/operations/permutations/example_test.go new file mode 100644 index 00000000..802f3d04 --- /dev/null +++ b/pkg/config/operations/permutations/example_test.go @@ -0,0 +1,38 @@ +package permutations + +import ( + "fmt" + + mapOperations "github.com/rancher/shepherd/pkg/config/operations" +) + +// ExamplePermute is an example of how to use the permutation objects and the permute function. +func ExamplePermute() { + config := map[string]any{ + "foo1": map[string]any{ + "nested-foo1": []any{"bar1", "bar2"}, + }, + "foo2": []any{"bar3", "bar4"}, + } + + nestedFoo1Path := []string{"foo1", "nested-foo1"} + nestedFoo1Value, _ := mapOperations.GetValue(nestedFoo1Path, config) + foo1Permutation := CreatePermutation(nestedFoo1Path, nestedFoo1Value.([]any), []Relationship{}) + + nestedFoo2Path := []string{"foo2"} + nestedFoo2Value, _ := mapOperations.GetValue(nestedFoo2Path, config) + foo2Permutation := CreatePermutation(nestedFoo2Path, nestedFoo2Value.([]any), []Relationship{}) + + permutations := []Permutation{foo1Permutation, foo2Permutation} + permutedConfigs, _ := Permute(permutations, config) + + for _, permutedConfig := range permutedConfigs { + fmt.Println(permutedConfig) + //Output: + //map[foo1:map[nested-foo1:bar1] foo2:bar3] + //map[foo1:map[nested-foo1:bar1] foo2:bar4] + //map[foo1:map[nested-foo1:bar2] foo2:bar3] + //map[foo1:map[nested-foo1:bar2] foo2:bar4] + } + +} diff --git a/pkg/config/operations/permutations/permutations.go b/pkg/config/operations/permutations/permutations.go new file mode 100644 index 00000000..da9b889a --- /dev/null +++ b/pkg/config/operations/permutations/permutations.go @@ -0,0 +1,106 @@ +package permutations + +import ( + "errors" + + mapOperations "github.com/rancher/shepherd/pkg/config/operations" +) + +// Relationship structs are used to create an association between a parent value and either a set of permutations or the value of a different key +type Relationship struct { + ParentValue any `json:"parentValue" yaml:"parentValue"` + ChildKeyPath []string `json:"childKeyPath" yaml:"childkeyPath"` + ChildKeyPathValue any `json:"childKeyPathValue" yaml:"childkeyPathValue"` + ChildPermutations []Permutation `json:"childPermutations" yaml:"childPermutations"` +} + +// Permutation structs are used to describe a single permutation +type Permutation struct { + KeyPath []string `json:"keyPath" yaml:"keyPath"` + KeyPathValues []any `json:"keyPathValue" yaml:"keyPath"` + KeyPathValueRelationships []Relationship `json:"keyPathValueRelationships" yaml:"KeyPathValueRelationships"` +} + +// CreateRelationship is a constructor for the relationship struct +func CreateRelationship(parentValue any, childKeyPath []string, childKeyPathValue any, childPermutations []Permutation) Relationship { + return Relationship{ + ParentValue: parentValue, + ChildKeyPath: childKeyPath, + ChildKeyPathValue: childKeyPathValue, + ChildPermutations: childPermutations, + } +} + +// CreatePermutation is a constructor for the permutation struct +func CreatePermutation(keyPath []string, keyPathValues []any, keyPathValueRelationships []Relationship) Permutation { + return Permutation{ + KeyPath: keyPath, + KeyPathValues: keyPathValues, + KeyPathValueRelationships: keyPathValueRelationships, + } +} + +// Permute iterates over a list of permutation structs and permutes the base config with each of the permutations +func Permute(permutations []Permutation, baseConfig map[string]any) ([]map[string]any, error) { + var configs []map[string]any + var err error + if len(permutations) == 0 { + err = errors.New("no permutations provided") + return configs, err + } + + for _, keyPathValue := range permutations[0].KeyPathValues { + permutedConfig, err := mapOperations.DeepCopyMap(baseConfig) + if err != nil { + return nil, err + } + + permutedConfig, err = mapOperations.ReplaceValue(permutations[0].KeyPath, keyPathValue, permutedConfig) + if err != nil { + return nil, err + } + + subPermutations := false + for _, relationship := range permutations[0].KeyPathValueRelationships { + if relationship.ParentValue == keyPathValue { + if len(relationship.ChildKeyPath) > 1 && relationship.ChildKeyPathValue != nil { + permutedConfig, err = mapOperations.ReplaceValue(relationship.ChildKeyPath, relationship.ChildKeyPathValue, permutedConfig) + if err != nil { + return nil, err + } + } + + var relationshipPermutedConfigs []map[string]any + if len(relationship.ChildPermutations) > 0 { + subPermutations = true + relationshipPermutedConfigs, err = Permute(relationship.ChildPermutations, permutedConfig) + if err != nil { + return nil, err + } + } + + configs = append(configs, relationshipPermutedConfigs...) + } + } + + if !subPermutations { + configs = append(configs, permutedConfig) + } + } + + var finalConfigs []map[string]any + if len(permutations) == 1 { + return configs, nil + } else { + for _, config := range configs { + permutedConfigs, err := Permute(permutations[1:], config) + if err != nil { + return nil, err + } + + finalConfigs = append(finalConfigs, permutedConfigs...) + } + } + + return finalConfigs, err +}