Skip to content

Commit

Permalink
feat: allow masking output on comments
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel Martinez <gabrielmartinez@sisti.pt>
  • Loading branch information
GMartinez-Sisti committed Nov 10, 2024
1 parent 41017f4 commit dd862aa
Show file tree
Hide file tree
Showing 14 changed files with 229 additions and 59 deletions.
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

0 comments on commit dd862aa

Please sign in to comment.