From 0d1f50a7a882bf92d0bcaddee21912c1e3b1dd93 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Thu, 22 Feb 2018 17:31:24 -0800 Subject: [PATCH 1/4] Implement parsing for -w and -d flags. --- server/events/event_parser.go | 77 +++++---- server/events/event_parser_test.go | 243 ++++++++++++++++++++--------- 2 files changed, 216 insertions(+), 104 deletions(-) diff --git a/server/events/event_parser.go b/server/events/event_parser.go index c9b71971a1..612bdd7257 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -9,6 +9,7 @@ import ( "github.com/lkysow/go-gitlab" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" + "github.com/spf13/pflag" ) const gitlabPullOpened = "opened" @@ -20,6 +21,7 @@ type Command struct { Workspace string Verbose bool Flags []string + Dir string } type EventParsing interface { @@ -60,10 +62,6 @@ func (e *EventParser) DetermineCommand(comment string, vcsHost vcs.Host) (*Comma return nil, err } - workspace := "default" - verbose := false - var flags []string - vcsUser := e.GithubUser if vcsHost == vcs.Gitlab { vcsUser = e.GitlabUser @@ -71,41 +69,56 @@ func (e *EventParser) DetermineCommand(comment string, vcsHost vcs.Host) (*Comma if !e.stringInSlice(args[0], []string{"run", "atlantis", "@" + vcsUser}) { return nil, err } - if !e.stringInSlice(args[1], []string{"plan", "apply", "help"}) { + if !e.stringInSlice(args[1], []string{"plan", "apply", "help", "-help", "--help"}) { return nil, err } - if args[1] == "help" { + + command := args[1] + if command == "help" || command == "-help" || command == "--help" { return &Command{Name: Help}, nil } - command := args[1] - - if len(args) > 2 { - flags = args[2:] - - // if the third arg doesn't start with '-' then we assume it's a - // workspace, not a flag - if !strings.HasPrefix(args[2], "-") { - workspace = args[2] - flags = args[3:] - } - // check for --verbose specially and then remove any additional - // occurrences - if e.stringInSlice("--verbose", flags) { - verbose = true - flags = e.removeOccurrences("--verbose", flags) - } + var workspace string + var dir string + var verbose bool + var extraArgs []string + var flagSet *pflag.FlagSet + var name CommandName + + // Set up the flag parsing depending on the command. + if command == "plan" { + name = Plan + flagSet = pflag.NewFlagSet("plan", pflag.ContinueOnError) + flagSet.StringVarP(&workspace, "workspace", "w", "default", "Switch to this Terraform workspace before planning.") + flagSet.StringVarP(&dir, "dir", "d", ".", "Which directory to run plan in. Defaults to the root of the repo.") + flagSet.BoolVarP(&verbose, "verbose", "", false, "Append Atlantis log to comment.") + } else if command == "apply" { + name = Apply + flagSet = pflag.NewFlagSet("apply", pflag.ContinueOnError) + flagSet.StringVarP(&workspace, "workspace", "w", "default", "Apply the plan for this Terraform workspace.") + flagSet.StringVarP(&dir, "dir", "d", ".", "Run apply in this directory. Defaults to the root of the repo.") + flagSet.BoolVarP(&verbose, "verbose", "", false, "Append Atlantis log to comment.") + + } else { + return nil, fmt.Errorf("unknown command %q – this is a bug", command) + } + + // Now parse the flags. + if err := flagSet.Parse(args[2:]); err != nil { + return nil, err } - - c := &Command{Verbose: verbose, Workspace: workspace, Flags: flags} - switch command { - case "plan": - c.Name = Plan - case "apply": - c.Name = Apply - default: - return nil, fmt.Errorf("something went wrong parsing the command, the command we parsed %q was not apply or plan", command) + // We only use the extra args after the --. For example given a comment: + // "atlantis plan -bad-option -- -target=hi" + // we only append "-target=hi" to the eventual command. + // todo: keep track of the args we're discarding and include that with + // comment as a warning. + if flagSet.ArgsLenAtDash() != -1 { + extraArgs = flagSet.Args()[flagSet.ArgsLenAtDash():] } + + // todo: validate args + + c := &Command{Name: name, Verbose: verbose, Workspace: workspace, Dir: dir, Flags: extraArgs} return c, nil } diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index eac4f53696..cad775e3f8 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -1,12 +1,11 @@ package events_test import ( - "testing" - + "encoding/json" "errors" + "fmt" "strings" - - "encoding/json" + "testing" "github.com/google/go-github/github" "github.com/lkysow/go-gitlab" @@ -26,7 +25,7 @@ var parser = events.EventParser{ } func TestDetermineCommandInvalid(t *testing.T) { - t.Log("given a comment that does not match the regex should return an error") + t.Log("given an invalid comment, should return an error") comments := []string{ // just the executable, no command "run", @@ -46,79 +45,179 @@ func TestDetermineCommandInvalid(t *testing.T) { } } -func TestDetermineCommandHelp(t *testing.T) { +func TestDetermineCommand_ExecutableNames(t *testing.T) { + t.Log("should be allowed to use different executable names in the comments") + parsed, err := parser.DetermineCommand("atlantis plan", vcs.Github) + Ok(t, err) + Equals(t, events.Plan, parsed.Name) + + parsed, err = parser.DetermineCommand("run plan", vcs.Github) + Ok(t, err) + Equals(t, events.Plan, parsed.Name) + + parsed, err = parser.DetermineCommand("@github-user plan", vcs.Github) + Ok(t, err) + Equals(t, events.Plan, parsed.Name) + + parsed, err = parser.DetermineCommand("@gitlab-user plan", vcs.Gitlab) + Ok(t, err) + Equals(t, events.Plan, parsed.Name) +} + +func TestDetermineCommand_Help(t *testing.T) { t.Log("given a help comment, should match") - comments := []string{ - "run help", - "atlantis help", - "@github-user help", - "atlantis help --verbose", + helpArgs := []string{ + "help", + "-help", + "--help", + "help -verbose", + "help --hi", + "help somethingelse", } - for _, c := range comments { - command, e := parser.DetermineCommand(c, vcs.Github) - Ok(t, e) - Equals(t, events.Help, command.Name) + for _, arg := range helpArgs { + comment := fmt.Sprintf("atlantis %s", arg) + command, err := parser.DetermineCommand(comment, vcs.Github) + Assert(t, err == nil, "did not parse comment %q as help command, got err: %s", comment, err) + Assert(t, command.Name == events.Help, "did not parse comment %q as help command", comment) } } -// nolint: gocyclo -func TestDetermineCommandPermutations(t *testing.T) { - execNames := []string{"run", "atlantis", "@github-user", "@gitlab-user"} - commandNames := []events.CommandName{events.Plan, events.Apply} - workspaces := []string{"", "default", "workspace", "workspace-dash", "workspace_underscore", "camelWorkspace"} - flagCases := [][]string{ - {}, - {"--verbose"}, - {"-key=value"}, - {"-key", "value"}, - {"-key1=value1", "-key2=value2"}, - {"-key1=value1", "-key2", "value2"}, - {"-key1", "value1", "-key2=value2"}, - {"--verbose", "key2=value2"}, - {"-key1=value1", "--verbose"}, +func TestDetermineCommand_Parsing(t *testing.T) { + cases := []struct { + flags string + expWorkspace string + expDir string + expVerbose bool + expExtraArgs string + }{ + // Test defaults. + { + "", + "default", + ".", + false, + "", + }, + // Test each flag individually. + { + "-w workspace", + "workspace", + ".", + false, + "", + }, + { + "-d dir", + "default", + "dir", + false, + "", + }, + { + "--verbose", + "default", + ".", + true, + "", + }, + // Test all of them with different permutations. + { + "-w workspace -d dir --verbose", + "workspace", + "dir", + true, + "", + }, + { + "-d dir -w workspace --verbose", + "workspace", + "dir", + true, + "", + }, + { + "--verbose -w workspace -d dir", + "workspace", + "dir", + true, + "", + }, + // Test that flags after -- are ignored + { + "-w workspace -d dir -- --verbose", + "workspace", + "dir", + false, + "--verbose", + }, + { + "-w workspace -- -d dir --verbose", + "workspace", + ".", + false, + "-d dir --verbose", + }, + // Test missing arguments. + { + "-w -d dir --verbose", + "-d", + ".", + true, + "", + }, + // Test the extra args parsing. + { + "--", + "default", + ".", + false, + "", + }, + { + "abc --", + "default", + ".", + false, + "", + }, + { + "-w workspace -d dir --verbose -- arg one -two --three &&", + "workspace", + "dir", + true, + "arg one -two --three &&", + }, + // Test whitespace. + { + "\t-w\tworkspace\t-d\tdir\t--verbose\t--\targ\tone\t-two\t--three\t&&", + "workspace", + "dir", + true, + "arg one -two --three &&", + }, + { + " -w workspace -d dir --verbose -- arg one -two --three &&", + "workspace", + "dir", + true, + "arg one -two --three &&", + }, } - - // test all permutations - for _, exec := range execNames { - for _, name := range commandNames { - for _, workspace := range workspaces { - for _, flags := range flagCases { - // If github comments end in a newline they get \r\n appended. - // Ensure that we parse commands properly either way. - for _, lineEnding := range []string{"", "\r\n"} { - comment := strings.Join(append([]string{exec, name.String(), workspace}, flags...), " ") + lineEnding - t.Log("testing comment: " + comment) - - // In order to test gitlab without fully refactoring this test - // we're just detecting if we're using the gitlab user as the - // exec name. - vcsHost := vcs.Github - if exec == "@gitlab-user" { - vcsHost = vcs.Gitlab - } - c, err := parser.DetermineCommand(comment, vcsHost) - Ok(t, err) - Equals(t, name, c.Name) - if workspace == "" { - Equals(t, "default", c.Workspace) - } else { - Equals(t, workspace, c.Workspace) - } - Equals(t, containsVerbose(flags), c.Verbose) - - // ensure --verbose never shows up in flags - for _, f := range c.Flags { - Assert(t, f != "--verbose", "Should not pass on the --verbose flag: %v", flags) - } - - // check all flags are present - for _, f := range flags { - if f != "--verbose" { - Contains(t, f, c.Flags) - } - } - } - } + for _, test := range cases { + for _, cmdName := range []string{"plan", "apply"} { + comment := fmt.Sprintf("atlantis %s %s", cmdName, test.flags) + t.Logf("testing comment: %s", comment) + cmd, err := parser.DetermineCommand(comment, vcs.Github) + Assert(t, err == nil, "unexpected err parsing %q: %s", comment, err) + Equals(t, test.expDir, cmd.Dir) + Equals(t, test.expWorkspace, cmd.Workspace) + Equals(t, test.expVerbose, cmd.Verbose) + Equals(t, test.expExtraArgs, strings.Join(cmd.Flags, " ")) + if cmdName == "plan" { + Assert(t, cmd.Name == events.Plan, "did not parse comment %q as plan command", comment) + } + if cmdName == "apply" { + Assert(t, cmd.Name == events.Apply, "did not parse comment %q as apply command", comment) } } } From 6dd82c6de1bdec6a76e35dff2d352d28e7f97af8 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 26 Feb 2018 14:28:49 -0800 Subject: [PATCH 2/4] Validate directory flag. Unlock on preexecute err. Ensure -d flag uses relative dirs and doesn't allow for directory traversal. Fix bug where if there was an error in PreExecute, we wouldn't unlock, leaving a possibly abandoned lock. --- server/events/apply_executor.go | 43 ++++++++++++------ server/events/event_parser.go | 48 ++++++++++++-------- server/events/event_parser_test.go | 68 +++++++++++++++++++++------- server/events/plan_executor.go | 28 ++++++++---- server/events/plan_executor_test.go | 34 ++++++++++++++ server/events/project_pre_execute.go | 28 +++++++++--- 6 files changed, 185 insertions(+), 64 deletions(-) diff --git a/server/events/apply_executor.go b/server/events/apply_executor.go index 47cfa658b4..f38033d82a 100644 --- a/server/events/apply_executor.go +++ b/server/events/apply_executor.go @@ -46,22 +46,39 @@ func (a *ApplyExecutor) Execute(ctx *CommandContext) CommandResponse { // Plans are stored at project roots by their workspace names. We just // need to find them. var plans []models.Plan - err = filepath.Walk(repoDir, func(path string, info os.FileInfo, err error) error { + // If they didn't specify a directory, we apply all plans we can find for + // this workspace. + if ctx.Command.Dir == "" { + err = filepath.Walk(repoDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // Check if the plan is for the right workspace, + if !info.IsDir() && info.Name() == ctx.Command.Workspace+".tfplan" { + rel, _ := filepath.Rel(repoDir, filepath.Dir(path)) + plans = append(plans, models.Plan{ + Project: models.NewProject(ctx.BaseRepo.FullName, rel), + LocalPath: path, + }) + } + return nil + }) if err != nil { - return err + return CommandResponse{Error: errors.Wrap(err, "finding plans")} } - // Check if the plan is for the right workspace, - if !info.IsDir() && info.Name() == ctx.Command.Workspace+".tfplan" { - rel, _ := filepath.Rel(repoDir, filepath.Dir(path)) - plans = append(plans, models.Plan{ - Project: models.NewProject(ctx.BaseRepo.FullName, rel), - LocalPath: path, - }) + } else { + // If they did specify a dir, we apply just the plan in that directory + // for this workspace. + path := filepath.Join(repoDir, ctx.Command.Dir, ctx.Command.Workspace+".tfplan") + stat, err := os.Stat(path) + if err != nil || stat.IsDir() { + return CommandResponse{Error: errors.Wrapf(err, "finding plan for dir %q and workspace %q", ctx.Command.Dir, ctx.Command.Workspace)} } - return nil - }) - if err != nil { - return CommandResponse{Error: errors.Wrap(err, "finding plans")} + rel, _ := filepath.Rel(repoDir, filepath.Dir(path)) + plans = append(plans, models.Plan{ + Project: models.NewProject(ctx.BaseRepo.FullName, filepath.Dir(rel)), + LocalPath: path, + }) } if len(plans) == 0 { return CommandResponse{Failure: "No plans found for that workspace."} diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 612bdd7257..bf7d76a894 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -3,6 +3,7 @@ package events import ( "errors" "fmt" + "path/filepath" "strings" "github.com/google/go-github/github" @@ -21,7 +22,10 @@ type Command struct { Workspace string Verbose bool Flags []string - Dir string + // Dir is the path relative to the repo root to run the command in. + // If empty string then it wasn't specified. "." is the root of the repo. + // Dir will never end in "/". + Dir string } type EventParsing interface { @@ -86,19 +90,19 @@ func (e *EventParser) DetermineCommand(comment string, vcsHost vcs.Host) (*Comma var name CommandName // Set up the flag parsing depending on the command. + const defaultWorkspace = "default" if command == "plan" { name = Plan flagSet = pflag.NewFlagSet("plan", pflag.ContinueOnError) - flagSet.StringVarP(&workspace, "workspace", "w", "default", "Switch to this Terraform workspace before planning.") - flagSet.StringVarP(&dir, "dir", "d", ".", "Which directory to run plan in. Defaults to the root of the repo.") + flagSet.StringVarP(&workspace, "workspace", "w", defaultWorkspace, fmt.Sprintf("Switch to this Terraform workspace before planning. Defaults to '%s'", defaultWorkspace)) + flagSet.StringVarP(&dir, "dir", "d", "", "Which directory to run plan in relative to root of repo. Use '.' for root. If not specified, will attempt to run plan for all Terraform projects we think were modified in this changeset.") flagSet.BoolVarP(&verbose, "verbose", "", false, "Append Atlantis log to comment.") } else if command == "apply" { name = Apply flagSet = pflag.NewFlagSet("apply", pflag.ContinueOnError) - flagSet.StringVarP(&workspace, "workspace", "w", "default", "Apply the plan for this Terraform workspace.") - flagSet.StringVarP(&dir, "dir", "d", ".", "Run apply in this directory. Defaults to the root of the repo.") + flagSet.StringVarP(&workspace, "workspace", "w", defaultWorkspace, fmt.Sprintf("Apply the plan for this Terraform workspace. Defaults to '%s'", defaultWorkspace)) + flagSet.StringVarP(&dir, "dir", "d", "", "Run apply in this directory relative to root of repo. Use '.' for root. If not specified, will run apply against all plans created for this workspace.") flagSet.BoolVarP(&verbose, "verbose", "", false, "Append Atlantis log to comment.") - } else { return nil, fmt.Errorf("unknown command %q – this is a bug", command) } @@ -116,7 +120,26 @@ func (e *EventParser) DetermineCommand(comment string, vcsHost vcs.Host) (*Comma extraArgs = flagSet.Args()[flagSet.ArgsLenAtDash():] } - // todo: validate args + // If dir is specified, must ensure it's a valid path. + if dir != "" { + validatedDir := filepath.Clean(dir) + // Join with . so the path is relative. This helps us if they use '/', + // and is safe to do if their path is relative since it's a no-op. + validatedDir = filepath.Join(".", validatedDir) + // Need to clean again to resolve relative validatedDirs. + validatedDir = filepath.Clean(validatedDir) + // Detect relative dirs since they're not allowed. + if strings.HasPrefix(validatedDir, "..") { + return nil, fmt.Errorf("relative path %q not allowed", dir) + } + + dir = validatedDir + } + // Because we use the workspace name as a file, need to make sure it's + // not doing something weird like being a relative dir. + if strings.Contains(workspace, "..") { + return nil, errors.New("workspace can't contain '..'") + } c := &Command{Name: name, Verbose: verbose, Workspace: workspace, Dir: dir, Flags: extraArgs} return c, nil @@ -321,14 +344,3 @@ func (e *EventParser) stringInSlice(a string, list []string) bool { } return false } - -// nolint: unparam -func (e *EventParser) removeOccurrences(a string, list []string) []string { - var out []string - for _, b := range list { - if b != a { - out = append(out, b) - } - } - return out -} diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index cad775e3f8..c8c5bc8aea 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -36,6 +36,15 @@ func TestDetermineCommandInvalid(t *testing.T) { "atlantis slkjd", "@github-user slkjd", "atlantis plans", + // relative dirs + "atlantis plan -d ..", + "atlantis plan -d ../", + "atlantis plan -d a/../../", + // using .. in workspace + "atlantis plan -w a..", + "atlantis plan -w ../", + "atlantis plan -w ..", + "atlantis plan -w a/../b", // misc "related comment mentioning atlantis", } @@ -94,7 +103,7 @@ func TestDetermineCommand_Parsing(t *testing.T) { { "", "default", - ".", + "", false, "", }, @@ -102,7 +111,7 @@ func TestDetermineCommand_Parsing(t *testing.T) { { "-w workspace", "workspace", - ".", + "", false, "", }, @@ -116,7 +125,7 @@ func TestDetermineCommand_Parsing(t *testing.T) { { "--verbose", "default", - ".", + "", true, "", }, @@ -153,7 +162,7 @@ func TestDetermineCommand_Parsing(t *testing.T) { { "-w workspace -- -d dir --verbose", "workspace", - ".", + "", false, "-d dir --verbose", }, @@ -161,7 +170,7 @@ func TestDetermineCommand_Parsing(t *testing.T) { { "-w -d dir --verbose", "-d", - ".", + "", true, "", }, @@ -169,14 +178,14 @@ func TestDetermineCommand_Parsing(t *testing.T) { { "--", "default", - ".", + "", false, "", }, { "abc --", "default", - ".", + "", false, "", }, @@ -202,6 +211,42 @@ func TestDetermineCommand_Parsing(t *testing.T) { true, "arg one -two --three &&", }, + // Test that the dir string is normalized. + { + "-d /", + "default", + ".", + false, + "", + }, + { + "-d /adir", + "default", + "adir", + false, + "", + }, + { + "-d .", + "default", + ".", + false, + "", + }, + { + "-d ./", + "default", + ".", + false, + "", + }, + { + "-d ./adir", + "default", + "adir", + false, + "", + }, } for _, test := range cases { for _, cmdName := range []string{"plan", "apply"} { @@ -437,15 +482,6 @@ func TestParseGitlabMergeCommentEvent(t *testing.T) { }, user) } -func containsVerbose(list []string) bool { - for _, b := range list { - if b == "--verbose" { - return true - } - } - return false -} - var mergeEventJSON = `{ "object_kind": "merge_request", "user": { diff --git a/server/events/plan_executor.go b/server/events/plan_executor.go index 17583a9ebf..b7c949984a 100644 --- a/server/events/plan_executor.go +++ b/server/events/plan_executor.go @@ -52,21 +52,29 @@ func (p *PlanExecutor) SetLockURL(f func(id string) (url string)) { // Execute executes terraform plan for the ctx. func (p *PlanExecutor) Execute(ctx *CommandContext) CommandResponse { - // Figure out what projects have been modified so we know where to run plan. - modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull, ctx.VCSHost) - if err != nil { - return CommandResponse{Error: errors.Wrap(err, "getting modified files")} - } - cloneDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Command.Workspace) if err != nil { return CommandResponse{Error: err} } - ctx.Log.Info("found %d files modified in this pull request", len(modifiedFiles)) - projects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.BaseRepo.FullName, cloneDir) - if len(projects) == 0 { - return CommandResponse{Failure: "No Terraform files were modified."} + var projects []models.Project + if ctx.Command.Dir == "" { + // If they didn't specify a directory to plan in, figure out what + // projects have been modified so we know where to run plan. + modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull, ctx.VCSHost) + if err != nil { + return CommandResponse{Error: errors.Wrap(err, "getting modified files")} + } + ctx.Log.Info("found %d files modified in this pull request", len(modifiedFiles)) + projects = p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.BaseRepo.FullName, cloneDir) + if len(projects) == 0 { + return CommandResponse{Failure: "No Terraform files were modified."} + } + } else { + projects = []models.Project{{ + Path: ctx.Command.Dir, + RepoFullName: ctx.BaseRepo.FullName, + }} } var results []ProjectResult diff --git a/server/events/plan_executor_test.go b/server/events/plan_executor_test.go index ec07b49064..a26440eed6 100644 --- a/server/events/plan_executor_test.go +++ b/server/events/plan_executor_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/mohae/deepcopy" . "github.com/petergtz/pegomock" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/locking" @@ -22,6 +23,7 @@ var planCtx = events.CommandContext{ Command: &events.Command{ Name: events.Plan, Workspace: "workspace", + Dir: "", }, Log: logging.NewNoopLogger(), BaseRepo: models.Repo{}, @@ -63,6 +65,38 @@ func TestExecute_CloneErr(t *testing.T) { Equals(t, "err", r.Error.Error()) } +func TestExecute_DirectoryAndWorkspaceSet(t *testing.T) { + t.Log("Test that we run plan in the right directory and workspace if they're set") + p, runner, _ := setupPlanExecutorTest(t) + ctx := deepcopy.Copy(planCtx).(events.CommandContext) + ctx.Log = logging.NewNoopLogger() + ctx.Command.Dir = "dir1/dir2" + ctx.Command.Workspace = "workspace-flag" + + When(p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, "workspace-flag")). + ThenReturn("/tmp/clone-repo", nil) + When(p.ProjectPreExecute.Execute(&ctx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "dir1/dir2"})). + ThenReturn(events.PreExecuteResult{ + LockResponse: locking.TryLockResponse{ + LockKey: "key", + }, + }) + r := p.Execute(&ctx) + + runner.VerifyWasCalledOnce().RunCommandWithVersion( + ctx.Log, + "/tmp/clone-repo/dir1/dir2", + []string{"plan", "-refresh", "-no-color", "-out", "/tmp/clone-repo/dir1/dir2/workspace-flag.tfplan", "-var", "atlantis_user=anubhavmishra"}, + nil, + "workspace-flag", + ) + Assert(t, len(r.ProjectResults) == 1, "exp one project result") + result := r.ProjectResults[0] + Assert(t, result.PlanSuccess != nil, "exp plan success to not be nil") + Equals(t, "", result.PlanSuccess.TerraformOutput) + Equals(t, "lockurl-key", result.PlanSuccess.LockURL) +} + func TestExecute_Success(t *testing.T) { t.Log("If there are no errors, the plan should be returned") p, runner, _ := setupPlanExecutorTest(t) diff --git a/server/events/project_pre_execute.go b/server/events/project_pre_execute.go index e831d8a41c..3dde2bf92c 100644 --- a/server/events/project_pre_execute.go +++ b/server/events/project_pre_execute.go @@ -51,14 +51,28 @@ func (p *DefaultProjectPreExecutor) Execute(ctx *CommandContext, repoDir string, lockAttempt.CurrLock.Pull.Num)}} } ctx.Log.Info("acquired lock with id %q", lockAttempt.LockKey) + config, tfVersion, err := p.executeWithLock(ctx, repoDir, project) + if err != nil { + p.Locker.Unlock(lockAttempt.LockKey) // nolint: errcheck + return PreExecuteResult{ProjectResult: ProjectResult{Error: err}} + } + return PreExecuteResult{ProjectConfig: config, TerraformVersion: tfVersion, LockResponse: lockAttempt} +} + +// executeWithLock executes the pre plan/apply tasks after the lock has been +// acquired. This helper func makes revoking the lock on error easier. +// Returns the project config, terraform version, or an error. +func (p *DefaultProjectPreExecutor) executeWithLock(ctx *CommandContext, repoDir string, project models.Project) (ProjectConfig, *version.Version, error) { + workspace := ctx.Command.Workspace // Check if config file is found, if not we continue the run. var config ProjectConfig absolutePath := filepath.Join(repoDir, project.Path) if p.ConfigReader.Exists(absolutePath) { + var err error config, err = p.ConfigReader.Read(absolutePath) if err != nil { - return PreExecuteResult{ProjectResult: ProjectResult{Error: err}} + return config, nil, err } ctx.Log.Info("parsed atlantis config file in %q", absolutePath) } @@ -74,25 +88,25 @@ func (p *DefaultProjectPreExecutor) Execute(ctx *CommandContext, repoDir string, if len(config.PreInit) > 0 { _, err := p.Run.Execute(ctx.Log, config.PreInit, absolutePath, workspace, terraformVersion, "pre_init") if err != nil { - return PreExecuteResult{ProjectResult: ProjectResult{Error: errors.Wrapf(err, "running %s commands", "pre_init")}} + return config, nil, errors.Wrapf(err, "running %s commands", "pre_init") } } _, err := p.Terraform.Init(ctx.Log, absolutePath, workspace, config.GetExtraArguments("init"), terraformVersion) if err != nil { - return PreExecuteResult{ProjectResult: ProjectResult{Error: err}} + return config, nil, err } } else { ctx.Log.Info("determined that we are running terraform with version < 0.9.0. Running version %s", terraformVersion) if len(config.PreGet) > 0 { _, err := p.Run.Execute(ctx.Log, config.PreGet, absolutePath, workspace, terraformVersion, "pre_get") if err != nil { - return PreExecuteResult{ProjectResult: ProjectResult{Error: errors.Wrapf(err, "running %s commands", "pre_get")}} + return config, nil, errors.Wrapf(err, "running %s commands", "pre_get") } } terraformGetCmd := append([]string{"get", "-no-color"}, config.GetExtraArguments("get")...) _, err := p.Terraform.RunCommandWithVersion(ctx.Log, absolutePath, terraformGetCmd, terraformVersion, workspace) if err != nil { - return PreExecuteResult{ProjectResult: ProjectResult{Error: err}} + return config, nil, err } } @@ -106,8 +120,8 @@ func (p *DefaultProjectPreExecutor) Execute(ctx *CommandContext, repoDir string, if len(commands) > 0 { _, err := p.Run.Execute(ctx.Log, commands, absolutePath, workspace, terraformVersion, stage) if err != nil { - return PreExecuteResult{ProjectResult: ProjectResult{Error: errors.Wrapf(err, "running %s commands", stage)}} + return config, nil, errors.Wrapf(err, "running %s commands", stage) } } - return PreExecuteResult{ProjectConfig: config, TerraformVersion: terraformVersion, LockResponse: lockAttempt} + return config, terraformVersion, nil } From 0fd503da8ad4f695e309d097f9b4172de93fd765 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 26 Feb 2018 15:00:47 -0800 Subject: [PATCH 3/4] Update README for new flags. --- README.md | 53 +++++++++++++++++++++++++--------- docs/pr-comment-apply.png | Bin 0 -> 19474 bytes docs/pr-comment-help.png | Bin 0 -> 19736 bytes docs/pr-comment-plan.png | Bin 0 -> 19305 bytes server/events/event_parser.go | 2 +- 5 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 docs/pr-comment-apply.png create mode 100644 docs/pr-comment-help.png create mode 100644 docs/pr-comment-plan.png diff --git a/README.md b/README.md index 961908f683..07c0bfc467 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ Read about [Why We Built Atlantis](https://www.atlantis.run/blog/atlantis-releas - Optionally, require a **review and approval** prior to running `apply` ➜ Also -- No more **copy-pasted code across workspaces/environments**. Atlantis supports using an `env/{env}.tfvars` file per workspace/environment so you can write your base configuration once - Support **multiple versions of Terraform** with a simple project config file ## Atlantis Works With * GitHub (public, private or enterprise) and GitLab (public, private or enterprise) * Any Terraform version (see [Terraform Versions](#terraform-version)) * Can be run with a [single binary](https://github.com/runatlantis/atlantis/releases) or with our [Docker image](https://hub.docker.com/r/runatlantis/atlantis/) +* Any repository structure ## Getting Started Download from [https://github.com/runatlantis/atlantis/releases](https://github.com/runatlantis/atlantis/releases) @@ -71,15 +71,41 @@ If you're ready to permanently set up Atlantis see [Production-Ready Deployment] ## Pull/Merge Request Commands Atlantis currently supports three commands that can be run via pull request comments (or merge request comments on GitLab): +![Help Command](./docs/pr-comment-help.png) +![Plan Command](./docs/pr-comment-plan.png) +![Apply Command](./docs/pr-comment-apply.png) + #### `atlantis help` View help -#### `atlantis plan [workspace]` -Runs `terraform plan` for the changes in this pull request. If `[workspace]` is specified, will switch to that workspace, before running `plan`. Any additional arguments passed to `atlantis plan` will be passed on to `terraform plan`. For example if you'd like to run `terraform plan -target={target}` then you can comment `atlantis plan -target={target}`. +#### `atlantis plan [options] -- [terraform plan flags]` +Runs `terraform plan` for the changes in this pull request. + +Options: +* `-d directory` Which directory to run plan in relative to root of repo. Use '.' for root. If not specified, will attempt to run plan for all Terraform projects we think were modified in this changeset. +* -w workspace` Switch to this [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html) before planning. Defaults to 'default'. If not using Terraform workspaces you can ignore this. +* `--verbose` Append Atlantis log to comment. + +Additional Terraform flags: + +If you need to run `terraform plan` with additional arguments, like `-target=resource` or `-var 'foo-bar'` +you can append them to the end of the comment after `--`, ex. +``` +atlantis plan -d dir -- -var 'foo=bar' +``` +If you always need to append a certain flag, see [Project-Specific Customization](#project-specific-customization). + +#### `atlantis apply [options] -- [terraform plan flags]` +Runs `terraform plan` for the changes in this pull request. + +Options: +* `-d directory` Apply the plan for this directory, relative to root of repo. Use '.' for root. If not specified, will run apply against all plans created for this workspace. +* -w workspace` Apply the plan for this [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html). Defaults to 'default'. If not using Terraform workspaces you can ignore this. +* `--verbose` Append Atlantis log to comment. + +Additional Terraform flags: -#### `atlantis apply [workspace]` -Runs `terraform apply` for the plan generated by `atlantis plan`. If `[workspace]` is specified, will switch to that workspace. -Any additional arguments passed to `atlantis apply` will be passed on to `terraform apply`. +Same as with `atlantis plan`. ## Project Structure Atlantis supports several Terraform project structures: @@ -131,23 +157,24 @@ or │   └── staging.tfvars └── main.tf ``` -With the above project structure you can de-duplicate your Terraform code between workspaces/environments without requiring extensive use of modules. At Hootsuite we've found this project format to be very successful and use it in all of our 100+ Terraform repositories. +With the above project structure you can de-duplicate your Terraform code between workspaces/environments without requiring extensive use of modules. At Hootsuite we found this project format to be very successful and use it in all of our 100+ Terraform repositories. ## Workspaces/Environments Terraform introduced [Workspaces](https://www.terraform.io/docs/state/workspaces.html) in 0.9. They allow for > a single directory of Terraform configuration to be used to manage multiple distinct sets of infrastructure resources -If you're using a Terraform version >= 0.9.0, Atlantis supports workspaces through an additional argument to the `atlantis plan` and `atlantis apply` commands. +If you're using a Terraform version >= 0.9.0, Atlantis supports workspaces through the `-w` flag. For example, ``` -atlantis plan staging +atlantis plan -w staging ``` If a workspace is specified, Atlantis will use `terraform workspace select {workspace}` prior to running `terraform plan` or `terraform apply`. If you're using the `env/{env}.tfvars` [project structure](#project-structure) we will also append `-tfvars=env/{env}.tfvars` to `plan` and `apply`. -If no workspace is specified, terraform will use the `default` workspace by default. +If no workspace is specified, we'll use the `default` workspace by default. +This replicates Terraform's default behaviour which also uses the `default` workspace. ## Terraform Versions By default, Atlantis will use the `terraform` executable that is in its path. To use a specific version of Terraform just install that version on the server that Atlantis is running on. @@ -209,13 +236,13 @@ extra_arguments: ``` When running the `pre_plan`, `post_plan`, `pre_apply`, and `post_apply` commands the following environment variables are available -- `WORKSPACE`: if a workspace argument is supplied to `atlantis plan` or `atlantis apply`, ex `atlantis plan staging`, this will +- `WORKSPACE`: if a workspace argument is supplied to `atlantis plan` or `atlantis apply`, ex `atlantis plan -w staging`, this will be the value of that argument. Else it will be `default` - `ATLANTIS_TERRAFORM_VERSION`: local version of `terraform` or the version from `terraform_version` if specified, ex. `0.10.0` - `DIR`: absolute path to the root of the project on disk ## Locking -When `plan` is run, the [project](#project) and [workspace](#workspaceenvironment) are **Locked** until an `apply` succeeds **and** the pull request/merge request is merged. +When `plan` is run, the [project](#project) and [workspace](#workspaceenvironment) (**but not the whole repo**) are **Locked** until an `apply` succeeds **and** the pull request/merge request is merged. This protects against concurrent modifications to the same set of infrastructure and prevents users from seeing a `plan` that will be invalid if another pull request is merged. @@ -463,7 +490,7 @@ A Terraform workspace. See [terraform docs](https://www.terraform.io/docs/state/ ## FAQ **Q: Does Atlantis affect Terraform [remote state](https://www.terraform.io/docs/state/remote.html)?** -A: No. Atlantis does not interfere with Terraform remote state in anyway. Under the hood, Atlantis is simply executing `terraform plan` and `terraform apply`. +A: No. Atlantis does not interfere with Terraform remote state in any way. Under the hood, Atlantis is simply executing `terraform plan` and `terraform apply`. **Q: How does Atlantis locking interact with Terraform [locking](https://www.terraform.io/docs/state/locking.html)?** diff --git a/docs/pr-comment-apply.png b/docs/pr-comment-apply.png new file mode 100644 index 0000000000000000000000000000000000000000..5adc8f049b7b43d12266b5a8df2024e3f4b96567 GIT binary patch literal 19474 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�EGdK&-bHL^xZuxzo7N$&yO6$rw~a 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 eXMz3^pR-#8xp2xlkvy?Kr&iGKkfsB7r} literal 0 HcmV?d00001 diff --git a/docs/pr-comment-plan.png b/docs/pr-comment-plan.png new file mode 100644 index 0000000000000000000000000000000000000000..9233996a80bcab512374df8035f264421427a5a8 GIT binary patch literal 19305 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

yvq%gA)%2Tg6FQ%c% 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 Vv-O7ZgykOw(B6Jt_dLVT{tJD_=#&5e literal 0 HcmV?d00001 diff --git a/server/events/event_parser.go b/server/events/event_parser.go index bf7d76a894..3225e527b7 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -101,7 +101,7 @@ func (e *EventParser) DetermineCommand(comment string, vcsHost vcs.Host) (*Comma name = Apply flagSet = pflag.NewFlagSet("apply", pflag.ContinueOnError) flagSet.StringVarP(&workspace, "workspace", "w", defaultWorkspace, fmt.Sprintf("Apply the plan for this Terraform workspace. Defaults to '%s'", defaultWorkspace)) - flagSet.StringVarP(&dir, "dir", "d", "", "Run apply in this directory relative to root of repo. Use '.' for root. If not specified, will run apply against all plans created for this workspace.") + flagSet.StringVarP(&dir, "dir", "d", "", "Apply the plan for this directory, relative to root of repo. Use '.' for root. If not specified, will run apply against all plans created for this workspace.") flagSet.BoolVarP(&verbose, "verbose", "", false, "Append Atlantis log to comment.") } else { return nil, fmt.Errorf("unknown command %q – this is a bug", command) From 3ec956ea0ac1bfb2fb34f4d15a1ec240b28c9e6c Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 26 Feb 2018 15:15:17 -0800 Subject: [PATCH 4/4] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 07c0bfc467..6f82de9633 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,11 @@ If you're ready to permanently set up Atlantis see [Production-Ready Deployment] Atlantis currently supports three commands that can be run via pull request comments (or merge request comments on GitLab): ![Help Command](./docs/pr-comment-help.png) -![Plan Command](./docs/pr-comment-plan.png) -![Apply Command](./docs/pr-comment-apply.png) - #### `atlantis help` View help +--- +![Plan Command](./docs/pr-comment-plan.png) #### `atlantis plan [options] -- [terraform plan flags]` Runs `terraform plan` for the changes in this pull request. @@ -95,7 +94,9 @@ atlantis plan -d dir -- -var 'foo=bar' ``` If you always need to append a certain flag, see [Project-Specific Customization](#project-specific-customization). -#### `atlantis apply [options] -- [terraform plan flags]` +--- +![Apply Command](./docs/pr-comment-apply.png) +#### `atlantis apply [options] -- [terraform apply flags]` Runs `terraform plan` for the changes in this pull request. Options: