Skip to content

Commit

Permalink
feat: Add pre and post workflow hook status (#2441)
Browse files Browse the repository at this point in the history
* Add pre and post workflow hook status

* fix missing mocks

* fix pre/post workflow hooks tests

* add cmd output to file

* fix typo

* add cmd output to file on pre workflow hooks

* Add hooks output to web UI

* Remove unnecessary dependencies

* Clean the code and make tests work again

* Make lint happy

* Small fixes, create dedicated UUID generator and docs

* Add missing commentary

* Use matchers instead of mocking for tests and fix e2e tests

* Change 'OUTPUT_FILE' to 'OUTPUT_STATUS_FILE'

* Reduce SendWorkflowHook calls on hooks

* Apply suggestions from code review

Co-authored-by: PePe Amengual <[email protected]>
Co-authored-by: nitrocode <[email protected]>
  • Loading branch information
3 people authored Dec 19, 2022
1 parent 3668659 commit cb485f1
Show file tree
Hide file tree
Showing 45 changed files with 1,403 additions and 239 deletions.
11 changes: 7 additions & 4 deletions runatlantis.io/docs/post-workflow-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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` commands.
Post workflow hooks also only allow `run` and `description` commands.

[[toc]]

Expand Down Expand Up @@ -40,6 +40,7 @@ repos:
workflow: myworkflow
post_workflow_hooks:
- run: infracost output --path=/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM-*-infracost.json --format=github-comment --out-file=/tmp/infracost-comment.md
description: Running infracost
# Now report the output as desired, e.g. post to GitHub as a comment.
# ...
```
Expand All @@ -55,9 +56,10 @@ command](custom-workflows.html#custom-run-command).
- run: custom-command
```
| Key | Type | Default | Required | Description |
| --- | ------ | ------- | -------- | -------------------- |
| run | string | none | no | Run a custom command |
| Key | Type | Default | Required | Description |
| ----------- | ------ | ------- | -------- | --------------------- |
| run | string | none | no | Run a custom command |
| description | string | none | no | Post hook description |
::: tip Notes
* `run` commands are executed with the following environment variables:
Expand All @@ -74,4 +76,5 @@ command](custom-workflows.html#custom-run-command).
* `USER_NAME` - Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`.
* `COMMENT_ARGS` - Any additional flags passed in the comment on the pull request. Flags are separated by commas and
every character is escaped, ex. `atlantis plan -- arg1 arg2` will result in `COMMENT_ARGS=\a\r\g\1,\a\r\g\2`.
* `OUTPUT_STATUS_FILE` - An output file to customize the success or failure status. ex. `echo 'failure' > $OUTPUT_STATUS_FILE`.
:::
11 changes: 7 additions & 4 deletions runatlantis.io/docs/pre-workflow-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ workflows](custom-workflows.html#custom-run-command) in several ways.
present. This 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` commands.
3. Pre workflow hooks only allow `run` and `description` commands.

[[toc]]

Expand All @@ -31,6 +31,7 @@ repos:
- id: /.*/
pre_workflow_hooks:
- run: ./repo-config-generator.sh
description: Generating configs
```
## Reference
### Custom `run` Command
Expand All @@ -39,9 +40,10 @@ command](custom-workflows.html#custom-run-command).
```yaml
- run: custom-command
```
| Key | Type | Default | Required | Description |
| --- | ------ | ------- | -------- | -------------------- |
| run | string | none | no | Run a custom command |
| Key | Type | Default | Required | Description |
| ----------- | ------ | ------- | -------- | -------------------- |
| run | string | none | no | Run a custom command |
| description | string | none | no | Pre hook description |

::: tip Notes
* `run` commands are executed with the following environment variables:
Expand All @@ -58,5 +60,6 @@ command](custom-workflows.html#custom-run-command).
* `USER_NAME` - Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`.
* `COMMENT_ARGS` - Any additional flags passed in the comment on the pull request. Flags are separated by commas and
every character is escaped, ex. `atlantis plan -- arg1 arg2` will result in `COMMENT_ARGS=\a\r\g\1,\a\r\g\2`.
* `OUTPUT_STATUS_FILE` - An output file to customize the success or failure status. ex. `echo 'failure' > $OUTPUT_STATUS_FILE`.
:::

8 changes: 8 additions & 0 deletions server/controllers/events/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -966,22 +966,30 @@ func setupE2E(t *testing.T, repoDir, repoConfigFile string) (events_controllers.
parallelPoolSize := 1
silenceNoProjects := false

commitStatusUpdater := mocks.NewMockCommitStatusUpdater()

mockPreWorkflowHookRunner = runtimemocks.NewMockPreWorkflowHookRunner()
preWorkflowHookURLGenerator := mocks.NewMockPreWorkflowHookURLGenerator()
preWorkflowHooksCommandRunner := &events.DefaultPreWorkflowHooksCommandRunner{
VCSClient: e2eVCSClient,
GlobalCfg: globalCfg,
WorkingDirLocker: locker,
WorkingDir: workingDir,
PreWorkflowHookRunner: mockPreWorkflowHookRunner,
CommitStatusUpdater: commitStatusUpdater,
Router: preWorkflowHookURLGenerator,
}

mockPostWorkflowHookRunner = runtimemocks.NewMockPostWorkflowHookRunner()
postWorkflowHookURLGenerator := mocks.NewMockPostWorkflowHookURLGenerator()
postWorkflowHooksCommandRunner := &events.DefaultPostWorkflowHooksCommandRunner{
VCSClient: e2eVCSClient,
GlobalCfg: globalCfg,
WorkingDirLocker: locker,
WorkingDir: workingDir,
PostWorkflowHookRunner: mockPostWorkflowHookRunner,
CommitStatusUpdater: commitStatusUpdater,
Router: postWorkflowHookURLGenerator,
}
statsScope, _, _ := metrics.NewLoggingScope(logger, "atlantis")

Expand Down
11 changes: 4 additions & 7 deletions server/core/config/raw/workflow_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,10 @@ func (s WorkflowHook) Validate() error {
func (s WorkflowHook) ToValid() *valid.WorkflowHook {
// This will trigger in case #4 (see WorkflowHook docs).
if len(s.StringVal) > 0 {
// After validation we assume there's only one key and it's a valid
// step name so we just use the first one.
for _, v := range s.StringVal {
return &valid.WorkflowHook{
StepName: RunStepName,
RunCommand: v,
}
return &valid.WorkflowHook{
StepName: RunStepName,
RunCommand: s.StringVal["run"],
StepDescription: s.StringVal["description"],
}
}

Expand Down
5 changes: 3 additions & 2 deletions server/core/config/valid/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ type MergedProjectCfg struct {

// WorkflowHook is a map of custom run commands to run before or after workflows.
type WorkflowHook struct {
StepName string
RunCommand string
StepName string
RunCommand string
StepDescription string
}

// DefaultApplyStage is the Atlantis default apply stage.
Expand Down

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

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

19 changes: 11 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.

19 changes: 11 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.

57 changes: 40 additions & 17 deletions server/core/runtime/post_workflow_hook_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,43 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/jobs"
)

//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_post_workflows_hook_runner.go PostWorkflowHookRunner
type PostWorkflowHookRunner interface {
Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error)
Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, string, error)
}

type DefaultPostWorkflowHookRunner struct{}
type DefaultPostWorkflowHookRunner struct {
OutputHandler jobs.ProjectCommandOutputHandler
}

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

func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) {
cmd := exec.Command("sh", "-c", command) // #nosec
cmd.Dir = path

baseEnvVars := os.Environ()
customEnvVars := map[string]string{
"BASE_BRANCH_NAME": ctx.Pull.BaseBranch,
"BASE_REPO_NAME": ctx.BaseRepo.Name,
"BASE_REPO_OWNER": ctx.BaseRepo.Owner,
"COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","),
"DIR": path,
"HEAD_BRANCH_NAME": ctx.Pull.HeadBranch,
"HEAD_COMMIT": ctx.Pull.HeadCommit,
"HEAD_REPO_NAME": ctx.HeadRepo.Name,
"HEAD_REPO_OWNER": ctx.HeadRepo.Owner,
"PULL_AUTHOR": ctx.Pull.Author,
"PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num),
"USER_NAME": ctx.User.Username,
"BASE_BRANCH_NAME": ctx.Pull.BaseBranch,
"BASE_REPO_NAME": ctx.BaseRepo.Name,
"BASE_REPO_OWNER": ctx.BaseRepo.Owner,
"COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","),
"DIR": path,
"HEAD_BRANCH_NAME": ctx.Pull.HeadBranch,
"HEAD_COMMIT": ctx.Pull.HeadCommit,
"HEAD_REPO_NAME": ctx.HeadRepo.Name,
"HEAD_REPO_OWNER": ctx.HeadRepo.Owner,
"PULL_AUTHOR": ctx.Pull.Author,
"PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num),
"USER_NAME": ctx.User.Username,
"OUTPUT_STATUS_FILE": outputFilePath,
}

finalEnvVars := baseEnvVars
Expand All @@ -47,8 +54,24 @@ func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContex
if err != nil {
err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out)
ctx.Log.Debug("error: %s", err)
return "", err
return "", "", err
}

// Read the value from the "outputFilePath" file
// to be returned as a custom description.
var customStatusOut []byte
if _, err := os.Stat(outputFilePath); err == nil {
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)
ctx.Log.Debug("error: %s", err)
return "", "", err
}
}

wh.OutputHandler.SendWorkflowHook(ctx, fmt.Sprintf("%s\n", string(out)), true)

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

0 comments on commit cb485f1

Please sign in to comment.