Skip to content

Commit

Permalink
feat: Add support for multiple init-script (#101)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
sunggun-yu authored Nov 22, 2023
1 parent 969a4eb commit b27a0cd
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 32 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/onsi/gomega v1.30.0
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand All @@ -30,5 +30,4 @@ require (
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
6 changes: 3 additions & 3 deletions internal/config/config_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"sync"

"github.com/sunggun-yu/envp/internal/util"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)

var (
Expand Down Expand Up @@ -59,7 +59,7 @@ func (c *ConfigFile) initConfigFile() error {
c.mu.Lock()
defer c.mu.Unlock()

// return error if config file name is not seet
// return error if config file name is not set
if c.name == "" {
return fmt.Errorf("Config file is not set")
}
Expand Down Expand Up @@ -91,7 +91,7 @@ func (c *ConfigFile) Read() (*Config, error) {
if err := yaml.Unmarshal(b, &c.config); err != nil {
return nil, err
}
// set mutex to Config to syncronize object along with file operation
// set mutex to Config to synchronize object along with file operation
c.config.SetMutex(&c.mu)
return c.config, nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"

"github.com/sunggun-yu/envp/internal/config"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)

var (
Expand Down
49 changes: 41 additions & 8 deletions internal/config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ type Profiles map[string]*Profile
// Profile is struct of profile
// TODO: linked list might be better. but unmarshal may not be supported(need test). rebuilding structure after reading the config may required.
type Profile struct {
// set it with mapstructure remain to unmashal config file item `profiles` as Profile
// set it with mapstructure remain to unmarshal config file item `profiles` as Profile
// yaml inline fixed the nested profiles issue
Profiles Profiles `mapstructure:",remain" yaml:",inline"`
Desc string `mapstructure:"desc" yaml:"desc,omitempty"`
Env Envs `mapstructure:"env" yaml:"env,omitempty"`
InitScript string `mapstructure:"init-script" yaml:"init-script,omitempty"`
Profiles Profiles `mapstructure:",remain" yaml:",inline"`
Desc string `mapstructure:"desc" yaml:"desc,omitempty"`
Env Envs `mapstructure:"env" yaml:"env,omitempty"`
InitScript interface{} `mapstructure:"init-script" yaml:"init-script,omitempty"`
}

// NewProfile creates the Profile
Expand Down Expand Up @@ -65,8 +65,8 @@ func NewProfileNameInputEmptyError() *ProfileNameInputEmptyError {
}

// SetProfile sets profile into the Profiles
// key is dot "." delimetered or plain string without no space.
// if it is dot delimeterd, considering it as nested profile
// key is dot "." delimited or plain string without no space.
// if it is dot delimited, considering it as nested profile
func (p *Profiles) SetProfile(key string, profile Profile) error {
if key == "" {
return NewProfileNameInputEmptyError()
Expand Down Expand Up @@ -172,6 +172,39 @@ func (p *Profiles) DeleteProfile(key string) error {
return nil
}

// InitScripts returns an array of strings representing initialization scripts.
// The `init-script` parameter can be either a string or an array of maps with the key `run`.
// This function processes the input and returns an array of strings containing the extracted 'run' values from the provided maps,
// or the original string if it's not an array of maps.
func (p *Profile) InitScripts() []string {
// Return early if profile or init-script is empty
if p.InitScript == nil {
return nil
}

var initScripts []string

switch scripts := p.InitScript.(type) {
case string:
initScripts = append(initScripts, scripts)
case []interface{}:
for _, script := range scripts {
if m, ok := script.(map[string]interface{}); ok {
if runScript, exist := m["run"]; exist {
initScripts = append(initScripts, fmt.Sprintf("%v", runScript))
}
}
}
}

// return if initScripts is empty
if len(initScripts) == 0 {
return nil
}

return initScripts
}

// FindProfileByDotNotationKey finds profile from dot notation of key such as "a.b.c"
// keys is array of string that in-order by nested profile. finding parent profile will be possible by keys[:len(keys)-1]
func findProfileByDotNotationKey(keys []string, profiles *Profiles) *Profile {
Expand All @@ -188,7 +221,7 @@ func findProfileByDotNotationKey(keys []string, profiles *Profiles) *Profile {
return profile
}

// list all the profiles in dot "." format. e.g. mygroup.my-subgroup.my-profile
// list all the profiles in dot "." format. e.g. my-group.my-subgroup.my-profile
// Do DFS to build viper keys for profiles
func listProfileKeys(key string, profiles Profiles, arr *[]string) *[]string {
for k, v := range profiles {
Expand Down
62 changes: 57 additions & 5 deletions internal/config/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/sunggun-yu/envp/internal/config"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)

var (
Expand Down Expand Up @@ -86,6 +87,11 @@ func TestProfileNames(t *testing.T) {
"org.nprod.vpn.vpn1",
"org.nprod.vpn.vpn2",
"parent-has-env",
"profile-with-init-script",
"profile-with-multi-init-script",
"profile-with-multi-init-script-but-no-run",
"profile-with-no-init-script",
"profile-with-single-init-script-but-array",
}

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

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

Expand All @@ -123,7 +129,7 @@ func TestFindParentProfile(t *testing.T) {
}
})

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

t.Run("delete non-exisiting nested profile", func(t *testing.T) {
t.Run("delete non-existing nested profile", func(t *testing.T) {
testCaseNonExistingProfile("non-existing-parent.non-existing-child")
})
}
Expand Down Expand Up @@ -267,3 +273,49 @@ func TestSetProfile(t *testing.T) {
}
})
}

func TestProfileInitScript(t *testing.T) {

cfg := testDataConfig
profile := cfg().Profiles

t.Run("profile with no init-script", func(t *testing.T) {
p, err := profile.FindProfile("profile-with-no-init-script")
assert.NoError(t, err, "error should not occurred")
assert.NotEmpty(t, p, "profile is found")
expect := 0
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
})

t.Run("profile with single init-script", func(t *testing.T) {
p, err := profile.FindProfile("profile-with-init-script")
assert.NoError(t, err, "error should not occurred")
assert.NotEmpty(t, p, "profile is found")
expect := 1
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
})

t.Run("profile with single init-script but array type", func(t *testing.T) {
p, err := profile.FindProfile("profile-with-single-init-script-but-array")
assert.NoError(t, err, "error should not occurred")
assert.NotEmpty(t, p, "profile is found")
expect := 1
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
})

t.Run("profile with multiple init-script", func(t *testing.T) {
p, err := profile.FindProfile("profile-with-multi-init-script")
assert.NoError(t, err, "error should not occurred")
assert.NotEmpty(t, p, "profile is found")
expect := 2
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
})

t.Run("profile with multiple init-script but has no map of run keyword", func(t *testing.T) {
p, err := profile.FindProfile("profile-with-multi-init-script-but-no-run")
assert.NoError(t, err, "error should not occurred")
assert.NotEmpty(t, p, "profile is found")
expect := 0
assert.Len(t, p.InitScripts(), expect, fmt.Sprintf("should be %v init-script", expect))
})
}
17 changes: 9 additions & 8 deletions internal/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,18 @@ func (s *ShellCommand) execCommand(argv0 string, argv []string, profile *config.

// executeInitScript executes the initial script for the shell
func (s *ShellCommand) executeInitScript(profile *config.NamedProfile) error {

// just return if init-script is empty
if profile == nil || len(profile.InitScript) == 0 {
// Return if profile or init-script is empty
if profile == nil || profile.InitScript == nil {
return nil
}

cmd := s.createCommand(&profile.Env, "/bin/sh", "-c", profile.InitScript)

err := cmd.Run()
if err != nil {
return fmt.Errorf("init-script error: %w", err)
// loop and run init script in order
for _, initScript := range profile.InitScripts() {
cmd := s.createCommand(&profile.Env, "/bin/sh", "-c", initScript)
err := cmd.Run()
if err != nil {
return fmt.Errorf("init-script error: %w", err)
}
}
return nil
}
Expand Down
31 changes: 31 additions & 0 deletions internal/shell/shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,34 @@ var _ = Describe("init-script", func() {
})
})
})

var _ = Describe("multiple init-script", func() {
var stdout, stderr bytes.Buffer
sc := NewShellCommand()
sc.Stdout = &stdout
sc.Stderr = &stderr

When("multiple init-script is defined", func() {
profile := config.NamedProfile{
Name: "my-profile",
Profile: config.NewProfile(),
}

var initScripts []interface{}
initScripts = append(initScripts, map[string]interface{}{"run": "echo meow-1"})
initScripts = append(initScripts, map[string]interface{}{"run": "echo meow-2"})
initScripts = append(initScripts, map[string]interface{}{"something-else": "echo meow-2"})

profile.InitScript = initScripts

err := sc.executeInitScript(&profile)

It("should not error", func() {
Expect(err).NotTo(HaveOccurred())
})

It("output should only have result of run(s)", func() {
Expect(stdout.String()).To(Equal("meow-1\nmeow-2\n"))
})
})
})
37 changes: 34 additions & 3 deletions testdata/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ profiles:
- name: HTTPS_PROXY
value: http://192.168.1.10:443
- name: NO_PROXY
value: localhost,127.0.0.1,.someapis.local
value: localhost,127.0.0.1,.some_apis.local
- name: KUBECONFIG
value: /Users/meow/.kube/lab-cluster1
cluster2:
Expand All @@ -17,7 +17,7 @@ profiles:
- name: HTTPS_PROXY
value: http://192.168.1.20:443
- name: NO_PROXY
value: localhost,127.0.0.1,.someapis.local
value: localhost,127.0.0.1,.some_apis.local
- name: KUBECONFIG
value: /Users/meow/.kube/lab-cluster2
cluster3:
Expand All @@ -26,7 +26,7 @@ profiles:
- name: HTTPS_PROXY
value: http://192.168.1.30:443
- name: NO_PROXY
value: localhost,127.0.0.1,.someapis.local
value: localhost,127.0.0.1,.some_apis.local
- name: KUBECONFIG
value: /Users/meow/.kube/lab-cluster3
docker:
Expand Down Expand Up @@ -66,3 +66,34 @@ profiles:
env:
- name: HTTPS_PROXY
value: http://192.168.2.11:3128
profile-with-init-script:
env:
- name: VAR
value: VAL
init-script: echo meow
profile-with-multi-init-script:
env:
- name: VAR
value: VAL
init-script:
- run: echo meow1
- run: echo meow2
- something-else: echo meow2
profile-with-multi-init-script-but-no-run:
env:
- name: VAR
value: VAL
init-script:
- something-else: echo meow1
- something-else: echo meow2
- something-else: echo meow2
profile-with-no-init-script:
env:
- name: VAR
value: VAL
profile-with-single-init-script-but-array:
env:
- name: VAR
value: VAL
init-script:
- run: echo meow

0 comments on commit b27a0cd

Please sign in to comment.