diff --git a/go.mod b/go.mod index dae0071..8dc9500 100644 --- a/go.mod +++ b/go.mod @@ -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 ( @@ -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 ) diff --git a/go.sum b/go.sum index 4c45369..c0db61b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/config/config_file.go b/internal/config/config_file.go index 7cdd1f9..15b3943 100644 --- a/internal/config/config_file.go +++ b/internal/config/config_file.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/sunggun-yu/envp/internal/util" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) var ( @@ -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") } @@ -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 } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f9f6e14..746d004 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/sunggun-yu/envp/internal/config" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) var ( diff --git a/internal/config/profile.go b/internal/config/profile.go index c6652f3..c8b4639 100644 --- a/internal/config/profile.go +++ b/internal/config/profile.go @@ -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 @@ -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() @@ -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 { @@ -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 { diff --git a/internal/config/profile_test.go b/internal/config/profile_test.go index 9b5a68f..890b6e1 100644 --- a/internal/config/profile_test.go +++ b/internal/config/profile_test.go @@ -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 ( @@ -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() @@ -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") }) @@ -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") @@ -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") }) } @@ -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)) + }) +} diff --git a/internal/shell/shell.go b/internal/shell/shell.go index aabb327..9775b2c 100644 --- a/internal/shell/shell.go +++ b/internal/shell/shell.go @@ -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 } diff --git a/internal/shell/shell_test.go b/internal/shell/shell_test.go index c02bf95..663a736 100644 --- a/internal/shell/shell_test.go +++ b/internal/shell/shell_test.go @@ -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")) + }) + }) +}) diff --git a/testdata/config.yaml b/testdata/config.yaml index 0cbcf3d..b894702 100644 --- a/testdata/config.yaml +++ b/testdata/config.yaml @@ -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: @@ -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: @@ -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: @@ -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