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

feat: allow masking output on comments #4331

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
12 changes: 7 additions & 5 deletions runatlantis.io/docs/custom-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,10 @@ workflows:
value: 'true'
- run:
command: terragrunt plan -input=false -out=$PLANFILE
output: strip_refreshing
output: strip_refreshing_with_custom_regex
# Filters text matching 'mySecret: "aaa"' -> 'mySecret: "<redacted>"'
regex_filter: "((?i)secret:\\s\")[^\"]*"

apply:
steps:
- env:
Expand Down Expand Up @@ -604,17 +607,16 @@ Full
- "--debug"
- "-c"
output: show
custom_regex: .*
```

| Key | Type | Default | Required | Description |
|-----|--------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| run | map\[string -> string\] | none | no | Run a custom command |
| run | map[string -> string] | none | no | Run a custom command |
| run.command | string | none | yes | Shell command to run |
| run.shell | string | "sh" | no | Name of the shell to use for command execution |
| run.shellArgs | string or []string | "-c" | no | Command line arguments to be passed to the shell. Cannot be set without `shell` |
| 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 |

#### Native Environment Variables
::: tip Notes

* `run` steps in the main `workflow` are executed with the following environment variables:
note: these variables are not available to `pre` or `post` workflows
Expand Down
80 changes: 51 additions & 29 deletions server/core/config/raw/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strings"

Expand All @@ -13,23 +14,24 @@ import (
)

const (
ExtraArgsKey = "extra_args"
NameArgKey = "name"
CommandArgKey = "command"
ValueArgKey = "value"
OutputArgKey = "output"
RunStepName = "run"
PlanStepName = "plan"
ShowStepName = "show"
PolicyCheckStepName = "policy_check"
ApplyStepName = "apply"
InitStepName = "init"
EnvStepName = "env"
MultiEnvStepName = "multienv"
ImportStepName = "import"
StateRmStepName = "state_rm"
ShellArgKey = "shell"
ShellArgsArgKey = "shellArgs"
ExtraArgsKey = "extra_args"
NameArgKey = "name"
CommandArgKey = "command"
ValueArgKey = "value"
OutputArgKey = "output"
OutputRegexFilterKey = "regex_filter"
RunStepName = "run"
PlanStepName = "plan"
ShowStepName = "show"
PolicyCheckStepName = "policy_check"
ApplyStepName = "apply"
InitStepName = "init"
EnvStepName = "env"
MultiEnvStepName = "multienv"
ImportStepName = "import"
StateRmStepName = "state_rm"
ShellArgKey = "shell"
ShellArgsArgKey = "shellArgs"
)

/*
Expand Down Expand Up @@ -59,6 +61,10 @@ Step represents a single action/command to perform. In YAML, it can be set as
- run:
command: my custom command
output: hide
- run:
command: my custom command
output: custom_regex
regex_filter: .*

3. A map for a built-in command and extra_args:
- plan:
Expand Down Expand Up @@ -249,20 +255,33 @@ func (s Step) Validate() error {
if _, ok := argMap[CommandArgKey].(string); !ok {
return fmt.Errorf("%q step must have a %q key set", stepName, CommandArgKey)
}
delete(argMap, CommandArgKey)
if v, ok := argMap[OutputArgKey].(string); ok {
if stepName == RunStepName && !(v == valid.PostProcessRunOutputShow ||
v == valid.PostProcessRunOutputHide || v == valid.PostProcessRunOutputStripRefreshing) {
return fmt.Errorf("run step %q option must be one of %q, %q, or %q",
OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide,
valid.PostProcessRunOutputStripRefreshing)
} else if stepName == MultiEnvStepName && !(v == valid.PostProcessRunOutputShow ||
v == valid.PostProcessRunOutputHide) {
return fmt.Errorf("multienv step %q option must be %q or %q",
OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide)
delete(args, CommandArgKey)
if v, ok := args[OutputArgKey].(string); ok {
if !valid.MatchesAnyPostProcessRunOutputOptions(v) {
return fmt.Errorf("run step %q option must be one of %q", OutputArgKey, strings.Join(valid.PostProcessRunOutputOptions(), ","))
}
// When output requires regex option
if v == valid.PostProcessRunOutputCustomRegex || v == valid.PostProcessRunOutputStripRefreshingWithCustomRegex {
if regex, ok := args[OutputRegexFilterKey]; ok {
if _, err := regexp.Compile(regex.(string)); err != nil {
return fmt.Errorf("run step %q option with expression %q is not a valid regex: %w", OutputRegexFilterKey, regex, err)
}
delete(args, OutputRegexFilterKey)
} else {
return fmt.Errorf("run step %q option requires %q to be set", OutputArgKey, OutputRegexFilterKey)
}
}
}
delete(argMap, OutputArgKey)
delete(args, OutputArgKey)
if len(args) > 0 {
var argKeys []string
for k := range args {
argKeys = append(argKeys, k)
}
// Sort so tests can be deterministic.
sort.Strings(argKeys)
return fmt.Errorf("run steps only support keys %q, %q and %q, found extra keys %q", RunStepName, CommandArgKey, OutputArgKey, strings.Join(argKeys, ","))
}
default:
return fmt.Errorf("%q is not a valid step type", stepName)
}
Expand Down Expand Up @@ -343,6 +362,9 @@ func (s Step) ToValid() valid.Step {
if output, ok := stepArgs[OutputArgKey].(string); ok {
step.Output = valid.PostProcessRunOutputOption(output)
}
if outputRegexFilter, ok := stepArgs[OutputRegexFilterKey].(string); ok {
step.OutputRegexFilter = outputRegexFilter
}
if shell, ok := stepArgs[ShellArgKey].(string); ok {
step.RunShell = &valid.CommandShell{
Shell: shell,
Expand Down
18 changes: 18 additions & 0 deletions server/core/config/raw/step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,24 @@ func TestStep_ToValid(t *testing.T) {
Output: "hide",
},
},
{
description: "run step with regex",
input: raw.Step{
CommandMap: RunType{
"run": {
"command": "my 'run command'",
"output": "regex_filter",
"regex_filter": ".*",
},
},
},
exp: valid.Step{
StepName: "run",
RunCommand: "my 'run command'",
Output: "regex_filter",
OutputRegexFilter: ".*",
},
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
Expand Down
32 changes: 29 additions & 3 deletions server/core/config/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,35 @@ type Autoplan struct {
type PostProcessRunOutputOption string

const (
PostProcessRunOutputShow = "show"
PostProcessRunOutputHide = "hide"
PostProcessRunOutputStripRefreshing = "strip_refreshing"
PostProcessRunOutputShow = "show"
PostProcessRunOutputHide = "hide"
PostProcessRunOutputStripRefreshing = "strip_refreshing"
PostProcessRunOutputCustomRegex = "custom_regex"
PostProcessRunOutputStripRefreshingWithCustomRegex = "strip_refreshing_with_custom_regex"
)

// PostProcessRunOutputOptions returns the available post processing options
// This list needs to be manually updated
func PostProcessRunOutputOptions() []string {
return []string{
PostProcessRunOutputShow,
PostProcessRunOutputHide,
PostProcessRunOutputStripRefreshing,
PostProcessRunOutputCustomRegex,
PostProcessRunOutputStripRefreshingWithCustomRegex,
}
}

// MatchesAnyPostProcessRunOutputOptions returns true when the input matches any of the available post processing options
func MatchesAnyPostProcessRunOutputOptions(option string) bool {
for _, c := range PostProcessRunOutputOptions() {
if option == c {
return true
}
}
return false
}

type Stage struct {
Steps []Step
}
Expand All @@ -207,6 +231,8 @@ type Step struct {
RunCommand string
// Output is option for post-processing a RunCommand output
Output PostProcessRunOutputOption
// OutputRegexFilter is a required option when post-processing uses a regex filter output
OutputRegexFilter string
// EnvVarName is the name of the
// environment variable that should be set by this step.
EnvVarName string
Expand Down
2 changes: 1 addition & 1 deletion server/core/runtime/env_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (r *EnvStepRunner) Run(
}
// Pass `false` for streamOutput because this isn't interesting to the user reading the build logs
// in the web UI.
res, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, valid.PostProcessRunOutputShow)
res, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, valid.PostProcessRunOutputShow, "")
// Trim newline from res to support running `echo env_value` which has
// a newline. We don't recommend users run echo -n env_value to remove the
// newline because -n doesn't work in the sh shell which is what we use
Expand Down
2 changes: 1 addition & 1 deletion server/core/runtime/multienv_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (r *MultiEnvStepRunner) Run(
envs map[string]string,
postProcessOutput valid.PostProcessRunOutputOption,
) (string, error) {
res, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, postProcessOutput)
res, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, postProcessOutput, "")
if err != nil {
return "", err
}
Expand Down
9 changes: 9 additions & 0 deletions server/core/runtime/plan_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,15 @@ func StripRefreshingFromPlanOutput(output string, tfVersion *version.Version) st
return output
}

func CustomRegexFromPlanOutput(output string, outputFilterRegex string) string {
if outputFilterRegex == "" {
return output
}
// Regex was validated previously
r := regexp.MustCompile(outputFilterRegex)
return r.ReplaceAllString(output, "${1}<redacted>$2")
}

// remoteOpsErr01114 is the error terraform plan will return if this project is
// using TFE remote operations in TF 0.11.15.
var remoteOpsErr01114 = `Error: Saving a generated plan is currently not supported!
Expand Down
85 changes: 85 additions & 0 deletions server/core/runtime/plan_step_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,31 @@ Plan: 0 to add, 0 to change, 1 to destroy.`, output)
}
}

// Test custom regex on output method
func TestCustomRegexFromPlanOutputFromPlanOutput(t *testing.T) {
cases := []struct {
in string
out string
regex string
}{
{
remotePlanOutput,
remotePlanOutput,
"",
},
{
remotePlanOutputSensitive,
remotePlanOutputSensitiveMasked,
`((?i)secret:\s")[^"]*`,
},
}

for _, c := range cases {
output := runtime.CustomRegexFromPlanOutput(c.in, c.regex)
Equals(t, c.out, output)
}
}

type remotePlanMock struct {
// LinesToSend will be sent on the channel.
LinesToSend string
Expand Down Expand Up @@ -603,3 +628,63 @@ Terraform will perform the following actions:


Plan: 0 to add, 0 to change, 1 to destroy.`

var remotePlanOutputSensitive = `Terraform will perform the following actions:

# kubectl_manifest.test[0] will be updated in-place
! resource "kubectl_manifest" "test" {
id = "/apis/argoproj.io/v1alpha1/namespaces/test/applications/test"
name = "test"
! yaml_body = (sensitive value)
! yaml_body_parsed = <<-EOT
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: test
namespace: test
spec:
destination:
namespace: test
server: https://kubernetes.default.svc
project: default
source:
helm:
values: |-
- clientID: "test_id"
- clientSecret: "super_secret_old"
+ clientID: "test_id"
+ clientSecret: "super_secret_new"
EOT
}

Plan: 0 to add, 1 to change, 0 to destroy.`

var remotePlanOutputSensitiveMasked = `Terraform will perform the following actions:

# kubectl_manifest.test[0] will be updated in-place
! resource "kubectl_manifest" "test" {
id = "/apis/argoproj.io/v1alpha1/namespaces/test/applications/test"
name = "test"
! yaml_body = (sensitive value)
! yaml_body_parsed = <<-EOT
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: test
namespace: test
spec:
destination:
namespace: test
server: https://kubernetes.default.svc
project: default
source:
helm:
values: |-
- clientID: "test_id"
- clientSecret: "<redacted>"
+ clientID: "test_id"
+ clientSecret: "<redacted>"
EOT
}

Plan: 0 to add, 1 to change, 0 to destroy.`
7 changes: 6 additions & 1 deletion server/core/runtime/run_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (r *RunStepRunner) Run(
envs map[string]string,
streamOutput bool,
postProcessOutput valid.PostProcessRunOutputOption,
postProcessRegexFilter string,
) (string, error) {
tfVersion := r.DefaultTFVersion
if ctx.TerraformVersion != nil {
Expand Down Expand Up @@ -97,7 +98,11 @@ func (r *RunStepRunner) Run(
case valid.PostProcessRunOutputHide:
return "", nil
case valid.PostProcessRunOutputStripRefreshing:
return output, nil
return StripRefreshingFromPlanOutput(output, tfVersion), nil
case valid.PostProcessRunOutputCustomRegex:
return CustomRegexFromPlanOutput(output, postProcessRegexFilter), nil
case valid.PostProcessRunOutputStripRefreshingWithCustomRegex:
return CustomRegexFromPlanOutput(StripRefreshingFromPlanOutput(output, tfVersion), postProcessRegexFilter), nil
case valid.PostProcessRunOutputShow:
return output, nil
default:
Expand Down
Loading