Skip to content

Commit a9c8f94

Browse files
feat: allow masking output on comments
1 parent 57aef30 commit a9c8f94

16 files changed

+267
-89
lines changed

runatlantis.io/docs/custom-workflows.md

+23-18
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ workflows:
4040
extra_args: ["-var-file", "staging.tfvars"]
4141
# NOTE: no need to define the apply stage because it will default
4242
# to the normal apply stage.
43-
43+
4444
production:
4545
plan:
4646
steps:
@@ -147,11 +147,11 @@ workflows:
147147
- run:
148148
command: terraform init -input=false
149149
output: hide
150-
150+
151151
# If you're using workspaces you need to select the workspace using the
152152
# $WORKSPACE environment variable.
153153
- run: terraform workspace select $WORKSPACE
154-
154+
155155
# You MUST output the plan using -out $PLANFILE because Atlantis expects
156156
# plans to be in a specific location.
157157
- run: terraform plan -input=false -refresh -out $PLANFILE
@@ -234,7 +234,7 @@ $ tree --gitignore
234234

235235
1. Container orchestrator (k8s/fargate/ecs/etc) uses the custom docker image of atlantis with `cdktf` installed with
236236
the `--autoplan-file-list` to trigger on `cdk.tf.json` files and `--include-git-untracked-files` set to include the
237-
CDKTF dynamically generated Terraform files in the Atlantis plan.
237+
CDKTF dynamically generated Terraform files in the Atlantis plan.
238238
1. PR branch is pushed up containing `cdktf` code changes.
239239
1. Atlantis checks out the branch in the repo.
240240
1. Atlantis runs the `npm i && cdktf get && cdktf synth` command in the repo root as a step in `pre_workflow_hooks`,
@@ -335,7 +335,10 @@ workflows:
335335
value: 'true'
336336
- run:
337337
command: terragrunt plan -input=false -out=$PLANFILE
338-
output: strip_refreshing
338+
output: strip_refreshing_with_custom_regex
339+
# Filters text matching 'mySecret: "aaa"' -> 'mySecret: "<redacted>"'
340+
regex_filter: "((?i)secret:\\s\")[^\"]*"
341+
339342
apply:
340343
steps:
341344
- env:
@@ -380,7 +383,7 @@ isn't set, Atlantis will use the default plan workflow which is what we want in
380383
* A custom command will only terminate if all output file descriptors are closed.
381384
Therefore a custom command can only be sent to the background (e.g. for an SSH tunnel during
382385
the terraform run) when its output is redirected to a different location. For example, Atlantis
383-
will execute a custom script containing the following code to create a SSH tunnel correctly:
386+
will execute a custom script containing the following code to create a SSH tunnel correctly:
384387
`ssh -f -M -S /tmp/ssh_tunnel -L 3306:database:3306 -N bastion 1>/dev/null 2>&1`. Without
385388
the redirect, the script would block the Atlantis workflow.
386389
:::
@@ -501,20 +504,22 @@ Compact:
501504

502505
Full
503506
```yaml
504-
- run:
507+
- run:
505508
command: custom-command arg1 arg2
506509
output: show
510+
custom_regex: .*
507511
```
508-
| Key | Type | Default | Required | Description |
509-
|-----|--------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
510-
| run | map[string -> string] | none | no | Run a custom command |
511-
| run.command | string | none | yes | Shell command to run |
512-
| run.output | string | "show" | no | How to post-process the output of this command when posted in the PR comment. The options are<br/>* `show` - preserve the full output<br/>* `hide` - hide output from comment (still visible in the real-time streaming output)<br/> * `strip_refreshing` - hide all output up until and including the last line containing "Refreshing...". This matches the behavior of the built-in `plan` command |
512+
| Key | Type | Default | Required | Description |
513+
|-------------------|-----------------------|---------|-----------|-----------------------|
514+
| run | map[string -> string] | none | no | Run a custom command |
515+
| run.command | string | none | yes | Shell command to run |
516+
| run.output | string | "show" | no | How to post-process the output of this command when posted in the PR comment. The options are<br/>* `show` - preserve the full output<br/>* `hide` - hide output from comment (still visible in the real-time streaming output)<br/> * `strip_refreshing` - hide all output up until and including the last line containing "Refreshing...". This matches the behavior of the built-in `plan` command<br/> * `custom_regex` - filters the comment output based on the regex specified on `run.regex_filter` by replacing matched patterns with the text `<redacted`. Note: this filter only applies to the comments posted by Atlantis, the plan output on the URL job is untouched <br/> * `strip_refreshing_with_custom_regex` - applies `strip_refreshing` and `custom_regex` to the output |
517+
| run.custom_regex | string | none | no | Regex filter to be applied to output. Required when `run.output` is `custom_regex` or `strip_refreshing_with_custom_regex` |
513518

514519
::: tip Notes
515-
* `run` steps in the main `workflow` are executed with the following environment variables:
520+
* `run` steps in the main `workflow` are executed with the following environment variables:
516521
note: these variables are not available to `pre` or `post` workflows
517-
* `WORKSPACE` - The Terraform workspace used for this project, ex. `default`.
522+
* `WORKSPACE` - The Terraform workspace used for this project, ex. `default`.
518523
NOTE: if the step is executed before `init` then Atlantis won't have switched to this workspace yet.
519524
* `ATLANTIS_TERRAFORM_VERSION` - The version of Terraform used for this project, ex. `0.11.0`.
520525
* `DIR` - Absolute path to the current directory.
@@ -544,10 +549,10 @@ Full
544549
* A custom command will only terminate if all output file descriptors are closed.
545550
Therefore a custom command can only be sent to the background (e.g. for an SSH tunnel during
546551
the terraform run) when its output is redirected to a different location. For example, Atlantis
547-
will execute a custom script containing the following code to create a SSH tunnel correctly:
552+
will execute a custom script containing the following code to create a SSH tunnel correctly:
548553
`ssh -f -M -S /tmp/ssh_tunnel -L 3306:database:3306 -N bastion 1>/dev/null 2>&1`. Without
549554
the redirect, the script would block the Atlantis workflow.
550-
* If a workflow step returns a non-zero exit code, the workflow will stop.
555+
* If a workflow step returns a non-zero exit code, the workflow will stop.
551556
:::
552557

553558
#### Environment Variable `env` Command
@@ -574,7 +579,7 @@ as the environment variable value.
574579

575580
::: tip Notes
576581
* `env` `command`'s can use any of the built-in environment variables available
577-
to `run` commands.
582+
to `run` commands.
578583
:::
579584

580585
#### Multiple Environment Variables `multienv` Command
@@ -594,5 +599,5 @@ The name-value pairs in the result are added as environment variables if success
594599

595600
::: tip Notes
596601
* `multienv` `command`'s can use any of the built-in environment variables available
597-
to `run` commands.
602+
to `run` commands.
598603
:::

server/core/config/raw/step.go

+40-22
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"regexp"
78
"sort"
89
"strings"
910

@@ -12,21 +13,22 @@ import (
1213
)
1314

1415
const (
15-
ExtraArgsKey = "extra_args"
16-
NameArgKey = "name"
17-
CommandArgKey = "command"
18-
ValueArgKey = "value"
19-
OutputArgKey = "output"
20-
RunStepName = "run"
21-
PlanStepName = "plan"
22-
ShowStepName = "show"
23-
PolicyCheckStepName = "policy_check"
24-
ApplyStepName = "apply"
25-
InitStepName = "init"
26-
EnvStepName = "env"
27-
MultiEnvStepName = "multienv"
28-
ImportStepName = "import"
29-
StateRmStepName = "state_rm"
16+
ExtraArgsKey = "extra_args"
17+
NameArgKey = "name"
18+
CommandArgKey = "command"
19+
ValueArgKey = "value"
20+
OutputArgKey = "output"
21+
OutputRegexFilterKey = "regex_filter"
22+
RunStepName = "run"
23+
PlanStepName = "plan"
24+
ShowStepName = "show"
25+
PolicyCheckStepName = "policy_check"
26+
ApplyStepName = "apply"
27+
InitStepName = "init"
28+
EnvStepName = "env"
29+
MultiEnvStepName = "multienv"
30+
ImportStepName = "import"
31+
StateRmStepName = "state_rm"
3032
)
3133

3234
// Step represents a single action/command to perform. In YAML, it can be set as
@@ -43,6 +45,10 @@ const (
4345
// - run:
4446
// command: my custom command
4547
// output: hide
48+
// - run:
49+
// command: my custom command
50+
// output: custom_regex
51+
// regex_filter: .*
4652
//
4753
// 3. A map for a built-in command and extra_args:
4854
// - plan:
@@ -203,8 +209,19 @@ func (s Step) Validate() error {
203209
}
204210
delete(args, CommandArgKey)
205211
if v, ok := args[OutputArgKey]; ok {
206-
if !(v == valid.PostProcessRunOutputShow || v == valid.PostProcessRunOutputHide || v == valid.PostProcessRunOutputStripRefreshing) {
207-
return fmt.Errorf("run step %q option must be one of %q, %q, or %q", OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide, valid.PostProcessRunOutputStripRefreshing)
212+
if !valid.MatchesAnyPostProcessRunOutputOptions(v) {
213+
return fmt.Errorf("run step %q option must be one of %q", OutputArgKey, strings.Join(valid.PostProcessRunOutputOptions(), ","))
214+
}
215+
// When output requires regex option
216+
if v == valid.PostProcessRunOutputCustomRegex || v == valid.PostProcessRunOutputStripRefreshingWithCustomRegex {
217+
if regex, ok := args[OutputRegexFilterKey]; ok {
218+
if _, err := regexp.Compile(regex); err != nil {
219+
return fmt.Errorf("run step %q option with expression %q is not a valid regex: %w", OutputRegexFilterKey, regex, err)
220+
}
221+
delete(args, OutputRegexFilterKey)
222+
} else {
223+
return fmt.Errorf("run step %q option requires %q to be set", OutputArgKey, OutputRegexFilterKey)
224+
}
208225
}
209226
}
210227
delete(args, OutputArgKey)
@@ -274,11 +291,12 @@ func (s Step) ToValid() valid.Step {
274291
// step name so we just use the first one.
275292
for stepName, stepArgs := range s.EnvOrRun {
276293
step := valid.Step{
277-
StepName: stepName,
278-
EnvVarName: stepArgs[NameArgKey],
279-
RunCommand: stepArgs[CommandArgKey],
280-
EnvVarValue: stepArgs[ValueArgKey],
281-
Output: valid.PostProcessRunOutputOption(stepArgs[OutputArgKey]),
294+
StepName: stepName,
295+
EnvVarName: stepArgs[NameArgKey],
296+
RunCommand: stepArgs[CommandArgKey],
297+
EnvVarValue: stepArgs[ValueArgKey],
298+
Output: valid.PostProcessRunOutputOption(stepArgs[OutputArgKey]),
299+
OutputRegexFilter: stepArgs[OutputRegexFilterKey],
282300
}
283301
if step.StepName == RunStepName && step.Output == "" {
284302
step.Output = valid.PostProcessRunOutputShow

server/core/config/raw/step_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,24 @@ func TestStep_ToValid(t *testing.T) {
574574
Output: "hide",
575575
},
576576
},
577+
{
578+
description: "run step with regex",
579+
input: raw.Step{
580+
EnvOrRun: EnvOrRunType{
581+
"run": {
582+
"command": "my 'run command'",
583+
"output": "regex_filter",
584+
"regex_filter": ".*",
585+
},
586+
},
587+
},
588+
exp: valid.Step{
589+
StepName: "run",
590+
RunCommand: "my 'run command'",
591+
Output: "regex_filter",
592+
OutputRegexFilter: ".*",
593+
},
594+
},
577595
}
578596
for _, c := range cases {
579597
t.Run(c.description, func(t *testing.T) {

server/core/config/valid/repo_cfg.go

+29-3
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,35 @@ type Autoplan struct {
177177
type PostProcessRunOutputOption string
178178

179179
const (
180-
PostProcessRunOutputShow = "show"
181-
PostProcessRunOutputHide = "hide"
182-
PostProcessRunOutputStripRefreshing = "strip_refreshing"
180+
PostProcessRunOutputShow = "show"
181+
PostProcessRunOutputHide = "hide"
182+
PostProcessRunOutputStripRefreshing = "strip_refreshing"
183+
PostProcessRunOutputCustomRegex = "custom_regex"
184+
PostProcessRunOutputStripRefreshingWithCustomRegex = "strip_refreshing_with_custom_regex"
183185
)
184186

187+
// PostProcessRunOutputOptions returns the available post processing options
188+
// This list needs to be manually updated
189+
func PostProcessRunOutputOptions() []string {
190+
return []string{
191+
PostProcessRunOutputShow,
192+
PostProcessRunOutputHide,
193+
PostProcessRunOutputStripRefreshing,
194+
PostProcessRunOutputCustomRegex,
195+
PostProcessRunOutputStripRefreshingWithCustomRegex,
196+
}
197+
}
198+
199+
// MatchesAnyPostProcessRunOutputOptions returns true when the input matches any of the available post processing options
200+
func MatchesAnyPostProcessRunOutputOptions(option string) bool {
201+
for _, c := range PostProcessRunOutputOptions() {
202+
if option == c {
203+
return true
204+
}
205+
}
206+
return false
207+
}
208+
185209
type Stage struct {
186210
Steps []Step
187211
}
@@ -194,6 +218,8 @@ type Step struct {
194218
RunCommand string
195219
// Output is option for post-processing a RunCommand output
196220
Output PostProcessRunOutputOption
221+
// OutputRegexFilter is a required option when post-processing uses a regex filter output
222+
OutputRegexFilter string
197223
// EnvVarName is the name of the
198224
// environment variable that should be set by this step.
199225
EnvVarName string

server/core/runtime/env_step_runner.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (r *EnvStepRunner) Run(ctx command.ProjectContext, command string, value st
2121
}
2222
// Pass `false` for streamOutput because this isn't interesting to the user reading the build logs
2323
// in the web UI.
24-
res, err := r.RunStepRunner.Run(ctx, command, path, envs, false, valid.PostProcessRunOutputShow)
24+
res, err := r.RunStepRunner.Run(ctx, command, path, envs, false, valid.PostProcessRunOutputShow, "")
2525
// Trim newline from res to support running `echo env_value` which has
2626
// a newline. We don't recommend users run echo -n env_value to remove the
2727
// newline because -n doesn't work in the sh shell which is what we use

server/core/runtime/mocks/mock_pull_approved_checker.go

+17-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/core/runtime/multienv_step_runner.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type MultiEnvStepRunner struct {
1717
// Run runs the multienv step command.
1818
// The command must return a json string containing the array of name-value pairs that are being added as extra environment variables
1919
func (r *MultiEnvStepRunner) Run(ctx command.ProjectContext, command string, path string, envs map[string]string) (string, error) {
20-
res, err := r.RunStepRunner.Run(ctx, command, path, envs, false, valid.PostProcessRunOutputShow)
20+
res, err := r.RunStepRunner.Run(ctx, command, path, envs, false, valid.PostProcessRunOutputShow, "")
2121
if err != nil {
2222
return "", err
2323
}

server/core/runtime/plan_step_runner.go

+9
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,15 @@ func StripRefreshingFromPlanOutput(output string, tfVersion *version.Version) st
265265
return output
266266
}
267267

268+
func CustomRegexFromPlanOutput(output string, outputFilterRegex string) string {
269+
if outputFilterRegex == "" {
270+
return output
271+
}
272+
// Regex was validated previously
273+
r := regexp.MustCompile(outputFilterRegex)
274+
return r.ReplaceAllString(output, "${1}<redacted>$2")
275+
}
276+
268277
// remoteOpsErr01114 is the error terraform plan will return if this project is
269278
// using TFE remote operations in TF 0.11.15.
270279
var remoteOpsErr01114 = `Error: Saving a generated plan is currently not supported!

0 commit comments

Comments
 (0)