Skip to content

Commit

Permalink
Add Option to globally disable apply (runatlantis#1230)
Browse files Browse the repository at this point in the history
* Add Option to globally disable apply

Co-authored-by: Gerald Barker <[email protected]>
  • Loading branch information
2 people authored and ilyaluk committed Dec 15, 2020
1 parent a891334 commit e9ce1b9
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 18 deletions.
7 changes: 6 additions & 1 deletion cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const (
DataDirFlag = "data-dir"
DefaultTFVersionFlag = "default-tf-version"
DisableApplyAllFlag = "disable-apply-all"
DisableApplyFlag = "disable-apply"
DisableAutoplanFlag = "disable-autoplan"
DisableMarkdownFoldingFlag = "disable-markdown-folding"
GHHostnameFlag = "gh-hostname"
Expand Down Expand Up @@ -273,7 +274,11 @@ var boolFlags = map[string]boolFlag{
defaultValue: false,
},
DisableApplyAllFlag: {
description: "Disable \"atlantis apply\" command so a specific project/workspace/directory has to be specified for applies.",
description: "Disable \"atlantis apply\" command without any flags (i.e. apply all). A specific project/workspace/directory has to be specified for applies.",
defaultValue: false,
},
DisableApplyFlag: {
description: "Disable all \"atlantis apply\" command regardless of which flags are passed with it.",
defaultValue: false,
},
DisableAutoplanFlag: {
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ var testFlags = map[string]interface{}{
DataDirFlag: "/path",
DefaultTFVersionFlag: "v0.11.0",
DisableApplyAllFlag: true,
DisableApplyFlag: true,
DisableMarkdownFoldingFlag: true,
GHHostnameFlag: "ghhostname",
GHTokenFlag: "token",
Expand Down
6 changes: 6 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ Values are chosen in this order:
Terraform version to default to. Will download to `<data-dir>/bin/terraform<version>`
if not in `PATH`. See [Terraform Versions](terraform-versions.html) for more details.

* ### `--disable-apply`
```bash
atlantis server --disable-apply
```
Disable all \"atlantis apply\" commands, regardless of which flags are passed with it.

* ### `--disable-apply-all`
```bash
atlantis server --disable-apply-all
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 @@ -76,6 +76,7 @@ type DefaultCommandRunner struct {
GitlabMergeRequestGetter GitlabMergeRequestGetter
CommitStatusUpdater CommitStatusUpdater
DisableApplyAll bool
DisableApply bool
DisableAutoplan bool
EventParser EventParsing
MarkdownRenderer *MarkdownRenderer
Expand Down Expand Up @@ -202,6 +203,14 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
log := c.buildLogger(baseRepo.FullName, pullNum)
defer c.logPanics(baseRepo, pullNum, log)

if c.DisableApply && cmd.Name == models.ApplyCommand {
log.Info("ignoring apply command since apply disabled globally")
if err := c.VCSClient.CreateComment(baseRepo, pullNum, applyDisabledComment, models.ApplyCommand.String()); err != nil {
log.Err("unable to comment on pull request: %s", err)
}
return
}

if c.DisableApplyAll && cmd.Name == models.ApplyCommand && !cmd.IsForSpecificProject() {
log.Info("ignoring apply command without flags since apply all is disabled")
if err := c.VCSClient.CreateComment(baseRepo, pullNum, applyAllDisabledComment, models.ApplyCommand.String()); err != nil {
Expand Down Expand Up @@ -609,3 +618,6 @@ var automergeComment = `Automatically merging because all plans have been succes
// are disabled and an apply all command is issued.
var applyAllDisabledComment = "**Error:** Running `atlantis apply` without flags is disabled." +
" You must specify which project to apply via the `-d <dir>`, `-w <workspace>` or `-p <project name>` flags."

// applyDisabledComment is posted when apply commands are disabled globally and an apply command is issued.
var applyDisabledComment = "**Error:** Running `atlantis apply` is disabled."
10 changes: 10 additions & 0 deletions server/events/command_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ func TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) {
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "**Error:** Running `atlantis apply` without flags is disabled. You must specify which project to apply via the `-d <dir>`, `-w <workspace>` or `-p <project name>` flags.", "apply")
}

func TestRunCommentCommand_ApplyDisabled(t *testing.T) {
t.Log("if \"atlantis apply\" is run and this is disabled globally atlantis should" +
" comment saying that this is not allowed")
vcsClient := setup(t)
ch.DisableApply = true
modelPull := models.PullRequest{State: models.OpenPullState}
ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, modelPull.Num, &events.CommentCommand{Name: models.ApplyCommand})
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "**Error:** Running `atlantis apply` is disabled.", "apply")
}

func TestRunCommentCommand_DisableDisableAutoplan(t *testing.T) {
t.Log("if \"DisableAutoplan is true\" are disabled and we are silencing return and do not comment with error")
setup(t)
Expand Down
38 changes: 28 additions & 10 deletions server/events/comment_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@
package events

import (
"bytes"
"fmt"
"github.com/flynn-archive/go-shlex"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/yaml"
"github.com/spf13/pflag"
"io/ioutil"
"net/url"
"path/filepath"
"regexp"
"strings"

"github.com/flynn-archive/go-shlex"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/yaml"
"github.com/spf13/pflag"
"text/template"
)

const (
Expand Down Expand Up @@ -69,6 +70,7 @@ type CommentParser struct {
GitlabUser string
BitbucketUser string
AzureDevopsUser string
ApplyDisabled bool
}

// CommentParseResult describes the result of parsing a comment as a command.
Expand Down Expand Up @@ -147,13 +149,13 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen
// If they've just typed the name of the executable then give them the help
// output.
if len(args) == 1 {
return CommentParseResult{CommentResponse: HelpComment}
return CommentParseResult{CommentResponse: e.HelpComment(e.ApplyDisabled)}
}
command := args[1]

// Help output.
if e.stringInSlice(command, []string{"help", "-h", "--help"}) {
return CommentParseResult{CommentResponse: HelpComment}
return CommentParseResult{CommentResponse: e.HelpComment(e.ApplyDisabled)}
}

// Need to have a plan, apply or unlock at this point.
Expand Down Expand Up @@ -330,9 +332,21 @@ func (e *CommentParser) errMarkdown(errMsg string, command string, flagSet *pfla
return fmt.Sprintf("```\nError: %s.\nUsage of %s:\n%s```", errMsg, command, flagSet.FlagUsagesWrapped(usagesCols))
}

// HelpComment is the comment we add to the pull request when someone runs
// `atlantis help`.
var HelpComment = "```cmake\n" +
func (e *CommentParser) HelpComment(applyDisabled bool) string {
buf := &bytes.Buffer{}
var tmpl = template.Must(template.New("").Parse(helpCommentTemplate))
if err := tmpl.Execute(buf, struct {
ApplyDisabled bool
}{
ApplyDisabled: applyDisabled,
}); err != nil {
return fmt.Sprintf("Failed to render template, this is a bug: %v", err)
}
return buf.String()

}

var helpCommentTemplate = "```cmake\n" +
`atlantis
Terraform Pull Request Automation
Expand All @@ -342,18 +356,22 @@ Usage:
Examples:
# run plan in the root directory passing the -target flag to terraform
atlantis plan -d . -- -target=resource
{{- if not .ApplyDisabled }}
# apply all unapplied plans from this pull request
atlantis apply
# apply the plan for the root directory and staging workspace
atlantis apply -d . -w staging
{{- end }}
Commands:
plan Runs 'terraform plan' for the changes in this pull request.
To plan a specific project, use the -d, -w and -p flags.
{{- if not .ApplyDisabled }}
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.
{{- end }}
unlock Removes all atlantis locks and discards all plans for this PR.
To unlock a specific plan you can use the Atlantis UI.
help View help.
Expand Down
96 changes: 94 additions & 2 deletions server/events/comment_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,25 @@ func TestParse_HelpResponse(t *testing.T) {
}
for _, c := range helpComments {
r := commentParser.Parse(c, models.Github)
Equals(t, events.HelpComment, r.CommentResponse)
Equals(t, commentParser.HelpComment(false), r.CommentResponse)
}
}

func TestParse_HelpResponseWithApplyDisabled(t *testing.T) {
helpComments := []string{
"run",
"atlantis",
"@github-user",
"atlantis help",
"atlantis --help",
"atlantis -h",
"atlantis help something else",
"atlantis help plan",
}
for _, c := range helpComments {
commentParser.ApplyDisabled = true
r := commentParser.Parse(c, models.Github)
Equals(t, commentParser.HelpComment(true), r.CommentResponse)
}
}

Expand Down Expand Up @@ -640,6 +658,80 @@ func TestBuildPlanApplyComment(t *testing.T) {
}
}

func TestCommentParser_HelpComment(t *testing.T) {
cases := []struct {
applyDisabled bool
expectResult string
}{
{
applyDisabled: false,
expectResult: "```cmake\n" +
`atlantis
Terraform Pull Request Automation
Usage:
atlantis <command> [options] -- [terraform options]
Examples:
# run plan in the root directory passing the -target flag to terraform
atlantis plan -d . -- -target=resource
# apply all unapplied plans from this pull request
atlantis apply
# apply the plan for the root directory and staging workspace
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.
unlock Removes all atlantis locks and discards all plans for this PR.
To unlock a specific plan you can use the Atlantis UI.
help View help.
Flags:
-h, --help help for atlantis
Use "atlantis [command] --help" for more information about a command.` +
"\n```",
},
{
applyDisabled: true,
expectResult: "```cmake\n" +
`atlantis
Terraform Pull Request Automation
Usage:
atlantis <command> [options] -- [terraform options]
Examples:
# run plan in the root directory passing the -target flag to terraform
atlantis plan -d . -- -target=resource
Commands:
plan Runs 'terraform plan' for the changes in this pull request.
To plan a specific project, use the -d, -w and -p flags.
unlock Removes all atlantis locks and discards all plans for this PR.
To unlock a specific plan you can use the Atlantis UI.
help View help.
Flags:
-h, --help help for atlantis
Use "atlantis [command] --help" for more information about a command.` +
"\n```",
},
}

for _, c := range cases {
t.Run(fmt.Sprintf("ApplyDisabled: %v", c.applyDisabled), func(t *testing.T) {
Equals(t, commentParser.HelpComment(c.applyDisabled), c.expectResult)
})
}
}

func TestParse_VCSUsername(t *testing.T) {
cp := events.CommentParser{
GithubUser: "gh",
Expand Down Expand Up @@ -676,7 +768,7 @@ func TestParse_VCSUsername(t *testing.T) {
for _, c := range cases {
t.Run(c.vcs.String(), func(t *testing.T) {
r := cp.Parse(fmt.Sprintf("@%s %s", c.user, "help"), c.vcs)
Equals(t, events.HelpComment, r.CommentResponse)
Equals(t, commentParser.HelpComment(false), r.CommentResponse)
})
}
}
Expand Down
15 changes: 10 additions & 5 deletions server/events/markdown_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type MarkdownRenderer struct {
// If we're not configured with a GitLab client, this will be false.
GitlabSupportsCommonMark bool
DisableApplyAll bool
DisableApply bool
DisableMarkdownFolding bool
}

Expand All @@ -48,6 +49,7 @@ type commonData struct {
Log string
PlansDeleted bool
DisableApplyAll bool
DisableApply bool
}

// errData is data about an error response.
Expand All @@ -71,6 +73,7 @@ type resultData struct {
type planSuccessData struct {
models.PlanSuccess
PlanWasDeleted bool
DisableApply bool
}

type projectResultTmplData struct {
Expand All @@ -89,7 +92,8 @@ func (m *MarkdownRenderer) Render(res CommandResult, cmdName models.CommandName,
Verbose: verbose,
Log: log,
PlansDeleted: res.PlansDeleted,
DisableApplyAll: m.DisableApplyAll,
DisableApplyAll: m.DisableApplyAll || m.DisableApply,
DisableApply: m.DisableApply,
}
if res.Error != nil {
return m.renderTemplate(unwrappedErrWithLogTmpl, errData{res.Error.Error(), common})
Expand Down Expand Up @@ -132,9 +136,9 @@ func (m *MarkdownRenderer) renderProjectResults(results []models.ProjectResult,
})
} else if result.PlanSuccess != nil {
if m.shouldUseWrappedTmpl(vcsHost, result.PlanSuccess.TerraformOutput) {
resultData.Rendered = m.renderTemplate(planSuccessWrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted})
resultData.Rendered = m.renderTemplate(planSuccessWrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply})
} else {
resultData.Rendered = m.renderTemplate(planSuccessUnwrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted})
resultData.Rendered = m.renderTemplate(planSuccessUnwrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply})
}
numPlanSuccesses++
} else if result.ApplySuccess != "" {
Expand Down Expand Up @@ -251,8 +255,9 @@ var planSuccessWrappedTmpl = template.Must(template.New("").Parse(

// planNextSteps are instructions appended after successful plans as to what
// 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" +
var planNextSteps = "{{ if .PlanWasDeleted }}This plan was not saved because one or more projects failed and automerge requires all plans pass.{{ else }}" +
"{{ if not .DisableApply }}* :arrow_forward: To **apply** this plan, comment:\n" +
" * `{{.ApplyCmd}}`\n{{end}}" +
"* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n" +
"* :repeat: To **plan** this project again, comment:\n" +
" * `{{.RePlanCmd}}`{{end}}"
Expand Down
Loading

0 comments on commit e9ce1b9

Please sign in to comment.