Skip to content

Commit b27a0cd

Browse files
authored
feat: Add support for multiple init-script (#101)
- implement an array type for `init-script` with the `run` keyword - maintain backward compatibility for `init-script` as a single string - upgrade Go YAML version from v2 to v3 - corrected typos in comments
1 parent 969a4eb commit b27a0cd

File tree

9 files changed

+177
-32
lines changed

9 files changed

+177
-32
lines changed

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/onsi/gomega v1.30.0
1010
github.com/spf13/cobra v1.8.0
1111
github.com/stretchr/testify v1.8.4
12-
gopkg.in/yaml.v2 v2.4.0
12+
gopkg.in/yaml.v3 v3.0.1
1313
)
1414

1515
require (
@@ -30,5 +30,4 @@ require (
3030
golang.org/x/text v0.13.0 // indirect
3131
golang.org/x/tools v0.14.0 // indirect
3232
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
33-
gopkg.in/yaml.v3 v3.0.1 // indirect
3433
)

go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
6969
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7070
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
7171
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
72-
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
73-
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
7472
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
7573
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
7674
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/config/config_file.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"sync"
88

99
"github.com/sunggun-yu/envp/internal/util"
10-
"gopkg.in/yaml.v2"
10+
"gopkg.in/yaml.v3"
1111
)
1212

1313
var (
@@ -59,7 +59,7 @@ func (c *ConfigFile) initConfigFile() error {
5959
c.mu.Lock()
6060
defer c.mu.Unlock()
6161

62-
// return error if config file name is not seet
62+
// return error if config file name is not set
6363
if c.name == "" {
6464
return fmt.Errorf("Config file is not set")
6565
}
@@ -91,7 +91,7 @@ func (c *ConfigFile) Read() (*Config, error) {
9191
if err := yaml.Unmarshal(b, &c.config); err != nil {
9292
return nil, err
9393
}
94-
// set mutex to Config to syncronize object along with file operation
94+
// set mutex to Config to synchronize object along with file operation
9595
c.config.SetMutex(&c.mu)
9696
return c.config, nil
9797
}

internal/config/config_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"testing"
77

88
"github.com/sunggun-yu/envp/internal/config"
9-
"gopkg.in/yaml.v2"
9+
"gopkg.in/yaml.v3"
1010
)
1111

1212
var (

internal/config/profile.go

+41-8
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ type Profiles map[string]*Profile
1212
// Profile is struct of profile
1313
// TODO: linked list might be better. but unmarshal may not be supported(need test). rebuilding structure after reading the config may required.
1414
type Profile struct {
15-
// set it with mapstructure remain to unmashal config file item `profiles` as Profile
15+
// set it with mapstructure remain to unmarshal config file item `profiles` as Profile
1616
// yaml inline fixed the nested profiles issue
17-
Profiles Profiles `mapstructure:",remain" yaml:",inline"`
18-
Desc string `mapstructure:"desc" yaml:"desc,omitempty"`
19-
Env Envs `mapstructure:"env" yaml:"env,omitempty"`
20-
InitScript string `mapstructure:"init-script" yaml:"init-script,omitempty"`
17+
Profiles Profiles `mapstructure:",remain" yaml:",inline"`
18+
Desc string `mapstructure:"desc" yaml:"desc,omitempty"`
19+
Env Envs `mapstructure:"env" yaml:"env,omitempty"`
20+
InitScript interface{} `mapstructure:"init-script" yaml:"init-script,omitempty"`
2121
}
2222

2323
// NewProfile creates the Profile
@@ -65,8 +65,8 @@ func NewProfileNameInputEmptyError() *ProfileNameInputEmptyError {
6565
}
6666

6767
// SetProfile sets profile into the Profiles
68-
// key is dot "." delimetered or plain string without no space.
69-
// if it is dot delimeterd, considering it as nested profile
68+
// key is dot "." delimited or plain string without no space.
69+
// if it is dot delimited, considering it as nested profile
7070
func (p *Profiles) SetProfile(key string, profile Profile) error {
7171
if key == "" {
7272
return NewProfileNameInputEmptyError()
@@ -172,6 +172,39 @@ func (p *Profiles) DeleteProfile(key string) error {
172172
return nil
173173
}
174174

175+
// InitScripts returns an array of strings representing initialization scripts.
176+
// The `init-script` parameter can be either a string or an array of maps with the key `run`.
177+
// This function processes the input and returns an array of strings containing the extracted 'run' values from the provided maps,
178+
// or the original string if it's not an array of maps.
179+
func (p *Profile) InitScripts() []string {
180+
// Return early if profile or init-script is empty
181+
if p.InitScript == nil {
182+
return nil
183+
}
184+
185+
var initScripts []string
186+
187+
switch scripts := p.InitScript.(type) {
188+
case string:
189+
initScripts = append(initScripts, scripts)
190+
case []interface{}:
191+
for _, script := range scripts {
192+
if m, ok := script.(map[string]interface{}); ok {
193+
if runScript, exist := m["run"]; exist {
194+
initScripts = append(initScripts, fmt.Sprintf("%v", runScript))
195+
}
196+
}
197+
}
198+
}
199+
200+
// return if initScripts is empty
201+
if len(initScripts) == 0 {
202+
return nil
203+
}
204+
205+
return initScripts
206+
}
207+
175208
// FindProfileByDotNotationKey finds profile from dot notation of key such as "a.b.c"
176209
// keys is array of string that in-order by nested profile. finding parent profile will be possible by keys[:len(keys)-1]
177210
func findProfileByDotNotationKey(keys []string, profiles *Profiles) *Profile {
@@ -188,7 +221,7 @@ func findProfileByDotNotationKey(keys []string, profiles *Profiles) *Profile {
188221
return profile
189222
}
190223

191-
// list all the profiles in dot "." format. e.g. mygroup.my-subgroup.my-profile
224+
// list all the profiles in dot "." format. e.g. my-group.my-subgroup.my-profile
192225
// Do DFS to build viper keys for profiles
193226
func listProfileKeys(key string, profiles Profiles, arr *[]string) *[]string {
194227
for k, v := range profiles {

internal/config/profile_test.go

+57-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66
"strings"
77
"testing"
88

9+
"github.com/stretchr/testify/assert"
910
"github.com/sunggun-yu/envp/internal/config"
10-
"gopkg.in/yaml.v2"
11+
"gopkg.in/yaml.v3"
1112
)
1213

1314
var (
@@ -86,6 +87,11 @@ func TestProfileNames(t *testing.T) {
8687
"org.nprod.vpn.vpn1",
8788
"org.nprod.vpn.vpn2",
8889
"parent-has-env",
90+
"profile-with-init-script",
91+
"profile-with-multi-init-script",
92+
"profile-with-multi-init-script-but-no-run",
93+
"profile-with-no-init-script",
94+
"profile-with-single-init-script-but-array",
8995
}
9096

9197
actual := profiles.ProfileNames()
@@ -111,8 +117,8 @@ func TestFindParentProfile(t *testing.T) {
111117
testCaseNormal("lab.cluster1", "lab")
112118
})
113119

114-
t.Run("find exisiting parent of non-existing child profile", func(t *testing.T) {
115-
// should return parent even child is not exisiting
120+
t.Run("find existing parent of non-existing child profile", func(t *testing.T) {
121+
// should return parent even child is not existing
116122
testCaseNormal("lab.cluster-not-existing-in-config", "lab")
117123
})
118124

@@ -123,7 +129,7 @@ func TestFindParentProfile(t *testing.T) {
123129
}
124130
})
125131

126-
t.Run("find non-exisiting parent of non-existing child profile", func(t *testing.T) {
132+
t.Run("find non-existing parent of non-existing child profile", func(t *testing.T) {
127133
// should return nil for non existing profile
128134
if p, err := profiles.FindParentProfile("non-existing-parent.non-existing-child"); p != nil && err == nil {
129135
t.Error("supposed to be nil and err")
@@ -181,7 +187,7 @@ func TestDeleteProfile(t *testing.T) {
181187
testCase("org.nprod.argocd.argo2")
182188
})
183189

184-
t.Run("delete non-exisiting nested profile", func(t *testing.T) {
190+
t.Run("delete non-existing nested profile", func(t *testing.T) {
185191
testCaseNonExistingProfile("non-existing-parent.non-existing-child")
186192
})
187193
}
@@ -267,3 +273,49 @@ func TestSetProfile(t *testing.T) {
267273
}
268274
})
269275
}
276+
277+
func TestProfileInitScript(t *testing.T) {
278+
279+
cfg := testDataConfig
280+
profile := cfg().Profiles
281+
282+
t.Run("profile with no init-script", func(t *testing.T) {
283+
p, err := profile.FindProfile("profile-with-no-init-script")
284+
assert.NoError(t, err, "error should not occurred")
285+
assert.NotEmpty(t, p, "profile is found")
286+
expect := 0
287+
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
288+
})
289+
290+
t.Run("profile with single init-script", func(t *testing.T) {
291+
p, err := profile.FindProfile("profile-with-init-script")
292+
assert.NoError(t, err, "error should not occurred")
293+
assert.NotEmpty(t, p, "profile is found")
294+
expect := 1
295+
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
296+
})
297+
298+
t.Run("profile with single init-script but array type", func(t *testing.T) {
299+
p, err := profile.FindProfile("profile-with-single-init-script-but-array")
300+
assert.NoError(t, err, "error should not occurred")
301+
assert.NotEmpty(t, p, "profile is found")
302+
expect := 1
303+
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
304+
})
305+
306+
t.Run("profile with multiple init-script", func(t *testing.T) {
307+
p, err := profile.FindProfile("profile-with-multi-init-script")
308+
assert.NoError(t, err, "error should not occurred")
309+
assert.NotEmpty(t, p, "profile is found")
310+
expect := 2
311+
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
312+
})
313+
314+
t.Run("profile with multiple init-script but has no map of run keyword", func(t *testing.T) {
315+
p, err := profile.FindProfile("profile-with-multi-init-script-but-no-run")
316+
assert.NoError(t, err, "error should not occurred")
317+
assert.NotEmpty(t, p, "profile is found")
318+
expect := 0
319+
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
320+
})
321+
}

internal/shell/shell.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,18 @@ func (s *ShellCommand) execCommand(argv0 string, argv []string, profile *config.
103103

104104
// executeInitScript executes the initial script for the shell
105105
func (s *ShellCommand) executeInitScript(profile *config.NamedProfile) error {
106-
107-
// just return if init-script is empty
108-
if profile == nil || len(profile.InitScript) == 0 {
106+
// Return if profile or init-script is empty
107+
if profile == nil || profile.InitScript == nil {
109108
return nil
110109
}
111110

112-
cmd := s.createCommand(&profile.Env, "/bin/sh", "-c", profile.InitScript)
113-
114-
err := cmd.Run()
115-
if err != nil {
116-
return fmt.Errorf("init-script error: %w", err)
111+
// loop and run init script in order
112+
for _, initScript := range profile.InitScripts() {
113+
cmd := s.createCommand(&profile.Env, "/bin/sh", "-c", initScript)
114+
err := cmd.Run()
115+
if err != nil {
116+
return fmt.Errorf("init-script error: %w", err)
117+
}
117118
}
118119
return nil
119120
}

internal/shell/shell_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,34 @@ var _ = Describe("init-script", func() {
362362
})
363363
})
364364
})
365+
366+
var _ = Describe("multiple init-script", func() {
367+
var stdout, stderr bytes.Buffer
368+
sc := NewShellCommand()
369+
sc.Stdout = &stdout
370+
sc.Stderr = &stderr
371+
372+
When("multiple init-script is defined", func() {
373+
profile := config.NamedProfile{
374+
Name: "my-profile",
375+
Profile: config.NewProfile(),
376+
}
377+
378+
var initScripts []interface{}
379+
initScripts = append(initScripts, map[string]interface{}{"run": "echo meow-1"})
380+
initScripts = append(initScripts, map[string]interface{}{"run": "echo meow-2"})
381+
initScripts = append(initScripts, map[string]interface{}{"something-else": "echo meow-2"})
382+
383+
profile.InitScript = initScripts
384+
385+
err := sc.executeInitScript(&profile)
386+
387+
It("should not error", func() {
388+
Expect(err).NotTo(HaveOccurred())
389+
})
390+
391+
It("output should only have result of run(s)", func() {
392+
Expect(stdout.String()).To(Equal("meow-1\nmeow-2\n"))
393+
})
394+
})
395+
})

testdata/config.yaml

+34-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ profiles:
88
- name: HTTPS_PROXY
99
value: http://192.168.1.10:443
1010
- name: NO_PROXY
11-
value: localhost,127.0.0.1,.someapis.local
11+
value: localhost,127.0.0.1,.some_apis.local
1212
- name: KUBECONFIG
1313
value: /Users/meow/.kube/lab-cluster1
1414
cluster2:
@@ -17,7 +17,7 @@ profiles:
1717
- name: HTTPS_PROXY
1818
value: http://192.168.1.20:443
1919
- name: NO_PROXY
20-
value: localhost,127.0.0.1,.someapis.local
20+
value: localhost,127.0.0.1,.some_apis.local
2121
- name: KUBECONFIG
2222
value: /Users/meow/.kube/lab-cluster2
2323
cluster3:
@@ -26,7 +26,7 @@ profiles:
2626
- name: HTTPS_PROXY
2727
value: http://192.168.1.30:443
2828
- name: NO_PROXY
29-
value: localhost,127.0.0.1,.someapis.local
29+
value: localhost,127.0.0.1,.some_apis.local
3030
- name: KUBECONFIG
3131
value: /Users/meow/.kube/lab-cluster3
3232
docker:
@@ -66,3 +66,34 @@ profiles:
6666
env:
6767
- name: HTTPS_PROXY
6868
value: http://192.168.2.11:3128
69+
profile-with-init-script:
70+
env:
71+
- name: VAR
72+
value: VAL
73+
init-script: echo meow
74+
profile-with-multi-init-script:
75+
env:
76+
- name: VAR
77+
value: VAL
78+
init-script:
79+
- run: echo meow1
80+
- run: echo meow2
81+
- something-else: echo meow2
82+
profile-with-multi-init-script-but-no-run:
83+
env:
84+
- name: VAR
85+
value: VAL
86+
init-script:
87+
- something-else: echo meow1
88+
- something-else: echo meow2
89+
- something-else: echo meow2
90+
profile-with-no-init-script:
91+
env:
92+
- name: VAR
93+
value: VAL
94+
profile-with-single-init-script-but-array:
95+
env:
96+
- name: VAR
97+
value: VAL
98+
init-script:
99+
- run: echo meow

0 commit comments

Comments
 (0)