Skip to content

feat: Add support for multiple init-script #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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