Skip to content

Commit ded1004

Browse files
aFlyBird0steinliber
authored andcommitted
feat: support nested vars
Signed-off-by: Bird <[email protected]>
1 parent 433874f commit ded1004

File tree

6 files changed

+263
-7
lines changed

6 files changed

+263
-7
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ require (
171171
github.com/modern-go/reflect2 v1.0.2 // indirect
172172
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
173173
github.com/morikuni/aec v1.0.0 // indirect
174+
github.com/nxadm/tail v1.4.8 // indirect
174175
github.com/opencontainers/go-digest v1.0.0 // indirect
175176
github.com/opencontainers/image-spec v1.0.1 // indirect
176177
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
@@ -222,6 +223,7 @@ require (
222223
gopkg.in/gorp.v1 v1.7.2 // indirect
223224
gopkg.in/inf.v0 v0.9.1 // indirect
224225
gopkg.in/ini.v1 v1.62.0 // indirect
226+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
225227
gopkg.in/warnings.v0 v0.1.2 // indirect
226228
gopkg.in/yaml.v2 v2.4.0 // indirect
227229
k8s.io/apiextensions-apiserver v0.22.4 // indirect

internal/pkg/configmanager/configmanager_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ config:
2222
vars:
2323
foo1: bar1
2424
foo2: 123
25+
foo3: foo1+foo2=[[foo1]][[foo2 ]]!
2526
appName: service-a
2627
registryType: dockerhub
2728
argocdNamespace: argocd
@@ -68,6 +69,7 @@ tools:
6869
dependsOn: []
6970
options:
7071
foo1: [[ foo1 ]]
72+
foo3: [[ foo3 ]]
7173
- name: plugin2
7274
instanceID: tluafed
7375
dependsOn: []
@@ -106,6 +108,7 @@ pipelineTemplates:
106108
DependsOn: []string{},
107109
Options: RawOptions{
108110
"foo1": "bar1",
111+
"foo3": "foo1+foo2=bar1123!",
109112
"instanceID": "default",
110113
},
111114
}
@@ -233,8 +236,10 @@ pipelineTemplates:
233236
}))
234237

235238
// vars
236-
Expect(len(cfg.Vars)).To(Equal(5))
239+
Expect(len(cfg.Vars)).To(Equal(6))
237240
Expect(cfg.Vars["foo1"]).To(Equal("bar1"))
241+
Expect(cfg.Vars["foo2"]).To(Equal(123))
242+
Expect(cfg.Vars["foo3"]).To(Equal("foo1+foo2=bar1123!"))
238243

239244
// tools
240245
Expect(len(cfg.Tools)).To(Equal(5))

internal/pkg/configmanager/rawconfig.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import (
44
"bytes"
55
"fmt"
66
"regexp"
7+
"strings"
78
"unicode"
89

910
"gopkg.in/yaml.v3"
1011
)
1112

12-
// rawConfig respent every valid config block for devstream
13+
// rawConfig represent every valid config block for devstream
1314
type rawConfig struct {
1415
apps []byte
1516
pipelineTemplates []byte
@@ -77,9 +78,40 @@ func (c *rawConfig) validate() error {
7778

7879
// getVars will generate variables from vars config
7980
func (c *rawConfig) getVars() (map[string]any, error) {
80-
var globalVars map[string]any
81-
err := yaml.Unmarshal(c.vars, &globalVars)
82-
return globalVars, err
81+
var (
82+
globalVarsOrigin, globalVarsParsedNest map[string]any
83+
err error
84+
)
85+
86+
// [[]] is array in yaml, replace them, or yaml.Unmarshal will return err
87+
const (
88+
leftOrigin, leftReplaced = "[[", "【$【"
89+
rightOrigin, rightReplaced = "]]", "】$】"
90+
)
91+
converter := strings.NewReplacer(leftOrigin, leftReplaced, rightOrigin, rightReplaced)
92+
varsReplacedSquareBrackets := converter.Replace(string(c.vars))
93+
94+
// get origin vars from config
95+
if err = yaml.Unmarshal([]byte(varsReplacedSquareBrackets), &globalVarsOrigin); err != nil {
96+
return nil, err
97+
}
98+
99+
// restore vars
100+
restorer := strings.NewReplacer(leftReplaced, leftOrigin, rightReplaced, rightOrigin)
101+
for k, v := range globalVarsOrigin {
102+
switch value := v.(type) {
103+
case string:
104+
globalVarsOrigin[k] = restorer.Replace(value)
105+
case []byte:
106+
globalVarsOrigin[k] = restorer.Replace(string(value))
107+
}
108+
}
109+
110+
// parse nested vars
111+
if globalVarsParsedNest, err = parseNestedVars(globalVarsOrigin); err != nil {
112+
return nil, err
113+
}
114+
return globalVarsParsedNest, nil
83115
}
84116

85117
// getToolsWithVars will generate Tools from tools config

internal/pkg/configmanager/rawconfig_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,18 @@ var _ = Describe("rawConfig struct", func() {
221221
vars: []byte(`---
222222
foo1: bar1
223223
foo2: 123
224-
foo3: bar3`)}
224+
foo3: [[foo1]]+[[ foo2]]!
225+
foo4: [[foo1]]+[[ foo2]]+sep+[[ foo3 ]]!`)}
225226
})
226227
It("should works fine", func() {
227228
varMap, err := r.getVars()
228229
Expect(err).NotTo(HaveOccurred())
229230
Expect(varMap).NotTo(BeNil())
230-
Expect(len(varMap)).To(Equal(3))
231+
Expect(len(varMap)).To(Equal(4))
231232
Expect(varMap["foo1"]).To(Equal(interface{}("bar1")))
232233
Expect(varMap["foo2"]).To(Equal(interface{}(123)))
234+
Expect(varMap["foo3"]).To(Equal("bar1+123!"))
235+
Expect(varMap["foo4"]).To(Equal("bar1+123+sep+bar1+123!!"))
233236
})
234237
})
235238
})

internal/pkg/configmanager/var.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package configmanager
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/devstream-io/devstream/pkg/util/template"
8+
)
9+
10+
func parseNestedVars(origin map[string]any) (map[string]any, error) {
11+
unparsed := make(map[string]any, len(origin))
12+
for k, v := range origin {
13+
unparsed[k] = v
14+
}
15+
16+
parsed := make(map[string]any, len(origin))
17+
updated := true // if any vars were updated in one loop
18+
19+
// loop until:
20+
// 1. all vars have been parsed
21+
// 2. or can not render more vars by existing parsed vars
22+
for len(unparsed) > 0 && updated {
23+
updated = false
24+
// use "parsed map" to parse each <key, value> in "unparsed map".
25+
// once one value doesn't contain other keys, put it into "parsed map".
26+
27+
// a util func which implements the "put" steps
28+
putParsedValue := func(k string, v any) {
29+
parsed[k] = v
30+
delete(unparsed, k)
31+
updated = true
32+
}
33+
34+
for k, v := range unparsed {
35+
// if value is not string or doesn't contain var, just put
36+
vString, ok := v.(string)
37+
if !ok || !ifContainVar(vString) {
38+
putParsedValue(k, v)
39+
continue
40+
}
41+
42+
// parse one value with "parsed map"
43+
valueParsed, err := template.NewRenderClient(&template.TemplateOption{},
44+
template.ContentGetter, template.AddDotForVariablesInConfigProcessor).
45+
Render(fmt.Sprintf("%v", v), parsed)
46+
// if no error(means this value doesn't import vars in "unparsed map"), put
47+
if err == nil {
48+
putParsedValue(k, valueParsed)
49+
}
50+
}
51+
}
52+
53+
// check if vars map is correct
54+
if len(unparsed) > 0 {
55+
errString := "failed to parse var "
56+
var errKeyValues []string
57+
for k := range unparsed {
58+
errKeyValues = append(errKeyValues, fmt.Sprintf(`<"%s": "%s">`, k, origin[k]))
59+
}
60+
errString += strings.Join(errKeyValues, ", ")
61+
return nil, fmt.Errorf(errString)
62+
}
63+
64+
return parsed, nil
65+
66+
}
67+
68+
// check if value contains other vars
69+
func ifContainVar(value string) bool {
70+
// this function is humble, some case could not be checked probably
71+
// e.g. "[[123]]" "[[]]" will be regard as a var
72+
if strings.Contains(value, "[[") && strings.Contains(value, "]]") {
73+
return true
74+
}
75+
return false
76+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package configmanager
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = Describe("parseNestedVars", func() {
9+
var (
10+
origin, expected, parsed map[string]any
11+
err error
12+
)
13+
14+
JustBeforeEach(func() {
15+
parsed, err = parseNestedVars(origin)
16+
})
17+
18+
When("vars map is correct", func() {
19+
When("case is simple(no nested)", func() {
20+
BeforeEach(func() {
21+
origin = map[string]any{
22+
"a": "a",
23+
"b": 123,
24+
}
25+
expected = origin
26+
})
27+
It("should parse succeed", func() {
28+
Expect(err).Should(Succeed())
29+
Expect(parsed).Should(Equal(expected))
30+
})
31+
})
32+
When("case is complex(nested once)", func() {
33+
BeforeEach(func() {
34+
origin = map[string]any{
35+
"a": "[[ b]]a",
36+
"b": "123",
37+
}
38+
expected = map[string]any{
39+
"a": "123a",
40+
"b": "123",
41+
}
42+
})
43+
It("should parse succeed", func() {
44+
Expect(err).Should(Succeed())
45+
Expect(parsed).Should(Equal(expected))
46+
})
47+
})
48+
When("case is complex(nested many times)", func() {
49+
BeforeEach(func() {
50+
origin = map[string]any{
51+
"a": "[[ b]]a",
52+
"b": 123,
53+
"c": "[[a]]c",
54+
"d": "[[a]]/[[ c ]]/[[b]]",
55+
}
56+
expected = map[string]any{
57+
"a": "123a",
58+
"b": 123,
59+
"c": "123ac",
60+
"d": "123a/123ac/123",
61+
}
62+
})
63+
64+
It("should parse succeed", func() {
65+
Expect(err).Should(Succeed())
66+
Expect(parsed).Should(Equal(expected))
67+
})
68+
})
69+
})
70+
71+
When("vars map is incorrect", func() {
72+
BeforeEach(func() {
73+
origin = map[string]any{
74+
"a": "[[ b]]a",
75+
"b": "123",
76+
"c": "[[a]]c[[c]]",
77+
"d": "[[a]]/[[ c ]]/[[b]]",
78+
}
79+
expected = map[string]any{
80+
"a": "123a",
81+
"b": "123",
82+
"c": "123ac",
83+
"d": "123a/123ac/123",
84+
}
85+
})
86+
87+
It("should return error", func() {
88+
Expect(err).Should(HaveOccurred())
89+
})
90+
})
91+
})
92+
93+
var _ = Describe("ifContainVar", func() {
94+
var (
95+
value string
96+
checkResult bool
97+
)
98+
99+
JustBeforeEach(func() {
100+
checkResult = ifContainVar(value)
101+
})
102+
103+
When("contains var", func() {
104+
AfterEach(func() {
105+
Expect(checkResult).To(Equal(true))
106+
})
107+
Context("case 1", func() {
108+
BeforeEach(func() {
109+
value = " [[a]]"
110+
})
111+
It("should check correctly", func() {})
112+
})
113+
Context("case 2", func() {
114+
BeforeEach(func() {
115+
value = "[[ b ]]"
116+
})
117+
It("should check correctly", func() {})
118+
})
119+
})
120+
121+
When("doesn't contain var", func() {
122+
AfterEach(func() {
123+
Expect(checkResult).To(Equal(false))
124+
})
125+
Context("case 1", func() {
126+
BeforeEach(func() {
127+
value = " [[a] ]"
128+
})
129+
It("should check correctly", func() {})
130+
})
131+
Context("case 2", func() {
132+
BeforeEach(func() {
133+
value = " [ b ]]"
134+
})
135+
It("should check correctly", func() {})
136+
})
137+
})
138+
})

0 commit comments

Comments
 (0)