Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2.9] Generic Permutations #325

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
95 changes: 95 additions & 0 deletions pkg/config/operations/operations.go
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 38 additions & 0 deletions pkg/config/operations/permutations/example_test.go
Original file line number Diff line number Diff line change
@@ -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]
}

}
106 changes: 106 additions & 0 deletions pkg/config/operations/permutations/permutations.go
Original file line number Diff line number Diff line change
@@ -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
}