Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Option to globally disable apply #1230

Merged
merged 4 commits into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -275,7 +276,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 @@ -205,6 +206,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 @@ -612,3 +621,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