Skip to content

Commit

Permalink
Atlantis lock discard via VCS comment
Browse files Browse the repository at this point in the history
This successfully deletes the atlantis lock and
allows a competing PR to plan, but doesn not delete
the actual plans, so a user can still apply after
the lock is discarded.
This will be investigated in a follow up commit

Also fixed the basic mark down rendering for the discard command
  • Loading branch information
Paris Morali committed Apr 22, 2020
1 parent fea1891 commit 65923b4
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 22 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ node_modules/
helm/test-values.yaml
*.swp
golangci-lint
atlantis
atlantis
temp.sh
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ docker run --rm -v $(pwd):/go/src/github.com/runatlantis/atlantis -w /go/src/git

## Calling Your Local Atlantis From GitHub
- Create a test terraform repository in your GitHub.
- Create a personal access token for Atlantis. See [Create a GitHub token](https://github.com/runatlantis/atlantis#create-a-github-token).
- Create a personal access token for Atlantis. See [Create a GitHub token](https://github.com/runatlantis/atlantis/tree/master/runatlantis.io/docs/access-credentials.md#generating-an-access-token).
- Start Atlantis in server mode using that token:
```
atlantis server --gh-user <your username> --gh-token <your token> --repo-whitelist <your repo> --gh-webhook-secret <your webhook secret> --log-level debug
Expand All @@ -65,7 +65,7 @@ atlantis server --gh-user <your username> --gh-token <your token> --repo-whiteli
```
ngrok http 4141
```
- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis#add-github-webhook).
- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis/blob/master/runatlantis.io/docs/configuring-webhooks.md#configuring-webhooks).
- Create a pull request and type `atlantis help`. You should see the request in the `ngrok` and Atlantis logs and you should also see Atlantis comment back.

## Code Style
Expand Down
12 changes: 12 additions & 0 deletions server/events/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
projectCmds, err = c.ProjectCommandBuilder.BuildPlanCommands(ctx, cmd)
case models.ApplyCommand:
projectCmds, err = c.ProjectCommandBuilder.BuildApplyCommands(ctx, cmd)
case models.DiscardCommand:
projectCmds, err = c.ProjectCommandBuilder.BuildDiscardCommands(ctx, cmd)
default:
ctx.Log.Err("failed to determine desired command, neither plan nor apply")
return
Expand All @@ -260,6 +262,14 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
c.deletePlans(ctx)
result.PlansDeleted = true
}

// If this was a successful discard command, delete plans anyway
if cmd.Name == models.DiscardCommand && !result.HasErrors() {
c.deletePlans(ctx)
result.PlansDeleted = true
}

// TODO: check here for updating PR with discard
c.updatePull(
ctx,
cmd,
Expand Down Expand Up @@ -348,6 +358,8 @@ func (c *DefaultCommandRunner) runProjectCmds(cmds []models.ProjectCommandContex
res = c.ProjectCommandRunner.Plan(pCmd)
case models.ApplyCommand:
res = c.ProjectCommandRunner.Apply(pCmd)
case models.DiscardCommand:
res = c.ProjectCommandRunner.Discard(pCmd)
}
results = append(results, res)
}
Expand Down
40 changes: 34 additions & 6 deletions server/events/comment_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type CommentBuilder interface {
BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string
// BuildApplyComment builds an apply comment for the specified args.
BuildApplyComment(repoRelDir string, workspace string, project string) string
// BuildDiscardComment builds a discard comment for the specified args.
BuildDiscardComment(repoRelDir string, workspace string, project string, commentArgs []string) string
}

// CommentParser implements CommentParsing
Expand Down Expand Up @@ -157,7 +159,7 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen
}

// Need to have a plan or apply at this point.
if !e.stringInSlice(command, []string{models.PlanCommand.String(), models.ApplyCommand.String()}) {
if !e.stringInSlice(command, []string{models.PlanCommand.String(), models.ApplyCommand.String(), models.DiscardCommand.String()}) {
return CommentParseResult{CommentResponse: fmt.Sprintf("```\nError: unknown command %q.\nRun 'atlantis --help' for usage.\n```", command)}
}

Expand Down Expand Up @@ -186,6 +188,14 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen
flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Apply the plan for this directory, relative to root of repo, ex. 'child/dir'.")
flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Apply the plan for this project. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", yaml.AtlantisYAMLFilename))
flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.")
case models.DiscardCommand.String():
name = models.DiscardCommand
flagSet = pflag.NewFlagSet(models.DiscardCommand.String(), pflag.ContinueOnError)
flagSet.SetOutput(ioutil.Discard)
flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before planning.")
flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan in relative to root of repo, ex. 'child/dir'.")
flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Which project to discard the plan for. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", yaml.AtlantisYAMLFilename))

default:
return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", command)}
}
Expand Down Expand Up @@ -264,6 +274,22 @@ func (e *CommentParser) BuildApplyComment(repoRelDir string, workspace string, p
return fmt.Sprintf("%s %s%s", atlantisExecutable, models.ApplyCommand.String(), flags)
}

// BuildDiscardComment builds discard comment for the specified args.
func (e *CommentParser) BuildDiscardComment(repoRelDir string, workspace string, project string, commentArgs []string) string {
flags := e.buildFlags(repoRelDir, workspace, project)
commentFlags := ""
if len(commentArgs) > 0 {
var flagsWithoutQuotes []string
for _, f := range commentArgs {
f = strings.TrimPrefix(f, "\"")
f = strings.TrimSuffix(f, "\"")
flagsWithoutQuotes = append(flagsWithoutQuotes, f)
}
commentFlags = fmt.Sprintf(" -- %s", strings.Join(flagsWithoutQuotes, " "))
}
return fmt.Sprintf("%s %s%s%s", atlantisExecutable, models.DiscardCommand.String(), flags, commentFlags)
}

func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string) string {
// Add quotes if dir has spaces.
if strings.Contains(repoRelDir, " ") {
Expand Down Expand Up @@ -342,11 +368,13 @@ Examples:
atlantis apply -d . -w staging
Commands:
plan Runs 'terraform plan' for the changes in this pull request.
To plan a specific project, use the -d, -w and -p flags.
apply Runs 'terraform apply' on all unapplied plans from this pull request.
To only apply a specific plan, use the -d, -w and -p flags.
help View help.
plan Runs 'terraform plan' for the changes in this pull request.
To plan a specific project, use the -d, -w and -p flags.
apply Runs 'terraform apply' on all unapplied plans from this pull request.
To only apply a specific plan, use the -d, -w and -p flags.
discard Discards a previous plan as well as the atlantis lock.
To discard a specific plan and atlantis lock use the -d flag.
help View help.
Flags:
-h, --help help for atlantis
Expand Down
1 change: 1 addition & 0 deletions server/events/db/boltdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ func TestPullStatus_UpdateMerge(t *testing.T) {
LockURL: "lock-url",
RePlanCmd: "plan command",
ApplyCmd: "apply command",
DiscardCmd: "discard command",
},
},
})
Expand Down
18 changes: 15 additions & 3 deletions server/events/markdown_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
)

const (
planCommandTitle = "Plan"
applyCommandTitle = "Apply"
planCommandTitle = "Plan"
applyCommandTitle = "Apply"
discardCommandTitle = "Discard"
// maxUnwrappedLines is the maximum number of lines the Terraform output
// can be before we wrap it in an expandable template.
maxUnwrappedLines = 12
Expand Down Expand Up @@ -144,6 +145,8 @@ func (m *MarkdownRenderer) renderProjectResults(results []models.ProjectResult,
resultData.Rendered = m.renderTemplate(applyUnwrappedSuccessTmpl, struct{ Output string }{result.ApplySuccess})
}

} else if result.DiscardSuccess != "" {
resultData.Rendered = m.renderTemplate(discardUnwrappedSuccessTmpl, struct{ Output string }{result.DiscardSuccess})
} else {
resultData.Rendered = "Found no template. This is a bug!"
}
Expand All @@ -156,6 +159,8 @@ func (m *MarkdownRenderer) renderProjectResults(results []models.ProjectResult,
tmpl = singleProjectPlanSuccessTmpl
case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses == 0:
tmpl = singleProjectPlanUnsuccessfulTmpl
case len(resultsTmplData) == 1 && common.Command == discardCommandTitle:
tmpl = singleProjectDiscardTmpl
case len(resultsTmplData) == 1 && common.Command == applyCommandTitle:
tmpl = singleProjectApplyTmpl
case common.Command == planCommandTitle:
Expand Down Expand Up @@ -209,6 +214,8 @@ var singleProjectPlanSuccessTmpl = template.Must(template.New("").Parse(
var singleProjectPlanUnsuccessfulTmpl = template.Must(template.New("").Parse(
"{{$result := index .Results 0}}Ran {{.Command}} for dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n" +
"{{$result.Rendered}}\n" + logTmpl))
var singleProjectDiscardTmpl = template.Must(template.New("").Parse(
"{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n{{$result.Rendered}}\n" + logTmpl))
var multiProjectPlanTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(
"Ran {{.Command}} for {{ len .Results }} projects:\n\n" +
"{{ range $result := .Results }}" +
Expand Down Expand Up @@ -249,7 +256,8 @@ var planSuccessWrappedTmpl = template.Must(template.New("").Parse(
// to do next.
var planNextSteps = "{{ if .PlanWasDeleted }}This plan was not saved because one or more projects failed and automerge requires all plans pass.{{ else }}* :arrow_forward: To **apply** this plan, comment:\n" +
" * `{{.ApplyCmd}}`\n" +
"* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n" +
"* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}}), or comment:\n" +
" * `{{.DiscardCmd}}`\n" +
"* :repeat: To **plan** this project again, comment:\n" +
" * `{{.RePlanCmd}}`{{end}}"
var applyUnwrappedSuccessTmpl = template.Must(template.New("").Parse(
Expand All @@ -262,6 +270,10 @@ var applyWrappedSuccessTmpl = template.Must(template.New("").Parse(
"{{.Output}}\n" +
"```\n" +
"</details>"))
var discardUnwrappedSuccessTmpl = template.Must(template.New("").Parse(
"```diff\n" +
"{{.Output}}\n" +
"```"))
var unwrappedErrTmplText = "**{{.Command}} Error**\n" +
"```\n" +
"{{.Error}}\n" +
Expand Down
26 changes: 18 additions & 8 deletions server/events/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ type ProjectCommandContext struct {
AutoplanEnabled bool
// BaseRepo is the repository that the pull request will be merged into.
BaseRepo Repo
// DiscardCmd is the command that users should run to discard a plan.
// If this is an apply then this will be empty.
DiscardCmd string
// EscapedCommentArgs are the extra arguments that were added to the atlantis
// command, ex. atlantis plan -- -target=resource. We then escape them
// by adding a \ before each character so that they can be used within
Expand Down Expand Up @@ -371,14 +374,15 @@ func SplitRepoFullName(repoFullName string) (owner string, repo string) {

// ProjectResult is the result of executing a plan/apply for a specific project.
type ProjectResult struct {
Command CommandName
RepoRelDir string
Workspace string
Error error
Failure string
PlanSuccess *PlanSuccess
ApplySuccess string
ProjectName string
Command CommandName
RepoRelDir string
Workspace string
Error error
Failure string
PlanSuccess *PlanSuccess
ApplySuccess string
DiscardSuccess string
ProjectName string
}

// CommitStatus returns the vcs commit status of this project result.
Expand Down Expand Up @@ -431,6 +435,8 @@ type PlanSuccess struct {
RePlanCmd string
// ApplyCmd is the command that users should run to apply this plan.
ApplyCmd string
// DiscardCmd is the command that users should run to discard this plan.
DiscardCmd string
// HasDiverged is true if we're using the checkout merge strategy and the
// branch we're merging into has been updated since we cloned and merged
// it.
Expand Down Expand Up @@ -508,6 +514,8 @@ const (
ApplyCommand CommandName = iota
// PlanCommand is a command to run terraform plan.
PlanCommand
// DiscardCommand is a command to discard a previous plan as well as the atlantis lock.
DiscardCommand
// Adding more? Don't forget to update String() below
)

Expand All @@ -518,6 +526,8 @@ func (c CommandName) String() string {
return "apply"
case PlanCommand:
return "plan"
case DiscardCommand:
return "discard"
}
return ""
}
43 changes: 43 additions & 0 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type ProjectCommandBuilder interface {
// comment doesn't specify one project then there may be multiple commands
// to be run.
BuildApplyCommands(ctx *CommandContext, comment *CommentCommand) ([]models.ProjectCommandContext, error)
// BuildDiscardCommands builds project discard commands for ctx and comment. If
// comment doesn't specify one project then there may be multiple commands
// to be run.
BuildDiscardCommands(ctx *CommandContext, comment *CommentCommand) ([]models.ProjectCommandContext, error)
}

// DefaultProjectCommandBuilder implements ProjectCommandBuilder.
Expand Down Expand Up @@ -93,6 +97,15 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, c
return []models.ProjectCommandContext{pac}, err
}

// See ProjectCommandBuilder.BuildDiscardCommands.
func (p *DefaultProjectCommandBuilder) BuildDiscardCommands(ctx *CommandContext, cmd *CommentCommand) ([]models.ProjectCommandContext, error) {
//if !cmd.IsForSpecificProject() {
// return p.buildDiscardAllCommands(ctx, cmd.Flags, cmd.Verbose)
//}
pcc, err := p.buildProjectDiscardCommand(ctx, cmd)
return []models.ProjectCommandContext{pcc}, err
}

// buildPlanAllCommands builds plan contexts for all projects we determine were
// modified in this ctx.
func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) {
Expand Down Expand Up @@ -249,6 +262,35 @@ func (p *DefaultProjectCommandBuilder) buildProjectApplyCommand(ctx *CommandCont
return p.buildProjectCommandCtx(ctx, models.ApplyCommand, cmd.ProjectName, cmd.Flags, repoDir, repoRelDir, workspace, cmd.Verbose)
}

// cmd must be for only one project.
func (p *DefaultProjectCommandBuilder) buildProjectDiscardCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) {
workspace := DefaultWorkspace
if cmd.Workspace != "" {
workspace = cmd.Workspace
}

var pcc models.ProjectCommandContext
ctx.Log.Debug("building plan command")
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, ctx.Pull.Num, workspace)
if err != nil {
return pcc, err
}
defer unlockFn()

ctx.Log.Debug("cloning repository")
repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace)
if err != nil {
return pcc, err
}

repoRelDir := DefaultRepoRelDir
if cmd.RepoRelDir != "" {
repoRelDir = cmd.RepoRelDir
}

return p.buildProjectCommandCtx(ctx, models.PlanCommand, cmd.ProjectName, cmd.Flags, repoDir, repoRelDir, workspace, cmd.Verbose)
}

// buildProjectCommandCtx builds a context for a single project identified
// by the parameters.
func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(
Expand Down Expand Up @@ -406,6 +448,7 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
ProjectName: projCfg.Name,
ApplyRequirements: projCfg.ApplyRequirements,
RePlanCmd: p.CommentBuilder.BuildPlanComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name, commentArgs),
DiscardCmd: p.CommentBuilder.BuildDiscardComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name, commentArgs),
RepoRelDir: projCfg.RepoRelDir,
RepoConfigVersion: projCfg.RepoCfgVersion,
TerraformVersion: projCfg.TerraformVersion,
Expand Down
Loading

0 comments on commit 65923b4

Please sign in to comment.