Skip to content
This repository has been archived by the owner on Mar 14, 2023. It is now read-only.

Commit

Permalink
feat: added var step for write command to env variable
Browse files Browse the repository at this point in the history
runatlantis#370

Signed-off-by: Andrii Nasinnyk <[email protected]>
  • Loading branch information
anasinnyk committed May 30, 2019
1 parent 354551d commit f2595ff
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 15 deletions.
2 changes: 2 additions & 0 deletions server/events/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ type ProjectCommandContext struct {
// Workspace is the Terraform workspace this project is in. It will always
// be set.
Workspace string
// Additional env variables
Env map[string]string
}

// SplitRepoFullName splits a repo full name up into its owner and repo name
Expand Down
2 changes: 2 additions & 0 deletions server/events/project_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx models.Pr
out, err = p.ApplyStepRunner.Run(ctx, step.ExtraArgs, absPath)
case "run":
out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath)
case "var":
ctx.Env[step.Variable], err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath)
}

if out != "" {
Expand Down
4 changes: 2 additions & 2 deletions server/events/runtime/apply_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []stri
// NOTE: we need to quote the plan path because Bitbucket Server can
// have spaces in its repo owner names which is part of the path.
args := append(append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.CommentArgs...), fmt.Sprintf("%q", planPath))
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, args, ctx.TerraformVersion, ctx.Workspace)
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, args, ctx.Env, ctx.TerraformVersion, ctx.Workspace)
}

// If the apply was successful, delete the plan.
Expand Down Expand Up @@ -138,7 +138,7 @@ func (a *ApplyStepRunner) runRemoteApply(

// Start the async command execution.
ctx.Log.Debug("starting async tf remote operation")
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), applyArgs, tfVersion, ctx.Workspace)
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), applyArgs, ctx.Env, tfVersion, ctx.Workspace)
var lines []string
nextLineIsRunURL := false
var runURL string
Expand Down
2 changes: 1 addition & 1 deletion server/events/runtime/init_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin
terraformInitCmd = append([]string{"get", "-no-color", "-upgrade"}, extraArgs...)
}

out, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, terraformInitCmd, tfVersion, ctx.Workspace)
out, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, terraformInitCmd, ctx.Env, tfVersion, ctx.Workspace)
// Only include the init output if there was an error. Otherwise it's
// unnecessary and lengthens the comment.
if err != nil {
Expand Down
10 changes: 5 additions & 5 deletions server/events/runtime/plan_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (p *PlanStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin

planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))
planCmd := p.buildPlanCmd(ctx, extraArgs, path, tfVersion, planFile)
output, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Clean(path), planCmd, tfVersion, ctx.Workspace)
output, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Clean(path), planCmd, ctx.Env, tfVersion, ctx.Workspace)
if p.isRemoteOpsErr(output, err) {
ctx.Log.Debug("detected that this project is using TFE remote ops")
return p.remotePlan(ctx, extraArgs, path, tfVersion, planFile)
Expand Down Expand Up @@ -130,7 +130,7 @@ func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path
// already in the right workspace then no need to switch. This will save us
// about ten seconds. This command is only available in > 0.10.
if !runningZeroPointNine {
workspaceShowOutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "show"}, tfVersion, ctx.Workspace)
workspaceShowOutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "show"}, ctx.Env, tfVersion, ctx.Workspace)
if err != nil {
return err
}
Expand All @@ -145,11 +145,11 @@ func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path
// To do this we can either select and catch the error or use list and then
// look for the workspace. Both commands take the same amount of time so
// that's why we're running select here.
_, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "select", "-no-color", ctx.Workspace}, tfVersion, ctx.Workspace)
_, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "select", "-no-color", ctx.Workspace}, ctx.Env, tfVersion, ctx.Workspace)
if err != nil {
// If terraform workspace select fails we run terraform workspace
// new to create a new workspace automatically.
_, err = p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "new", "-no-color", ctx.Workspace}, tfVersion, ctx.Workspace)
_, err = p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "new", "-no-color", ctx.Workspace}, ctx.Env, tfVersion, ctx.Workspace)
return err
}
return nil
Expand Down Expand Up @@ -261,7 +261,7 @@ func (p *PlanStepRunner) runRemotePlan(

// Start the async command execution.
ctx.Log.Debug("starting async tf remote operation")
_, outCh := p.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), cmdArgs, tfVersion, ctx.Workspace)
_, outCh := p.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), cmdArgs, ctx.Env, tfVersion, ctx.Workspace)
var lines []string
nextLineIsRunURL := false
var runURL string
Expand Down
3 changes: 3 additions & 0 deletions server/events/runtime/run_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command string, pa
for key, val := range customEnvVars {
finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val))
}
for key, val := range ctx.Env {
finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val))
}
cmd.Env = finalEnvVars
out, err := cmd.CombinedOutput()

Expand Down
14 changes: 12 additions & 2 deletions server/events/terraform/terraform_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,16 @@ func (c *DefaultClient) DefaultVersion() *version.Version {
}

// See Client.RunCommandWithVersion.
func (c *DefaultClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error) {
func (c *DefaultClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (string, error) {
tfCmd, cmd, err := c.prepCmd(log, v, workspace, path, args)
if err != nil {
return "", err
}
envVars := cmd.Env
for key, val := range customEnvVars {
envVars = append(envVars, fmt.Sprintf("%s=%s", key, val))
}
cmd.Env = envVars
out, err := cmd.CombinedOutput()
if err != nil {
err = errors.Wrapf(err, "running %q in %q", tfCmd, path)
Expand Down Expand Up @@ -250,7 +255,7 @@ type Line struct {
// Callers can use the input channel to pass stdin input to the command.
// If any error is passed on the out channel, there will be no
// further output (so callers are free to exit).
func (c *DefaultClient) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (chan<- string, <-chan Line) {
func (c *DefaultClient) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (chan<- string, <-chan Line) {
outCh := make(chan Line)
inCh := make(chan string)

Expand All @@ -273,6 +278,11 @@ func (c *DefaultClient) RunCommandAsync(log *logging.SimpleLogger, path string,
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
stdin, _ := cmd.StdinPipe()
envVars := cmd.Env
for key, val := range customEnvVars {
envVars = append(envVars, fmt.Sprintf("%s=%s", key, val))
}
cmd.Env = envVars

log.Debug("starting %q in %q", tfCmd, path)
err = cmd.Start()
Expand Down
81 changes: 76 additions & 5 deletions server/events/yaml/raw/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,27 @@ import (

const (
ExtraArgsKey = "extra_args"
NameArgKey = "name"
CommandArgKey = "command"
RunStepName = "run"
PlanStepName = "plan"
ApplyStepName = "apply"
InitStepName = "init"
VarStepName = "var"
)

// Step represents a single action/command to perform. In YAML, it can be set as
// 1. A single string for a built-in command:
// - init
// - plan
// 2. A map for a built-in command and extra_args:
// 2. A map for a built-in var with name and command
// - var:
// name: test
// command: echo 312
// 3. A map for a built-in command and extra_args:
// - plan:
// extra_args: [-var-file=staging.tfvars]
// 3. A map for a custom run command:
// 4. A map for a custom run command:
// - run: my custom command
// Here we parse step in the most generic fashion possible. See fields for more
// details.
Expand All @@ -35,8 +42,10 @@ type Step struct {
// could be multiple keys (since the element is a map) so we don't set Key.
Key *string
// Map will be set in case #2 above.
Var map[string]map[string]string
// Map will be set in case #3 above.
Map map[string]map[string][]string
// StringVal will be set in case #3 above.
// StringVal will be set in case #4 above.
StringVal map[string]string
}

Expand All @@ -52,7 +61,7 @@ func (s *Step) UnmarshalJSON(data []byte) error {
func (s Step) Validate() error {
validStep := func(value interface{}) error {
str := *value.(*string)
if str != InitStepName && str != PlanStepName && str != ApplyStepName {
if str != InitStepName && str != PlanStepName && str != ApplyStepName && str != VarStepName {
return fmt.Errorf("%q is not a valid step type, maybe you omitted the 'run' key", str)
}
return nil
Expand Down Expand Up @@ -94,6 +103,40 @@ func (s Step) Validate() error {
return nil
}

varStep := func(value interface{}) error {
elem := value.(map[string]map[string]string)
var keys []string
for k := range elem {
keys = append(keys, k)
}
// Sort so tests can be deterministic.
sort.Strings(keys)

if len(keys) > 1 {
return fmt.Errorf("step element can only contain a single key, found %d: %s",
len(keys), strings.Join(keys, ","))
}
for stepName, args := range elem {
if stepName != VarStepName {
return fmt.Errorf("%q is not a valid step type", stepName)
}
var argKeys []string
for k := range args {
argKeys = append(argKeys, k)
}
if len(argKeys) != 2 {
return fmt.Errorf("built-in steps only support two keys %s and %s, found %d: %s",
NameArgKey, CommandArgKey, len(argKeys), strings.Join(argKeys, ","))
}
for k := range args {
if k != NameArgKey && k != CommandArgKey {
return fmt.Errorf("built-in steps only support two keys %s and %s, found %q in step %s", NameArgKey, CommandArgKey, k, stepName)
}
}
}
return nil
}

runStep := func(value interface{}) error {
elem := value.(map[string]string)
var keys []string
Expand Down Expand Up @@ -121,6 +164,9 @@ func (s Step) Validate() error {
if len(s.Map) > 0 {
return validation.Validate(s.Map, validation.By(extraArgs))
}
if len(s.Var) > 0 {
return validation.Validate(s.Var, validation.By(varStep))
}
if len(s.StringVal) > 0 {
return validation.Validate(s.StringVal, validation.By(runStep))
}
Expand All @@ -136,6 +182,19 @@ func (s Step) ToValid() valid.Step {
}

// This will trigger in case #2 (see Step docs).
if len(s.Var) > 0 {
// After validation we assume there's only one key and it's a valid
// step name so we just use the first one.
for stepName, stepArgs := range s.Var {
return valid.Step{
StepName: stepName,
Variable: stepArgs[NameArgKey],
RunCommand: stepArgs[CommandArgKey],
}
}
}

// This will trigger in case #3 (see Step docs).
if len(s.Map) > 0 {
// After validation we assume there's only one key and it's a valid
// step name so we just use the first one.
Expand All @@ -147,7 +206,7 @@ func (s Step) ToValid() valid.Step {
}
}

// This will trigger in case #3 (see Step docs).
// This will trigger in case #4 (see Step docs).
if len(s.StringVal) > 0 {
// After validation we assume there's only one key and it's a valid
// step name so we just use the first one.
Expand Down Expand Up @@ -196,6 +255,18 @@ func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error {
return nil
}

// This represents a step with extra_args, ex:
// var:
// name: k
// command: exec
// We validate if the key var
var varStep map[string]map[string]string
err = unmarshal(&varStep)
if err == nil {
s.Var = varStep
return nil
}

// Try to unmarshal as a custom run step, ex.
// steps:
// - run: my command
Expand Down
29 changes: 29 additions & 0 deletions server/events/yaml/raw/step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,18 @@ func TestStep_Validate(t *testing.T) {
},
expErr: "",
},
{
description: "var",
input: raw.Step{
Var: VarType{
"var": {
"name": "test",
"command": "echo 123",
},
},
},
expErr: "",
},
{
description: "apply extra_args",
input: raw.Step{
Expand Down Expand Up @@ -323,6 +335,22 @@ func TestStep_ToValid(t *testing.T) {
StepName: "apply",
},
},
{
description: "var step",
input: raw.Step{
Var: VarType{
"var": {
"name": "test",
"command": "echo 123",
},
},
},
exp: valid.Step{
StepName: "var",
RunCommand: "echo 123",
Variable: "test",
},
},
{
description: "init extra_args",
input: raw.Step{
Expand Down Expand Up @@ -386,3 +414,4 @@ func TestStep_ToValid(t *testing.T) {
}

type MapType map[string]map[string][]string
type VarType map[string]map[string]string
1 change: 1 addition & 0 deletions server/events/yaml/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type Step struct {
StepName string
ExtraArgs []string
RunCommand string
Variable string
}

type Workflow struct {
Expand Down

0 comments on commit f2595ff

Please sign in to comment.