diff --git a/atmos.yaml b/atmos.yaml index e13d91a2a..7476f2943 100644 --- a/atmos.yaml +++ b/atmos.yaml @@ -170,20 +170,20 @@ commands: # Steps support using Go templates and can access all configuration settings (e.g. {{ .ComponentConfig.xxx.yyy.zzz }}) # Steps also have access to the ENV vars defined in the 'env' section of the 'command' steps: - - 'echo Atmos component from argument: {{ .Arguments.component }}' - - 'echo ATMOS_COMPONENT: $ATMOS_COMPONENT' - - 'echo Atmos stack: {{ .Flags.stack }}' - - 'echo Terraform component: {{ .ComponentConfig.component }}' - - 'echo Backend S3 bucket: {{ .ComponentConfig.backend.bucket }}' - - 'echo Terraform workspace: {{ .ComponentConfig.workspace }}' - - 'echo Namespace: {{ .ComponentConfig.vars.namespace }}' - - 'echo Tenant: {{ .ComponentConfig.vars.tenant }}' - - 'echo Environment: {{ .ComponentConfig.vars.environment }}' - - 'echo Stage: {{ .ComponentConfig.vars.stage }}' - - 'echo settings.spacelift.workspace_enabled: {{ .ComponentConfig.settings.spacelift.workspace_enabled }}' - - 'echo Dependencies: {{ .ComponentConfig.deps }}' - - 'echo settings.config.is_prod: {{ .ComponentConfig.settings.config.is_prod }}' - - 'echo ATMOS_IS_PROD: $ATMOS_IS_PROD' + - 'echo Atmos component from argument: "{{ .Arguments.component }}"' + - 'echo ATMOS_COMPONENT: "$ATMOS_COMPONENT"' + - 'echo Atmos stack: "{{ .Flags.stack }}"' + - 'echo Terraform component: "{{ .ComponentConfig.component }}"' + - 'echo Backend S3 bucket: "{{ .ComponentConfig.backend.bucket }}"' + - 'echo Terraform workspace: "{{ .ComponentConfig.workspace }}"' + - 'echo Namespace: "{{ .ComponentConfig.vars.namespace }}"' + - 'echo Tenant: "{{ .ComponentConfig.vars.tenant }}"' + - 'echo Environment: "{{ .ComponentConfig.vars.environment }}"' + - 'echo Stage: "{{ .ComponentConfig.vars.stage }}"' + - 'echo settings.spacelift.workspace_enabled: "{{ .ComponentConfig.settings.spacelift.workspace_enabled }}"' + - 'echo Dependencies: "{{ .ComponentConfig.deps }}"' + - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' + - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' # Integrations integrations: diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 1be5da941..90edbc770 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -7,7 +7,6 @@ import ( e "github.com/cloudposse/atmos/internal/exec" "github.com/spf13/cobra" "os" - "strings" "text/template" cfg "github.com/cloudposse/atmos/pkg/config" @@ -192,8 +191,8 @@ func executeCustomCommand(cmd *cobra.Command, args []string, parentCommand *cobr // If the command to get the value for the ENV var is provided, execute it if valCommand != "" { - valCommandArgs := strings.Fields(valCommand) - res, err := e.ExecuteShellCommandAndReturnOutput(valCommandArgs[0], valCommandArgs[1:], ".", nil, false, commandConfig.Verbose) + valCommandName := fmt.Sprintf("env-var-%s-valcommand", key) + res, err := e.ExecuteShellAndReturnOutput(valCommand, valCommandName, ".", nil, false, commandConfig.Verbose) if err != nil { u.PrintErrorToStdErrorAndExit(err) } @@ -222,15 +221,14 @@ func executeCustomCommand(cmd *cobra.Command, args []string, parentCommand *cobr // Process Go templates in the command's steps. // Steps support Go templates and have access to {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables - commandTmpl, err := processTmpl(fmt.Sprintf("step-%d", i), step, data) + commandToRun, err := processTmpl(fmt.Sprintf("step-%d", i), step, data) if err != nil { u.PrintErrorToStdErrorAndExit(err) } - commandToRun := os.ExpandEnv(commandTmpl) // Execute the command step - stepArgs := strings.Fields(commandToRun) - err = e.ExecuteShellCommand(stepArgs[0], stepArgs[1:], ".", envVarsList, false, commandConfig.Verbose) + commandName := fmt.Sprintf("%s-step-%d", commandConfig.Name, i) + err = e.ExecuteShell(commandToRun, commandName, ".", envVarsList, false, commandConfig.Verbose) if err != nil { u.PrintErrorToStdErrorAndExit(err) } diff --git a/go.mod b/go.mod index 56a4364c5..9e7f5fefb 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/zclconf/go-cty v1.12.1 gopkg.in/yaml.v2 v2.4.0 + mvdan.cc/sh/v3 v3.5.1 ) require ( @@ -111,6 +112,7 @@ require ( golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect golang.org/x/text v0.4.0 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.102.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 6cbff0ce1..b0f87963b 100644 --- a/go.sum +++ b/go.sum @@ -745,6 +745,7 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1039,6 +1040,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= +mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= oras.land/oras-go v1.2.1 h1:/VcGS8FUy3eEXLl/1vC4QypLHwrfSmgW7ygsoklqKK8= oras.land/oras-go v1.2.1/go.mod h1:3N11Z5E3c4ZzOjroCl1RtAdB4yNAYl7A27j2SVf913A= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/exec/shell_utils.go b/internal/exec/shell_utils.go index b5be0a7ab..07152ffe8 100644 --- a/internal/exec/shell_utils.go +++ b/internal/exec/shell_utils.go @@ -1,13 +1,19 @@ package exec import ( + "bytes" + "context" "fmt" + "io" "os" "os/exec" "runtime" "strings" u "github.com/cloudposse/atmos/pkg/utils" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" ) // ExecuteShellCommand prints and executes the provided command with args and flags @@ -31,6 +37,62 @@ func ExecuteShellCommand(command string, args []string, dir string, env []string return cmd.Run() } +// ExecuteShell runs a shell script +func ExecuteShell(command string, name string, dir string, env []string, dryRun bool, verbose bool) error { + if verbose { + u.PrintInfo("\nExecuting command:") + fmt.Println(command) + } + + if dryRun { + return nil + } + + return shellRunner(command, name, dir, env, os.Stdout) +} + +// ExecuteShellAndReturnOutput runs a shell script and capture its standard output +func ExecuteShellAndReturnOutput(command string, name string, dir string, env []string, dryRun bool, verbose bool) (string, error) { + var b bytes.Buffer + + if verbose { + u.PrintInfo("\nExecuting command:") + fmt.Println(command) + } + + if dryRun { + return "", nil + } + + err := shellRunner(command, name, dir, env, &b) + if err != nil { + return "", err + } + + return b.String(), nil +} + +// shellRunner uses mvdan.cc/sh/v3's parser and interpreter to run a shell script and divert its stdout +func shellRunner(command string, name string, dir string, env []string, out io.Writer) error { + parser, err := syntax.NewParser().Parse(strings.NewReader(command), name) + if err != nil { + return err + } + + environ := append(os.Environ(), env...) + listEnviron := expand.ListEnviron(environ...) + runner, err := interp.New( + interp.Dir(dir), + interp.Env(listEnviron), + interp.StdIO(os.Stdin, out, os.Stderr), + ) + if err != nil { + return err + } + + return runner.Run(context.TODO(), parser) +} + // ExecuteShellCommandAndReturnOutput prints and executes the provided command with args and flags and returns the command output func ExecuteShellCommandAndReturnOutput(command string, args []string, dir string, env []string, dryRun bool, verbose bool) (string, error) { cmd := exec.Command(command, args...)