Skip to content
Snippets Groups Projects
Commit 1875a33eb32b authored by Christophe de Vienne's avatar Christophe de Vienne
Browse files

Implement variable overlay with dotted paths

If an overlay variable has a dotted name, it is used as a path to patch
a nested value in the original variable.

For instance, given the following original variables:

variables:
  d:
    a: value
    b: other

A config overlay can change the 'a' value without touching the other 'd'
attributes :

variables:
  d.a: new value


The resulting variables would be :

variables:
  d:
    a: new value
    b: other
parent 80cefe17f4bb
No related branches found
No related tags found
1 merge request!3Topic/default/variables as mapping
Pipeline #49534 failed
...@@ -676,7 +676,5 @@ ...@@ -676,7 +676,5 @@
// variables into the current cmdconfig by replacing old ones // variables into the current cmdconfig by replacing old ones
// and adding the new ones // and adding the new ones
func (c *CmdConfig) MergeVariables(other *Config) { func (c *CmdConfig) MergeVariables(other *Config) {
for _, variable := range other.Variables { c.Spec.Variables.Overlay(other.Variables...)
c.overlayVariable(variable)
}
} }
...@@ -682,16 +680,1 @@ ...@@ -682,16 +680,1 @@
} }
// overlayVariable takes a variable in and either replaces an existing variable
// of the same name or create a new variable in the config if no matching name
// is found
func (c *CmdConfig) overlayVariable(v Variable) {
// find same variable by name and replace is value
// if not found then create the variable
for index, originalVariable := range c.Spec.Variables {
if originalVariable.Name == v.Name {
c.Spec.Variables[index].Value = v.Value
return
}
}
c.Spec.Variables = append(c.Spec.Variables, v)
}
...@@ -168,6 +168,22 @@ ...@@ -168,6 +168,22 @@
) )
} }
func TestInheritVariables(t *testing.T) {
tl := testutils.NewTestLogger(t)
testNS := "environments/ns1"
absConfigDir, err := filepath.Abs(fixtures)
require.NoError(t, err)
c := runner.NewCmdConfig(tl.Logger(), absConfigDir, testNS, false, "")
tmpDir, err := os.MkdirTemp(os.TempDir(), "beaver-")
require.NoError(t, err)
defer func() {
assert.NoError(t, os.RemoveAll(tmpDir))
}()
require.NoError(t, c.Initialize(tmpDir))
assert.Equal(t, "another value", c.Spec.Variables.GetD("test-nested.nested-value1", nil))
assert.Equal(t, "Value2", c.Spec.Variables.GetD("test-nested.nested-value2", nil))
}
func TestCreateConfig(t *testing.T) { func TestCreateConfig(t *testing.T) {
tl := testutils.NewTestLogger(t) tl := testutils.NewTestLogger(t)
testNS := "environments/ns1" testNS := "environments/ns1"
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
value: orus.io value: orus.io
- name: ROLE - name: ROLE
value: odoo-batch value: odoo-batch
- name: test-nested
value:
nested-value1: Value1
nested-value2: Value2
charts: charts:
postgres: postgres:
type: helm type: helm
...@@ -18,4 +22,4 @@ ...@@ -18,4 +22,4 @@
name: xbus-pipelines name: xbus-pipelines
args: args:
- flag: --from-file - flag: --from-file
value: pipelines value: pipelines
\ No newline at end of file
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
variables: variables:
- name: VAULT_KV - name: VAULT_KV
value: k8s.orus.io value: k8s.orus.io
- name: test-nested.nested-value1
value: another value
create: create:
- type: configmap - type: configmap
name: xbus-pipelines name: xbus-pipelines
......
...@@ -16,6 +16,30 @@ ...@@ -16,6 +16,30 @@
type Variables []Variable type Variables []Variable
func (v Variables) Get(path string) (interface{}, bool) {
sp := strings.Split(path, ".")
head := sp[0]
tail := sp[1:]
for _, variable := range v {
if variable.Name == head {
if len(tail) == 0 {
return variable.Value, true
}
return lookupVariable(variable.Value, strings.Join(tail, "."))
}
}
return nil, false
}
func (v Variables) GetD(path string, defaultValue interface{}) interface{} {
ret, ok := v.Get(path)
if ok {
return ret
}
return defaultValue
}
func (v *Variables) UnmarshalYAML(node *yaml.Node) error { func (v *Variables) UnmarshalYAML(node *yaml.Node) error {
if err := node.Decode((*[]Variable)(v)); err == nil { if err := node.Decode((*[]Variable)(v)); err == nil {
return nil return nil
...@@ -36,7 +60,72 @@ ...@@ -36,7 +60,72 @@
return nil return nil
} }
func lookupVariable(variables map[string]interface{}, name string) (interface{}, bool) { func (v *Variables) Overlay(variables ...Variable) {
var newVariables Variables
for _, inputVar := range variables {
path := strings.Split(inputVar.Name, ".")
head := path[0]
tail := path[1:]
for i := range *v {
if (*v)[i].Name == head {
if len(tail) == 0 {
(*v)[i].Value = inputVar.Value
} else {
setVariable((*v)[i].Value, tail, inputVar.Value)
}
continue
}
}
newVariables = append(newVariables, inputVar)
}
*v = append(*v, newVariables...)
}
func setVariable(v interface{}, path []string, value interface{}) {
head := path[0]
tail := path[1:]
hasTail := len(tail) != 0
switch t := v.(type) {
case map[string]interface{}:
if hasTail {
nextValue, ok := t[head]
if !ok {
return
}
setVariable(nextValue, tail, value)
} else {
t[head] = value
}
case map[interface{}]interface{}:
if hasTail {
nextValue, ok := t[head]
if !ok {
return
}
setVariable(nextValue, tail, value)
} else {
t[head] = value
}
case []interface{}:
index, err := strconv.Atoi(head)
if err != nil {
return
}
if index >= len(t) || index < 0 {
return
}
if hasTail {
setVariable(t[index], tail, value)
} else {
t[index] = value
}
default:
}
}
func lookupVariable(variables interface{}, name string) (interface{}, bool) {
path := strings.Split(name, ".") path := strings.Split(name, ".")
var v interface{} = variables var v interface{} = variables
......
package runner package runner
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
...@@ -115,3 +116,44 @@ ...@@ -115,3 +116,44 @@
}) })
} }
} }
func TestSetVariable(t *testing.T) {
type V = map[string]interface{}
variables := func(setters ...func(V)) V {
ret := V{
"string": "a string",
"int": 3,
"map": map[interface{}]interface{}{
"float": 12.3,
},
"list": []interface{}{
map[interface{}]interface{}{
"float": 12.3,
},
},
}
for _, s := range setters {
s(ret)
}
return ret
}
for _, tt := range []struct {
name string
value interface{}
expected interface{}
}{
{"string", "new string", variables(func(v V) { v["string"] = "new string" })},
{"int", "not an int anymore", variables(func(v V) { v["int"] = "not an int anymore" })},
{"map.float", 13.0, variables(func(v V) { v["map"].(map[interface{}]interface{})["float"] = 13.0 })},
{"list.0.float", 15.0, variables(func(v V) { v["list"].([]interface{})[0].(map[interface{}]interface{})["float"] = 15.0 })},
{"list.2.float", nil, variables()},
{"list.-2.float", nil, variables()},
} {
t.Run(tt.name, func(t *testing.T) {
v := variables()
setVariable(v, strings.Split(tt.name, "."), tt.value)
assert.Equal(t, tt.expected, v)
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment