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: Add Support for Customising the Shell for Pre and Post Workflow Hooks #3451

Merged
merged 13 commits into from
Jul 7, 2023
25 changes: 22 additions & 3 deletions runatlantis.io/docs/post-workflow-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ workflows](custom-workflows.html#custom-run-command) in that they are run
outside of Atlantis commands. Which means they do not surface their output
back to the PR as a comment.

Post workflow hooks also only allow `run` and `description` commands.

[[toc]]

## Usage

Post workflow hooks can only be specified in the Server-Side Repo Config under
`repos` key.
the `repos` key.

## Use Cases

Expand Down Expand Up @@ -45,6 +43,25 @@ repos:
# ...
```

## Customizing the Shell

By default, the commands will be run using the 'sh' shell with an argument of '-c'. This
can be customized using the `shell` and `shellArgs` keys.

Example:

```yaml
repos:
- id: /.*/
post_workflow_hooks:
- run: |
echo 'atlantis.yaml config:'
cat atlantis.yaml
description: atlantis.yaml report
shell: bash
shellArgs: -cv
```

## Reference

### Custom `run` Command
Expand All @@ -60,6 +77,8 @@ command](custom-workflows.html#custom-run-command).
| ----------- | ------ | ------- | -------- | --------------------- |
| run | string | none | no | Run a custom command |
| description | string | none | no | Post hook description |
| shell | string | 'sh' | no | The shell to use for running the command |
| shellArgs | string | '-c' | no | The shell arguments to use for running the command |

::: tip Notes
* `run` commands are executed with the following environment variables:
Expand Down
47 changes: 37 additions & 10 deletions runatlantis.io/docs/pre-workflow-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@ Pre workflow hooks can be defined to run scripts before default or custom
workflows are executed. Pre workflow hooks differ from [custom
workflows](custom-workflows.html#custom-run-command) in several ways.

1. Pre workflow hooks do not require for repository configuration to be
1. Pre workflow hooks do not require the repository configuration to be
present. This can be utilized to [dynamically generate repo configs](pre-workflow-hooks.html#dynamic-repo-config-generation).
2. Pre workflow hooks are run outside of Atlantis commands. Which means
they do not surface their output back to the PR as a comment.
3. Pre workflow hooks only allow `run` and `description` commands.

[[toc]]

## Usage
Pre workflow hooks can only be specified in the Server-Side Repo Config under
`repos` key.

Pre workflow hooks can only be specified in the Server-Side Repo Config under the
`repos` key.
::: tip Note
`pre-workflow-hooks` do not prevent Atlantis from executing its
workflows(`plan`, `apply`) even if a `run` command exits with an error.
:::

## Use Cases

### Dynamic Repo Config Generation
If you want generate your `atlantis.yaml` before Atlantis can parse it. You
can add a `run` command to `pre_workflow_hooks`. Your Repo config will be generated
right before Atlantis can parse it.

To generate the repo `atlantis.yaml` before Atlantis can parse it,
add a `run` command to `pre_workflow_hooks`. Your Repo config will be generated
right before Atlantis parses it.

```yaml
repos:
Expand All @@ -33,17 +35,43 @@ repos:
- run: ./repo-config-generator.sh
description: Generating configs
```

## Customizing the Shell

By default, the command will be run using the 'sh' shell with an argument of '-c'. This
can be customized using the `shell` and `shellArgs` keys.

Example:

```yaml
repos:
- id: /.*/
pre_workflow_hooks:
- run: |
echo "generating atlantis.yaml"
terragrunt-atlantis-config generate --output atlantis.yaml --autoplan --parallel
description: Generating atlantis.yaml
shell: bash
shellArgs: -cv
```

## Reference

### Custom `run` Command
This is very similar to [custom workflow run
command](custom-workflows.html#custom-run-command).

This is very similar to the [custom workflow run
command](custom-workflows.html#custom-run-command).

```yaml
- run: custom-command
```

| Key | Type | Default | Required | Description |
| ----------- | ------ | ------- | -------- | -------------------- |
| run | string | none | no | Run a custom command |
| description | string | none | no | Pre hook description |
| shell | string | 'sh' | no | The shell to use for running the command |
| shellArgs | string | '-c' | no | The shell arguments to use for running the command |

::: tip Notes
* `run` commands are executed with the following environment variables:
Expand All @@ -64,4 +92,3 @@ command](custom-workflows.html#custom-run-command).
* `COMMAND_NAME` - The name of the command that is being executed, i.e. `plan`, `apply` etc.
* `OUTPUT_STATUS_FILE` - An output file to customize the success or failure status. ex. `echo 'failure' > $OUTPUT_STATUS_FILE`.
:::

9 changes: 6 additions & 3 deletions server/controllers/events/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,11 @@ func TestGitHubWorkflow(t *testing.T) {

expNumHooks := len(c.Comments) + 1 - c.ExpParseFailedCount
// Let's verify the pre-workflow hook was called for each comment including the pull request opened event
mockPreWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](), Eq("some dummy command"), Any[string]())
mockPreWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](),
Eq("some dummy command"), Any[string](), Any[string](), Any[string]())
// Let's verify the post-workflow hook was called for each comment including the pull request opened event
mockPostWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](), Eq("some post dummy command"), Any[string]())
mockPostWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](),
Eq("some post dummy command"), Any[string](), Any[string](), Any[string]())

// Now we're ready to verify Atlantis made all the comments back (or
// replies) that we expect. We expect each plan to have 1 comment,
Expand Down Expand Up @@ -794,7 +796,8 @@ func TestSimpleWorkflow_terraformLockFile(t *testing.T) {
}

// Let's verify the pre-workflow hook was called for each comment including the pull request opened event
mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(Any[models.WorkflowHookCommandContext](), Eq("some dummy command"), Any[string]())
mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(Any[models.WorkflowHookCommandContext](),
Eq("some dummy command"), Any[string](), Any[string](), Any[string]())

// Now we're ready to verify Atlantis made all the comments back (or
// replies) that we expect. We expect each plan to have 1 comment,
Expand Down
2 changes: 2 additions & 0 deletions server/core/config/raw/workflow_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ func (s WorkflowHook) ToValid() *valid.WorkflowHook {
StepName: RunStepName,
RunCommand: s.StringVal["run"],
StepDescription: s.StringVal["description"],
Shell: s.StringVal["shell"],
ShellArgs: s.StringVal["shellArgs"],
}
}

Expand Down
2 changes: 2 additions & 0 deletions server/core/config/valid/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ type WorkflowHook struct {
StepName string
RunCommand string
StepDescription string
Shell string
ShellArgs string
}

// DefaultApplyStage is the Atlantis default apply stage.
Expand Down
24 changes: 16 additions & 8 deletions server/core/runtime/mocks/mock_post_workflows_hook_runner.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 16 additions & 8 deletions server/core/runtime/mocks/mock_pre_workflows_hook_runner.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions server/core/runtime/post_workflow_hook_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ import (

//go:generate pegomock generate --package mocks -o mocks/mock_post_workflows_hook_runner.go PostWorkflowHookRunner
type PostWorkflowHookRunner interface {
Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error)
Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error)
}

type DefaultPostWorkflowHookRunner struct {
OutputHandler jobs.ProjectCommandOutputHandler
}

func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error) {
func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) {
outputFilePath := filepath.Join(path, "OUTPUT_STATUS_FILE")

cmd := exec.Command("sh", "-c", command) // #nosec
shellArgsSlice := append(strings.Split(shellArgs, " "), command)
cmd := exec.Command(shell, shellArgsSlice...) // #nosec
cmd.Dir = path

baseEnvVars := os.Environ()
Expand Down Expand Up @@ -58,7 +59,7 @@ func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContex
wh.OutputHandler.SendWorkflowHook(ctx, "\n", true)

if err != nil {
err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out)
err = fmt.Errorf("%s: running %q in %q: \n%s", err, shell+" "+shellArgs+" "+command, path, out)
ctx.Log.Debug("error: %s", err)
return string(out), "", err
}
Expand All @@ -70,12 +71,12 @@ func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContex
var customStatusErr error
customStatusOut, customStatusErr = os.ReadFile(outputFilePath)
if customStatusErr != nil {
err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out)
err = fmt.Errorf("%s: running %q in %q: \n%s", err, shell+" "+shellArgs+" "+command, path, out)
ctx.Log.Debug("error: %s", err)
return string(out), "", err
}
}

ctx.Log.Info("successfully ran %q in %q", command, path)
return outString, strings.Trim(string(customStatusOut), "\n"), nil
ctx.Log.Info("successfully ran %q in %q", shell+" "+shellArgs+" "+command, path)
return string(out), strings.Trim(string(customStatusOut), "\n"), nil
}
Loading