Skip to content

Commit

Permalink
Support for strategic-merge (#9)
Browse files Browse the repository at this point in the history
* Support for strategic-merge

This change, if applied, adds strategic-merge custom merge strategy to
agnosticv.

It can be applied to dict or list using the x-merge key in any schema.

Once this change is merged, we will update in a separate PR the default for `__meta__`
and `agnosticv_meta` variables to use 'strategic-merge' instead of 'merge'.

* Use asciidoc footnote instead of dedicated column

* wording
  • Loading branch information
fridim authored May 10, 2022
1 parent 13b3e82 commit b34efc0
Show file tree
Hide file tree
Showing 9 changed files with 445 additions and 1 deletion.
2 changes: 2 additions & 0 deletions cli/fixtures/.schemas/schema1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ x-merge:
strategy: merge
- path: /adict/alist
strategy: merge
- path: /adict/strategic_list
strategy: strategic-merge
properties:
purpose:
type: string
Expand Down
5 changes: 5 additions & 0 deletions cli/fixtures/test/BABYLON_EMPTY_CONFIG/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
# Agnosticd variables
platform: babylon

adict:
strategic_list:
- name: foo
value: common

output_dir: /tmp/output_dir/{{ env_type }}-{{ guid }}
env_type: test-empty-config
cloud_provider: test
Expand Down
5 changes: 5 additions & 0 deletions cli/fixtures/test/BABYLON_EMPTY_CONFIG/prod.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
---
purpose: prod

adict:
strategic_list:
- name: foo
value: prod

#########################################
# Meta variables for admin host scripts #
# ALL_agnosticv.sh #
Expand Down
3 changes: 3 additions & 0 deletions cli/fixtures/test/account.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

cloud_provider: none
adict:
strategic_list:
- name: foo
value: account
alist:
- fromaccount
blist:
Expand Down
19 changes: 19 additions & 0 deletions cli/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
"github.com/imdario/mergo"
)


// MergeStrategy type to define custom merge strategies.
// Strategy: the name of the strategy
// Path: the path in the structure of the vars to apply the strategy against.
type MergeStrategy struct {
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Expand Down Expand Up @@ -148,6 +152,16 @@ func customStrategyMerge(final map[string]any, source map[string]any, strategy M
logDebug.Println("src", src)
logDebug.Println("dst", dst)
dst = append(dst, src...)

case "strategic-merge":
logDebug.Printf("customStrategyMerge(%v) strategic merge", strategy)
logDebug.Println("src", src)
logDebug.Println("dst", dst)
dst = append(dst, src...)
if dst, err = strategicCleanupSlice(dst); err != nil {
return err
}

default:
logErr.Fatal("Unknown merge strategy for list: ", strategy.Strategy)
}
Expand Down Expand Up @@ -196,6 +210,11 @@ func customStrategyMerge(final map[string]any, source map[string]any, strategy M
return err
}

case "strategic-merge":
if err := strategicMerge(dstMap, src.(map[string]any)); err != nil {
return err
}

default:
logErr.Fatal("Unknown merge strategy ", strategy.Strategy)
}
Expand Down
28 changes: 28 additions & 0 deletions cli/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,31 @@ func TestMergeStrategyOverwrite(t *testing.T) {
t.Error("Only myspecialgroup should be present", value, expected)
}
}

func TestMergeStrategicMergeList(t *testing.T) {
rootFlag = abs("fixtures")
initSchemaList()
initMergeStrategies()
validateFlag = true
merged, _, err := mergeVars(
"fixtures/test/BABYLON_EMPTY_CONFIG/prod.yaml",
mergeStrategies,
)
if err != nil {
t.Fatal(err)
}

_, value, _, err := Get(merged, "/adict/strategic_list")
if err != nil {
t.Error(err)
}
expected := []any{
map[string]any{
"name": "foo",
"value": "prod",
},
}
if !reflect.DeepEqual(value, expected) {
t.Error("Only myspecialgroup should be present", value, expected)
}
}
90 changes: 90 additions & 0 deletions cli/strategic_merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"fmt"
"reflect"

"github.com/imdario/mergo"
)


func strategicCleanupSlice(elems []any) ([]any, error) {
result := []any{}
done := map[string]int{}

for index, elem := range elems {
if reflect.TypeOf(elem).Kind() != reflect.Map {
// strategic merge works only on map, if it's not a map, just add the elem and continue
result = append(result, elem)
continue
}

elemMap := elem.(map[string]any)


if name, ok := elemMap["name"]; ok {
if reflect.TypeOf(elemMap["name"]).Kind() != reflect.String {
return result, fmt.Errorf("strategic merge cannot work if 'name' is not a string")
}
nameStr := name.(string)
if doneIndex, ok := done[nameStr]; ok {
// An element with the same name exists, replace that element
result1 := append(result[:doneIndex], elem)
result = append(result1, result[doneIndex+1:]...)
continue
}
// Append element
result = append(result, elem)
done[nameStr] = index
continue
}
result = append(result, elem)
}

return result, nil
}
func strategicCleanupMap(m map[string]any) error {
for key, v := range m {
if v == nil {
continue
}
if reflect.TypeOf(v).Kind() == reflect.Map {
vMap := v.(map[string]any)

if err := strategicCleanupMap(vMap); err != nil {
return err
}
continue
}

if reflect.TypeOf(v).Kind() == reflect.Slice {
vSlice := v.([]any)
res, err := strategicCleanupSlice(vSlice)
if err != nil {
return err
}

m[key] = res
continue
}
}

return nil
}

func strategicMerge(dst map[string]any, src map[string]any) error {
if err := mergo.Merge(
&dst,
src,
mergo.WithOverride,
mergo.WithOverwriteWithEmptyValue,
mergo.WithAppendSlice,
); err != nil {
return err
}

if err := strategicCleanupMap(dst); err != nil {
return err
}
return nil
}
Loading

0 comments on commit b34efc0

Please sign in to comment.