From a2f2cfcd2c570812759eebe0b5b256d50d967799 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Tue, 13 Oct 2020 12:06:48 -0700 Subject: [PATCH 01/11] Adding PreWorkflowHooks into global config. --- go.sum | 1 + server/events/command_runner.go | 49 ++++++ server/events/event_parser.go | 19 +++ server/events/models/models.go | 4 + server/events/project_command_builder.go | 18 +++ server/events/yaml/parser_validator_test.go | 1 + server/events/yaml/raw/global_cfg.go | 1 + server/events/yaml/raw/pre_workflow_step.go | 115 ++++++++++++++ .../events/yaml/raw/pre_workflow_step_test.go | 147 ++++++++++++++++++ server/events/yaml/valid/global_cfg.go | 10 +- server/events/yaml/valid/global_cfg_test.go | 1 + server/events_controller.go | 3 + 12 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 server/events/yaml/raw/pre_workflow_step.go create mode 100644 server/events/yaml/raw/pre_workflow_step_test.go diff --git a/go.sum b/go.sum index 4c4dcd5b98..62fa09146d 100644 --- a/go.sum +++ b/go.sum @@ -411,6 +411,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c h1:HjRaKPaiWks0f5tA6ELVF7ZfqSppfPwOEEAvsrKUTO4= golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 05f6949624..285683e1ea 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -42,6 +42,7 @@ type CommandRunner interface { // and then calling the appropriate services to finish executing the command. RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *CommentCommand) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) + RunPreWorkflowHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_github_pull_getter.go GithubPullGetter @@ -113,6 +114,54 @@ type DefaultCommandRunner struct { DeleteLockCommand DeleteLockCommand } +func (c *DefaultCommandRunner) RunPreWorkflowHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) { + if opStarted := c.Drainer.StartOp(); !opStarted { + if commentErr := c.VCSClient.CreateComment(baseRepo, pull.Num, ShutdownComment, models.PreWorkflowHookCommand.String()); commentErr != nil { + c.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr) + } + return + } + defer c.Drainer.OpDone() + + log := c.buildLogger(baseRepo.FullName, pull.Num) + defer c.logPanics(baseRepo, pull.Num, log) + ctx := &CommandContext{ + User: user, + Log: log, + Pull: pull, + HeadRepo: headRepo, + BaseRepo: baseRepo, + } + if !c.validateCtxAndComment(ctx) { + return + } + + projectCmds, err := c.ProjectCommandBuilder.BuildPreWorkflowHookCommands(ctx) + if err != nil { + if statusErr := c.CommitStatusUpdater.UpdateCombined(ctx.BaseRepo, ctx.Pull, models.FailedCommitStatus, models.PreWorkflowHookCommand); statusErr != nil { + ctx.Log.Warn("unable to update commit status: %s", statusErr) + } + + c.updatePull(ctx, PreWorkflowHookCommand{}, CommandResult{Error: err}) + return + } + if len(projectCmds) == 0 { + log.Info("determined there was no pre workflow hooks to run") + return + } + + var result CommandResult + result = c.runProjectCmds(projectCmds, models.PlanCommand) + + c.updatePull(ctx, PreWorkflowHookCommand{}, result) + pullStatus, err := c.updateDB(ctx, ctx.Pull, result.ProjectResults) + if err != nil { + c.Logger.Err("writing results: %s", err) + } + + c.updateCommitStatus(ctx, models.PreWorkflowHookCommand, pullStatus) +} + // RunAutoplanCommand runs plan when a pull request is opened or updated. func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) { if opStarted := c.Drainer.StartOp(); !opStarted { diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 90d63b0926..dd227643ba 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -43,6 +43,25 @@ type PullCommand interface { IsAutoplan() bool } +// PreWorkflowHookCommand is a command that runs pre workflow commands specified +// in the server config when pull request is opened or updated. +type PreWorkflowHookCommand struct{} + +// CommandName is Plan. +func (c PreWorkflowHookCommand) CommandName() models.CommandName { + return models.PreWorkflowHookCommand +} + +// IsVerbose is false for autoplan commands. +func (c PreWorkflowHookCommand) IsVerbose() bool { + return false +} + +// IsAutoplan is false for non autoplan commands. +func (c PreWorkflowHookCommand) IsAutoplan() bool { + return false +} + // AutoplanCommand is a plan command that is automatically triggered when a // pull request is opened or updated. type AutoplanCommand struct{} diff --git a/server/events/models/models.go b/server/events/models/models.go index 533f8097db..584c81cb84 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -521,6 +521,8 @@ const ( // UnlockCommand is a command to discard previous plans as well as the atlantis locks. UnlockCommand // Adding more? Don't forget to update String() below + // PreWorkflowHookCommand is a command to run pre workflow steps + PreWorkflowHookCommand ) // String returns the string representation of c. @@ -532,6 +534,8 @@ func (c CommandName) String() string { return "plan" case UnlockCommand: return "unlock" + case PreWorkflowHookCommand: + return "pre_workflow_steps" } return "" } diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 391987fbda..ce63cbe99a 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -36,6 +36,9 @@ const ( // ProjectCommandBuilder builds commands that run on individual projects. type ProjectCommandBuilder interface { + // BuildPreWorkflowHookCommands build project commands that will run before + // any other commands. + BuildPreWorkflowHookCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) // BuildAutoplanCommands builds project commands that will run plan on // the projects determined to be modified. BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) @@ -64,6 +67,15 @@ type DefaultProjectCommandBuilder struct { SkipCloneNoChanges bool } +func (p *DefaultProjectCommandBuilder) BuildPreWorkflowHookCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) { + projCtxs, err := p.buildPreWorkflowHookCommands(ctx, nil, false) + if err != nil { + return nil, err + } + + return projCtxs, nil +} + // See ProjectCommandBuilder.BuildAutoplanCommands. func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) { projCtxs, err := p.buildPlanAllCommands(ctx, nil, false) @@ -99,6 +111,11 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, c return []models.ProjectCommandContext{pac}, err } +func (p *DefaultProjectCommandBuilder) buildPreWorkflowHookCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { + + return []models.ProjectCommandContext{}, nil +} + // buildPlanAllCommands builds plan contexts for all projects we determine were // modified in this ctx. func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { @@ -151,6 +168,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, if err != nil { return nil, err } + // Parse config file if it exists. hasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir) if err != nil { diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index 27310d20a5..0ecb1b12cc 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -1023,6 +1023,7 @@ workflows: input: ` repos: - id: github.com/owner/repo + apply_requirements: [approved, mergeable] workflow: custom1 allowed_overrides: [apply_requirements, workflow] diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go index dc2fbd9be0..fdfb82af08 100644 --- a/server/events/yaml/raw/global_cfg.go +++ b/server/events/yaml/raw/global_cfg.go @@ -20,6 +20,7 @@ type GlobalCfg struct { type Repo struct { ID string `yaml:"id" json:"id"` ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` + PreWorkflowHooks []string `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"` AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` diff --git a/server/events/yaml/raw/pre_workflow_step.go b/server/events/yaml/raw/pre_workflow_step.go new file mode 100644 index 0000000000..2ac1656db8 --- /dev/null +++ b/server/events/yaml/raw/pre_workflow_step.go @@ -0,0 +1,115 @@ +package raw + +import ( + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + + validation "github.com/go-ozzo/ozzo-validation" + "github.com/runatlantis/atlantis/server/events/yaml/valid" +) + +// PreWorkflowHook represents a single action/command to perform. In YAML, +// it can be set as +// A map for a custom run commands: +// - run: my custom command +type PreWorkflowHook struct { + StringVal map[string]string +} + +func (s *PreWorkflowHook) UnmarshalYAML(unmarshal func(interface{}) error) error { + return s.unmarshalGeneric(unmarshal) +} + +func (s PreWorkflowHook) MarshalYAML() (interface{}, error) { + return s.marshalGeneric() +} + +func (s *PreWorkflowHook) UnmarshalJSON(data []byte) error { + return s.unmarshalGeneric(func(i interface{}) error { + return json.Unmarshal(data, i) + }) +} + +func (s *PreWorkflowHook) MarshalJSON() ([]byte, error) { + out, err := s.marshalGeneric() + if err != nil { + return nil, err + } + return json.Marshal(out) +} + +func (s PreWorkflowHook) Validate() error { + runStep := func(value interface{}) error { + elem := value.(map[string]string) + var keys []string + for k := range elem { + keys = append(keys, k) + } + // Sort so tests can be deterministic. + sort.Strings(keys) + + if len(keys) > 1 { + return fmt.Errorf("step element can only contain a single key, found %d: %s", + len(keys), strings.Join(keys, ",")) + } + for stepName := range elem { + if stepName != RunStepName { + return fmt.Errorf("%q is not a valid step type", stepName) + } + } + return nil + } + + if len(s.StringVal) > 0 { + return validation.Validate(s.StringVal, validation.By(runStep)) + } + return errors.New("step element is empty") +} + +func (s PreWorkflowHook) ToValid() valid.PreWorkflowHook { + // This will trigger in case #4 (see PreWorkflowHook 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.PreWorkflowHook{ + StepName: RunStepName, + RunCommand: v, + } + } + } + + panic("step was not valid. This is a bug!") +} + +// unmarshalGeneric is used by UnmarshalJSON and UnmarshalYAML to unmarshal +// a step a custom run step: " - run: my custom command" +// It takes a parameter unmarshal that is a function that tries to unmarshal +// the current element into a given object. +func (s *PreWorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error { + // Try to unmarshal as a custom run step, ex. + // repo_config: + // - run: my command + // We validate if the key is run later. + var runStep map[string]string + err := unmarshal(&runStep) + if err == nil { + s.StringVal = runStep + return nil + } + + return err +} + +func (s PreWorkflowHook) marshalGeneric() (interface{}, error) { + if len(s.StringVal) != 0 { + return s.StringVal, nil + } + + // empty step should be marshalled to null, although this is generally + // unexpected behavior. + return nil, nil +} diff --git a/server/events/yaml/raw/pre_workflow_step_test.go b/server/events/yaml/raw/pre_workflow_step_test.go new file mode 100644 index 0000000000..6f6e8668a3 --- /dev/null +++ b/server/events/yaml/raw/pre_workflow_step_test.go @@ -0,0 +1,147 @@ +package raw_test + +import ( + "testing" + + "github.com/runatlantis/atlantis/server/events/yaml/raw" + "github.com/runatlantis/atlantis/server/events/yaml/valid" + . "github.com/runatlantis/atlantis/testing" + yaml "gopkg.in/yaml.v2" +) + +func TestPreWorkflowHook_YAMLMarshalling(t *testing.T) { + cases := []struct { + description string + input string + exp raw.PreWorkflowHook + expErr string + }{ + // Run-step style + { + description: "run step", + input: ` +run: my command`, + exp: raw.PreWorkflowHook{ + StringVal: map[string]string{ + "run": "my command", + }, + }, + }, + { + description: "run step multiple top-level keys", + input: ` +run: my command +key: value`, + exp: raw.PreWorkflowHook{ + StringVal: map[string]string{ + "run": "my command", + "key": "value", + }, + }, + }, + + // Errors + { + description: "extra args style no slice strings", + input: ` +key: + value: + another: map`, + expErr: "yaml: unmarshal errors:\n line 3: cannot unmarshal !!map into string", + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + var got raw.PreWorkflowHook + err := yaml.UnmarshalStrict([]byte(c.input), &got) + if c.expErr != "" { + ErrEquals(t, c.expErr, err) + return + } + Ok(t, err) + Equals(t, c.exp, got) + + _, err = yaml.Marshal(got) + Ok(t, err) + + var got2 raw.PreWorkflowHook + err = yaml.UnmarshalStrict([]byte(c.input), &got2) + Ok(t, err) + Equals(t, got2, got) + }) + } +} + +func TestGlobalConfigStep_Validate(t *testing.T) { + cases := []struct { + description string + input raw.PreWorkflowHook + expErr string + }{ + { + description: "run step", + input: raw.PreWorkflowHook{ + StringVal: map[string]string{ + "run": "my command", + }, + }, + expErr: "", + }, + { + description: "invalid key in string val", + input: raw.PreWorkflowHook{ + StringVal: map[string]string{ + "invalid": "", + }, + }, + expErr: "\"invalid\" is not a valid step type", + }, + { + // For atlantis.yaml v2, this wouldn't parse, but now there should + // be no error. + description: "unparseable shell command", + input: raw.PreWorkflowHook{ + StringVal: map[string]string{ + "run": "my 'c", + }, + }, + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + err := c.input.Validate() + if c.expErr == "" { + Ok(t, err) + return + } + ErrEquals(t, c.expErr, err) + }) + } +} + +func TestPreWorkflowHook_ToValid(t *testing.T) { + cases := []struct { + description string + input raw.PreWorkflowHook + exp valid.PreWorkflowHook + }{ + { + description: "run step", + input: raw.PreWorkflowHook{ + StringVal: map[string]string{ + "run": "my 'run command'", + }, + }, + exp: valid.PreWorkflowHook{ + StepName: "run", + RunCommand: "my 'run command'", + }, + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + Equals(t, c.exp, c.input.ToValid()) + }) + } +} diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index 00e8f4ec06..bfb69de24f 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -12,6 +12,7 @@ import ( const MergeableApplyReq = "mergeable" const ApprovedApplyReq = "approved" const ApplyRequirementsKey = "apply_requirements" +const PreWorkflowHooksKey = "pre_workflow_hooks" const WorkflowKey = "workflow" const AllowedWorkflowsKey = "allowed_workflows" const AllowedOverridesKey = "allowed_overrides" @@ -24,6 +25,12 @@ type GlobalCfg struct { Workflows map[string]Workflow } +// PreWorkflowHook is a map of custom run commands to run before workflows. +type PreWorkflowHook struct { + StepName string + RunCommand string +} + // Repo is the final parsed version of server-side repo config. type Repo struct { // ID is the exact match id of this config. @@ -33,6 +40,7 @@ type Repo struct { // If ID is set then this will be nil. IDRegex *regexp.Regexp ApplyRequirements []string + PreWorkflowHooks []PreWorkflowHook Workflow *Workflow AllowedWorkflows []string AllowedOverrides []string @@ -318,7 +326,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } - for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey} { + for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, PreWorkflowHooksKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { switch key { diff --git a/server/events/yaml/valid/global_cfg_test.go b/server/events/yaml/valid/global_cfg_test.go index e96a0c3d24..85fcedfc14 100644 --- a/server/events/yaml/valid/global_cfg_test.go +++ b/server/events/yaml/valid/global_cfg_test.go @@ -108,6 +108,7 @@ func TestNewGlobalCfg(t *testing.T) { if c.approvedReq { exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "approved") } + Equals(t, exp, act) // Have to hand-compare regexes because Equals doesn't do it. diff --git a/server/events_controller.go b/server/events_controller.go index de08599083..30616884cb 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -343,6 +343,9 @@ func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRep // closed. fmt.Fprintln(w, "Processing...") + e.Logger.Info("running pre workflow hooks") + e.CommandRunner.RunPreWorkflowHooks(baseRepo, headRepo, pull, user) + e.Logger.Info("executing autoplan") if !e.TestingMode { go e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user) From 3995729929077537c686fdfed89a4014f86ed404 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Wed, 14 Oct 2020 15:29:45 -0700 Subject: [PATCH 02/11] Implemented WorkflowHooksCommand to run on RP open --- server/events/command_runner.go | 49 ------------ server/events/event_parser.go | 13 ++-- server/events/models/models.go | 57 +++++++++++++- server/events/project_command_builder.go | 9 +-- server/events/workflow_hooks_runner.go | 41 ++++++++++ server/events/yaml/parser_validator_test.go | 27 +++++-- server/events/yaml/raw/global_cfg.go | 21 +++-- server/events/yaml/raw/pre_workflow_step.go | 24 +++--- .../events/yaml/raw/pre_workflow_step_test.go | 30 +++---- server/events/yaml/valid/global_cfg.go | 13 ++-- server/events_controller.go | 19 +++-- server/server.go | 78 +++++++++++-------- .../test-repos/server-side-cfg/repos.yaml | 2 + 13 files changed, 235 insertions(+), 148 deletions(-) create mode 100644 server/events/workflow_hooks_runner.go diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 285683e1ea..05f6949624 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -42,7 +42,6 @@ type CommandRunner interface { // and then calling the appropriate services to finish executing the command. RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *CommentCommand) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) - RunPreWorkflowHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_github_pull_getter.go GithubPullGetter @@ -114,54 +113,6 @@ type DefaultCommandRunner struct { DeleteLockCommand DeleteLockCommand } -func (c *DefaultCommandRunner) RunPreWorkflowHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) { - if opStarted := c.Drainer.StartOp(); !opStarted { - if commentErr := c.VCSClient.CreateComment(baseRepo, pull.Num, ShutdownComment, models.PreWorkflowHookCommand.String()); commentErr != nil { - c.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr) - } - return - } - defer c.Drainer.OpDone() - - log := c.buildLogger(baseRepo.FullName, pull.Num) - defer c.logPanics(baseRepo, pull.Num, log) - ctx := &CommandContext{ - User: user, - Log: log, - Pull: pull, - HeadRepo: headRepo, - BaseRepo: baseRepo, - } - if !c.validateCtxAndComment(ctx) { - return - } - - projectCmds, err := c.ProjectCommandBuilder.BuildPreWorkflowHookCommands(ctx) - if err != nil { - if statusErr := c.CommitStatusUpdater.UpdateCombined(ctx.BaseRepo, ctx.Pull, models.FailedCommitStatus, models.PreWorkflowHookCommand); statusErr != nil { - ctx.Log.Warn("unable to update commit status: %s", statusErr) - } - - c.updatePull(ctx, PreWorkflowHookCommand{}, CommandResult{Error: err}) - return - } - if len(projectCmds) == 0 { - log.Info("determined there was no pre workflow hooks to run") - return - } - - var result CommandResult - result = c.runProjectCmds(projectCmds, models.PlanCommand) - - c.updatePull(ctx, PreWorkflowHookCommand{}, result) - pullStatus, err := c.updateDB(ctx, ctx.Pull, result.ProjectResults) - if err != nil { - c.Logger.Err("writing results: %s", err) - } - - c.updateCommitStatus(ctx, models.PreWorkflowHookCommand, pullStatus) -} - // RunAutoplanCommand runs plan when a pull request is opened or updated. func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) { if opStarted := c.Drainer.StartOp(); !opStarted { diff --git a/server/events/event_parser.go b/server/events/event_parser.go index dd227643ba..d0c75870e9 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -43,22 +43,23 @@ type PullCommand interface { IsAutoplan() bool } -// PreWorkflowHookCommand is a command that runs pre workflow commands specified +// WorkflowHooksCommand is a command that runs pre workflow commands specified // in the server config when pull request is opened or updated. -type PreWorkflowHookCommand struct{} +type WorkflowHooksCommand struct { +} // CommandName is Plan. -func (c PreWorkflowHookCommand) CommandName() models.CommandName { - return models.PreWorkflowHookCommand +func (c WorkflowHooksCommand) CommandName() models.CommandName { + return models.WorkflowHooksCommand } // IsVerbose is false for autoplan commands. -func (c PreWorkflowHookCommand) IsVerbose() bool { +func (c WorkflowHooksCommand) IsVerbose() bool { return false } // IsAutoplan is false for non autoplan commands. -func (c PreWorkflowHookCommand) IsAutoplan() bool { +func (c WorkflowHooksCommand) IsAutoplan() bool { return false } diff --git a/server/events/models/models.go b/server/events/models/models.go index 584c81cb84..bc7819a167 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -521,8 +521,8 @@ const ( // UnlockCommand is a command to discard previous plans as well as the atlantis locks. UnlockCommand // Adding more? Don't forget to update String() below - // PreWorkflowHookCommand is a command to run pre workflow steps - PreWorkflowHookCommand + // WorkflowHooksCommand is a command to run pre workflow steps + WorkflowHooksCommand ) // String returns the string representation of c. @@ -534,8 +534,59 @@ func (c CommandName) String() string { return "plan" case UnlockCommand: return "unlock" - case PreWorkflowHookCommand: + case WorkflowHooksCommand: return "pre_workflow_steps" } return "" } + +// WorkflowHookCommandContext defines the context for a plan or apply stage that will +// be executed for a project. +type WorkflowHookCommandContext struct { + // BaseRepo is the repository that the pull request will be merged into. + BaseRepo Repo + // HeadRepo is the repository that is getting merged into the BaseRepo. + // If the pull request branch is from the same repository then HeadRepo will + // be the same as BaseRepo. + HeadRepo Repo + // Log is a logger that's been set up for this context. + Log *logging.SimpleLogger + // PullMergeable is true if the pull request for this project is able to be merged. + PullMergeable bool + // Pull is the pull request we're responding to. + Pull PullRequest + // RepoRelDir is the directory of this project relative to the repo root. + RepoRelDir string + // Steps are the sequence of commands we need to run for this project and this + // stage. + Steps []valid.Step + // User is the user that triggered this command. + User User + // Verbose is true when the user would like verbose output. + Verbose bool +} + +// WorkflowHookCommandResult is the result of executing a plan/apply for a specific project. +type WorkflowHookCommandResult struct { + Command CommandName + RepoRelDir string + Error error + Failure string + Success bool +} + +// CommitStatus returns the vcs commit status of this project result. +func (w WorkflowHookCommandResult) CommitStatus() CommitStatus { + if w.Error != nil { + return FailedCommitStatus + } + if w.Failure != "" { + return FailedCommitStatus + } + return SuccessCommitStatus +} + +// IsSuccessful returns true if this project result had no errors. +func (w WorkflowHookCommandResult) IsSuccessful() bool { + return w.Success +} diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index ce63cbe99a..2573ec91bd 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -36,9 +36,6 @@ const ( // ProjectCommandBuilder builds commands that run on individual projects. type ProjectCommandBuilder interface { - // BuildPreWorkflowHookCommands build project commands that will run before - // any other commands. - BuildPreWorkflowHookCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) // BuildAutoplanCommands builds project commands that will run plan on // the projects determined to be modified. BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) @@ -67,8 +64,8 @@ type DefaultProjectCommandBuilder struct { SkipCloneNoChanges bool } -func (p *DefaultProjectCommandBuilder) BuildPreWorkflowHookCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) { - projCtxs, err := p.buildPreWorkflowHookCommands(ctx, nil, false) +func (p *DefaultProjectCommandBuilder) BuildWorkflowHooksCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) { + projCtxs, err := p.buildWorkflowHooksCommands(ctx, nil, false) if err != nil { return nil, err } @@ -111,7 +108,7 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, c return []models.ProjectCommandContext{pac}, err } -func (p *DefaultProjectCommandBuilder) buildPreWorkflowHookCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { +func (p *DefaultProjectCommandBuilder) buildWorkflowHooksCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { return []models.ProjectCommandContext{}, nil } diff --git a/server/events/workflow_hooks_runner.go b/server/events/workflow_hooks_runner.go new file mode 100644 index 0000000000..131fde8177 --- /dev/null +++ b/server/events/workflow_hooks_runner.go @@ -0,0 +1,41 @@ +package events + +import ( + "github.com/runatlantis/atlantis/server/events/db" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs" + "github.com/runatlantis/atlantis/server/events/yaml/valid" + "github.com/runatlantis/atlantis/server/logging" +) + +type WorkflowHookRunner interface { + Run(ctx models.WorkflowHookCommandContext) +} + +type WorkflowHooksCommandRunner interface { + RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) +} + +// DefaultWorkflowHooksCommandRunner is the first step when processing a workflow hook commands. +type DefaultWorkflowHooksCommandRunner struct { + VCSClient vcs.Client + GithubPullGetter GithubPullGetter + AzureDevopsPullGetter AzureDevopsPullGetter + GitlabMergeRequestGetter GitlabMergeRequestGetter + Logger logging.SimpleLogging + WorkingDir WorkingDir + GlobalCfg valid.GlobalCfg + DB *db.BoltDB + Drainer *Drainer + DeleteLockCommand DeleteLockCommand +} + +func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( + baseRepo models.Repo, + headRepo models.Repo, + pull models.PullRequest, + user models.User, +) { + w.Logger.Info("Running Pre Hooks for repo: %s and pr: %d", baseRepo.ID(), pull.Num) + return +} diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index 0ecb1b12cc..b44766a413 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -885,6 +885,12 @@ func TestParseGlobalCfg_NotExist(t *testing.T) { func TestParseGlobalCfg(t *testing.T) { defaultCfg := valid.NewGlobalCfg(false, false, false) + emptyWorkflowHooks := make([]valid.WorkflowHook, 0) + workflowHook := valid.WorkflowHook{ + StepName: "run", + RunCommand: "custom workflow command", + } + customWorkflow1 := valid.Workflow{ Name: "custom1", Plan: valid.Stage{ @@ -1025,11 +1031,14 @@ repos: - id: github.com/owner/repo apply_requirements: [approved, mergeable] + pre_workflow_hooks: + - run: custom workflow command workflow: custom1 allowed_overrides: [apply_requirements, workflow] allow_custom_workflows: true - id: /.*/ - + pre_workflow_hooks: + - run: custom workflow command workflows: custom1: plan: @@ -1049,12 +1058,14 @@ workflows: { ID: "github.com/owner/repo", ApplyRequirements: []string{"approved", "mergeable"}, + WorkflowHooks: &[]valid.WorkflowHook{workflowHook}, Workflow: &customWorkflow1, AllowedOverrides: []string{"apply_requirements", "workflow"}, AllowCustomWorkflows: Bool(true), }, { - IDRegex: regexp.MustCompile(".*"), + IDRegex: regexp.MustCompile(".*"), + WorkflowHooks: &[]valid.WorkflowHook{workflowHook}, }, }, Workflows: map[string]valid.Workflow{ @@ -1072,7 +1083,8 @@ repos: Repos: []valid.Repo{ defaultCfg.Repos[0], { - IDRegex: regexp.MustCompile("github.com/"), + IDRegex: regexp.MustCompile("github.com/"), + WorkflowHooks: &emptyWorkflowHooks, }, }, Workflows: map[string]valid.Workflow{ @@ -1090,8 +1102,9 @@ repos: Repos: []valid.Repo{ defaultCfg.Repos[0], { - ID: "github.com/owner/repo", - Workflow: defaultCfg.Repos[0].Workflow, + ID: "github.com/owner/repo", + WorkflowHooks: &emptyWorkflowHooks, + Workflow: defaultCfg.Repos[0].Workflow, }, }, Workflows: map[string]valid.Workflow{ @@ -1114,6 +1127,7 @@ workflows: { IDRegex: regexp.MustCompile(".*"), ApplyRequirements: []string{}, + WorkflowHooks: &emptyWorkflowHooks, Workflow: &valid.Workflow{ Name: "default", Apply: valid.Stage{ @@ -1161,6 +1175,7 @@ workflows: Ok(t, ioutil.WriteFile(path, []byte(c.input), 0600)) act, err := r.ParseGlobalCfg(path, valid.NewGlobalCfg(false, false, false)) + if c.expErr != "" { expErr := strings.Replace(c.expErr, "", path, -1) ErrEquals(t, expErr, err) @@ -1182,6 +1197,7 @@ workflows: // Test that if we pass in JSON strings everything should parse fine. func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { + emptyWorkflowHooks := make([]valid.WorkflowHook, 0) customWorkflow := valid.Workflow{ Name: "custom", Plan: valid.Stage{ @@ -1262,6 +1278,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { { IDRegex: regexp.MustCompile(".*"), ApplyRequirements: []string{"mergeable", "approved"}, + WorkflowHooks: &emptyWorkflowHooks, Workflow: &customWorkflow, AllowedWorkflows: []string{"custom"}, AllowedOverrides: []string{"workflow", "apply_requirements"}, diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go index fdfb82af08..803a3e4107 100644 --- a/server/events/yaml/raw/global_cfg.go +++ b/server/events/yaml/raw/global_cfg.go @@ -18,13 +18,12 @@ type GlobalCfg struct { // Repo is the raw schema for repos in the server-side repo config. type Repo struct { - ID string `yaml:"id" json:"id"` - ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` - PreWorkflowHooks []string `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` - Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` - AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"` - AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` - AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` + ID string `yaml:"id" json:"id"` + ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` + WorkflowHooks []WorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` + Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` + AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` + AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` } func (g GlobalCfg) Validate() error { @@ -171,10 +170,18 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo { workflow = &ptr } + workflowHooks := make([]valid.WorkflowHook, 0) + if len(r.WorkflowHooks) > 0 { + for _, hook := range r.WorkflowHooks { + workflowHooks = append(workflowHooks, hook.ToValid()) + } + } + return valid.Repo{ ID: id, IDRegex: idRegex, ApplyRequirements: r.ApplyRequirements, + WorkflowHooks: &workflowHooks, Workflow: workflow, AllowedWorkflows: r.AllowedWorkflows, AllowedOverrides: r.AllowedOverrides, diff --git a/server/events/yaml/raw/pre_workflow_step.go b/server/events/yaml/raw/pre_workflow_step.go index 2ac1656db8..34b2421c1b 100644 --- a/server/events/yaml/raw/pre_workflow_step.go +++ b/server/events/yaml/raw/pre_workflow_step.go @@ -11,29 +11,29 @@ import ( "github.com/runatlantis/atlantis/server/events/yaml/valid" ) -// PreWorkflowHook represents a single action/command to perform. In YAML, +// WorkflowHook represents a single action/command to perform. In YAML, // it can be set as // A map for a custom run commands: // - run: my custom command -type PreWorkflowHook struct { +type WorkflowHook struct { StringVal map[string]string } -func (s *PreWorkflowHook) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (s *WorkflowHook) UnmarshalYAML(unmarshal func(interface{}) error) error { return s.unmarshalGeneric(unmarshal) } -func (s PreWorkflowHook) MarshalYAML() (interface{}, error) { +func (s WorkflowHook) MarshalYAML() (interface{}, error) { return s.marshalGeneric() } -func (s *PreWorkflowHook) UnmarshalJSON(data []byte) error { +func (s *WorkflowHook) UnmarshalJSON(data []byte) error { return s.unmarshalGeneric(func(i interface{}) error { return json.Unmarshal(data, i) }) } -func (s *PreWorkflowHook) MarshalJSON() ([]byte, error) { +func (s *WorkflowHook) MarshalJSON() ([]byte, error) { out, err := s.marshalGeneric() if err != nil { return nil, err @@ -41,7 +41,7 @@ func (s *PreWorkflowHook) MarshalJSON() ([]byte, error) { return json.Marshal(out) } -func (s PreWorkflowHook) Validate() error { +func (s WorkflowHook) Validate() error { runStep := func(value interface{}) error { elem := value.(map[string]string) var keys []string @@ -69,13 +69,13 @@ func (s PreWorkflowHook) Validate() error { return errors.New("step element is empty") } -func (s PreWorkflowHook) ToValid() valid.PreWorkflowHook { - // This will trigger in case #4 (see PreWorkflowHook docs). +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.PreWorkflowHook{ + return valid.WorkflowHook{ StepName: RunStepName, RunCommand: v, } @@ -89,7 +89,7 @@ func (s PreWorkflowHook) ToValid() valid.PreWorkflowHook { // a step a custom run step: " - run: my custom command" // It takes a parameter unmarshal that is a function that tries to unmarshal // the current element into a given object. -func (s *PreWorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error { +func (s *WorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error { // Try to unmarshal as a custom run step, ex. // repo_config: // - run: my command @@ -104,7 +104,7 @@ func (s *PreWorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) er return err } -func (s PreWorkflowHook) marshalGeneric() (interface{}, error) { +func (s WorkflowHook) marshalGeneric() (interface{}, error) { if len(s.StringVal) != 0 { return s.StringVal, nil } diff --git a/server/events/yaml/raw/pre_workflow_step_test.go b/server/events/yaml/raw/pre_workflow_step_test.go index 6f6e8668a3..5245bb1ca6 100644 --- a/server/events/yaml/raw/pre_workflow_step_test.go +++ b/server/events/yaml/raw/pre_workflow_step_test.go @@ -9,11 +9,11 @@ import ( yaml "gopkg.in/yaml.v2" ) -func TestPreWorkflowHook_YAMLMarshalling(t *testing.T) { +func TestWorkflowHook_YAMLMarshalling(t *testing.T) { cases := []struct { description string input string - exp raw.PreWorkflowHook + exp raw.WorkflowHook expErr string }{ // Run-step style @@ -21,7 +21,7 @@ func TestPreWorkflowHook_YAMLMarshalling(t *testing.T) { description: "run step", input: ` run: my command`, - exp: raw.PreWorkflowHook{ + exp: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my command", }, @@ -32,7 +32,7 @@ run: my command`, input: ` run: my command key: value`, - exp: raw.PreWorkflowHook{ + exp: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my command", "key": "value", @@ -53,7 +53,7 @@ key: for _, c := range cases { t.Run(c.description, func(t *testing.T) { - var got raw.PreWorkflowHook + var got raw.WorkflowHook err := yaml.UnmarshalStrict([]byte(c.input), &got) if c.expErr != "" { ErrEquals(t, c.expErr, err) @@ -65,7 +65,7 @@ key: _, err = yaml.Marshal(got) Ok(t, err) - var got2 raw.PreWorkflowHook + var got2 raw.WorkflowHook err = yaml.UnmarshalStrict([]byte(c.input), &got2) Ok(t, err) Equals(t, got2, got) @@ -76,12 +76,12 @@ key: func TestGlobalConfigStep_Validate(t *testing.T) { cases := []struct { description string - input raw.PreWorkflowHook + input raw.WorkflowHook expErr string }{ { description: "run step", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my command", }, @@ -90,7 +90,7 @@ func TestGlobalConfigStep_Validate(t *testing.T) { }, { description: "invalid key in string val", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "invalid": "", }, @@ -101,7 +101,7 @@ func TestGlobalConfigStep_Validate(t *testing.T) { // For atlantis.yaml v2, this wouldn't parse, but now there should // be no error. description: "unparseable shell command", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my 'c", }, @@ -120,20 +120,20 @@ func TestGlobalConfigStep_Validate(t *testing.T) { } } -func TestPreWorkflowHook_ToValid(t *testing.T) { +func TestWorkflowHook_ToValid(t *testing.T) { cases := []struct { description string - input raw.PreWorkflowHook - exp valid.PreWorkflowHook + input raw.WorkflowHook + exp valid.WorkflowHook }{ { description: "run step", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my 'run command'", }, }, - exp: valid.PreWorkflowHook{ + exp: valid.WorkflowHook{ StepName: "run", RunCommand: "my 'run command'", }, diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index bfb69de24f..3f1504f802 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -12,7 +12,7 @@ import ( const MergeableApplyReq = "mergeable" const ApprovedApplyReq = "approved" const ApplyRequirementsKey = "apply_requirements" -const PreWorkflowHooksKey = "pre_workflow_hooks" +const WorkflowHooksKey = "pre_workflow_hooks" const WorkflowKey = "workflow" const AllowedWorkflowsKey = "allowed_workflows" const AllowedOverridesKey = "allowed_overrides" @@ -25,8 +25,8 @@ type GlobalCfg struct { Workflows map[string]Workflow } -// PreWorkflowHook is a map of custom run commands to run before workflows. -type PreWorkflowHook struct { +// WorkflowHook is a map of custom run commands to run before workflows. +type WorkflowHook struct { StepName string RunCommand string } @@ -40,7 +40,7 @@ type Repo struct { // If ID is set then this will be nil. IDRegex *regexp.Regexp ApplyRequirements []string - PreWorkflowHooks []PreWorkflowHook + WorkflowHooks *[]WorkflowHook Workflow *Workflow AllowedWorkflows []string AllowedOverrides []string @@ -96,7 +96,7 @@ func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) Global // we treat nil slices differently. applyReqs := []string{} allowedOverrides := []string{} - allowedWorkflows := []string{} + workflowHooks := []WorkflowHook{} if mergeableReq { applyReqs = append(applyReqs, MergeableApplyReq) } @@ -115,6 +115,7 @@ func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) Global { IDRegex: regexp.MustCompile(".*"), ApplyRequirements: applyReqs, + WorkflowHooks: &workflowHooks, Workflow: &defaultWorkflow, AllowedWorkflows: allowedWorkflows, AllowedOverrides: allowedOverrides, @@ -326,7 +327,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } - for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, PreWorkflowHooksKey} { + for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, WorkflowHooksKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { switch key { diff --git a/server/events_controller.go b/server/events_controller.go index 30616884cb..4d73ab3144 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -45,12 +45,13 @@ const bitbucketServerSignatureHeader = "X-Hub-Signature" // EventsController handles all webhook requests which signify 'events' in the // VCS host, ex. GitHub. type EventsController struct { - CommandRunner events.CommandRunner - PullCleaner events.PullCleaner - Logger *logging.SimpleLogger - Parser events.EventParsing - CommentParser events.CommentParsing - ApplyDisabled bool + WorkflowHooksCommandRunner events.WorkflowHooksCommandRunner + CommandRunner events.CommandRunner + PullCleaner events.PullCleaner + Logger *logging.SimpleLogger + Parser events.EventParsing + CommentParser events.CommentParsing + ApplyDisabled bool // GithubWebhookSecret is the secret added to this webhook via the GitHub // UI that identifies this call as coming from GitHub. If empty, no // request validation is done. @@ -344,7 +345,7 @@ func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRep fmt.Fprintln(w, "Processing...") e.Logger.Info("running pre workflow hooks") - e.CommandRunner.RunPreWorkflowHooks(baseRepo, headRepo, pull, user) + e.WorkflowHooksCommandRunner.RunPreHooks(baseRepo, headRepo, pull, user) e.Logger.Info("executing autoplan") if !e.TestingMode { @@ -439,6 +440,10 @@ func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo mo return } + // TODO: run pre workflow hooks + // e.Logger.Info("running pre workflow hooks") + // e.WorkflowHooksCommandRunner.RunPreHooks(baseRepo, headRepo, pull, user) + e.Logger.Debug("executing command") fmt.Fprintln(w, "Processing...") if !e.TestingMode { diff --git a/server/server.go b/server/server.go index 5e8ee13c2d..560cece892 100644 --- a/server/server.go +++ b/server/server.go @@ -66,22 +66,23 @@ const ( // Server runs the Atlantis web server. type Server struct { - AtlantisVersion string - AtlantisURL *url.URL - Router *mux.Router - Port int - CommandRunner *events.DefaultCommandRunner - Logger *logging.SimpleLogger - Locker locking.Locker - EventsController *EventsController - GithubAppController *GithubAppController - LocksController *LocksController - StatusController *StatusController - IndexTemplate TemplateWriter - LockDetailTemplate TemplateWriter - SSLCertFile string - SSLKeyFile string - Drainer *events.Drainer + AtlantisVersion string + AtlantisURL *url.URL + Router *mux.Router + Port int + WorkflowHooksCommandRunner *events.DefaultWorkflowHooksCommandRunner + CommandRunner *events.DefaultCommandRunner + Logger *logging.SimpleLogger + Locker locking.Locker + EventsController *EventsController + GithubAppController *GithubAppController + LocksController *LocksController + StatusController *StatusController + IndexTemplate TemplateWriter + LockDetailTemplate TemplateWriter + SSLCertFile string + SSLKeyFile string + Drainer *events.Drainer } // Config holds config for server that isn't passed in by the user. @@ -355,6 +356,18 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Logger: logger, Drainer: drainer, } + workflowHooksCommandRunner := &events.DefaultWorkflowHooksCommandRunner{ + VCSClient: vcsClient, + GithubPullGetter: githubClient, + GitlabMergeRequestGetter: gitlabClient, + AzureDevopsPullGetter: azuredevopsClient, + GlobalCfg: globalCfg, + Logger: logger, + WorkingDir: workingDir, + DB: boltdb, + Drainer: drainer, + DeleteLockCommand: deleteLockCommand, + } commandRunner := &events.DefaultCommandRunner{ VCSClient: vcsClient, GithubPullGetter: githubClient, @@ -464,22 +477,23 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } return &Server{ - AtlantisVersion: config.AtlantisVersion, - AtlantisURL: parsedURL, - Router: underlyingRouter, - Port: userConfig.Port, - CommandRunner: commandRunner, - Logger: logger, - Locker: lockingClient, - EventsController: eventsController, - GithubAppController: githubAppController, - LocksController: locksController, - StatusController: statusController, - IndexTemplate: indexTemplate, - LockDetailTemplate: lockTemplate, - SSLKeyFile: userConfig.SSLKeyFile, - SSLCertFile: userConfig.SSLCertFile, - Drainer: drainer, + AtlantisVersion: config.AtlantisVersion, + AtlantisURL: parsedURL, + Router: underlyingRouter, + Port: userConfig.Port, + WorkflowHooksCommandRunner: workflowHooksCommandRunner, + CommandRunner: commandRunner, + Logger: logger, + Locker: lockingClient, + EventsController: eventsController, + GithubAppController: githubAppController, + LocksController: locksController, + StatusController: statusController, + IndexTemplate: indexTemplate, + LockDetailTemplate: lockTemplate, + SSLKeyFile: userConfig.SSLKeyFile, + SSLCertFile: userConfig.SSLCertFile, + Drainer: drainer, }, nil } diff --git a/server/testfixtures/test-repos/server-side-cfg/repos.yaml b/server/testfixtures/test-repos/server-side-cfg/repos.yaml index 28e38d9121..2b1d564d47 100644 --- a/server/testfixtures/test-repos/server-side-cfg/repos.yaml +++ b/server/testfixtures/test-repos/server-side-cfg/repos.yaml @@ -1,5 +1,7 @@ repos: - id: /.*/ + workflow_hooks: + - run: echo "hello" workflow: custom allowed_overrides: [workflow] workflows: From bb454359cd52dd2aa79fe3e229b68882fee597a1 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Wed, 14 Oct 2020 17:56:52 -0700 Subject: [PATCH 03/11] POC finished for running an arbitrary script before running autoplan --- server/events/models/models.go | 9 +- server/events/project_command_builder.go | 14 -- server/events/workflow_hooks_runner.go | 127 ++++++++++++++++-- server/events/yaml/parser_validator_test.go | 19 ++- server/events/yaml/raw/global_cfg.go | 4 +- server/events/yaml/raw/pre_workflow_step.go | 4 +- .../events/yaml/raw/pre_workflow_step_test.go | 4 +- server/events/yaml/valid/global_cfg.go | 6 +- server/server.go | 2 + 9 files changed, 140 insertions(+), 49 deletions(-) diff --git a/server/events/models/models.go b/server/events/models/models.go index bc7819a167..74b16918a1 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -535,7 +535,7 @@ func (c CommandName) String() string { case UnlockCommand: return "unlock" case WorkflowHooksCommand: - return "pre_workflow_steps" + return "pre_workflow_hooks" } return "" } @@ -551,15 +551,8 @@ type WorkflowHookCommandContext struct { HeadRepo Repo // Log is a logger that's been set up for this context. Log *logging.SimpleLogger - // PullMergeable is true if the pull request for this project is able to be merged. - PullMergeable bool // Pull is the pull request we're responding to. Pull PullRequest - // RepoRelDir is the directory of this project relative to the repo root. - RepoRelDir string - // Steps are the sequence of commands we need to run for this project and this - // stage. - Steps []valid.Step // User is the user that triggered this command. User User // Verbose is true when the user would like verbose output. diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 2573ec91bd..9341ce8acc 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -64,15 +64,6 @@ type DefaultProjectCommandBuilder struct { SkipCloneNoChanges bool } -func (p *DefaultProjectCommandBuilder) BuildWorkflowHooksCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) { - projCtxs, err := p.buildWorkflowHooksCommands(ctx, nil, false) - if err != nil { - return nil, err - } - - return projCtxs, nil -} - // See ProjectCommandBuilder.BuildAutoplanCommands. func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) { projCtxs, err := p.buildPlanAllCommands(ctx, nil, false) @@ -108,11 +99,6 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, c return []models.ProjectCommandContext{pac}, err } -func (p *DefaultProjectCommandBuilder) buildWorkflowHooksCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { - - return []models.ProjectCommandContext{}, nil -} - // buildPlanAllCommands builds plan contexts for all projects we determine were // modified in this ctx. func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { diff --git a/server/events/workflow_hooks_runner.go b/server/events/workflow_hooks_runner.go index 131fde8177..68bde56550 100644 --- a/server/events/workflow_hooks_runner.go +++ b/server/events/workflow_hooks_runner.go @@ -1,19 +1,24 @@ package events import ( + "fmt" + "os" + "os/exec" + "github.com/runatlantis/atlantis/server/events/db" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/events/yaml/valid" "github.com/runatlantis/atlantis/server/logging" + "github.com/runatlantis/atlantis/server/recovery" ) -type WorkflowHookRunner interface { - Run(ctx models.WorkflowHookCommandContext) -} +// type WorkflowHookRunner interface { +// Run(ctx models.WorkflowHookCommandContext) +// } type WorkflowHooksCommandRunner interface { - RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) + RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) error } // DefaultWorkflowHooksCommandRunner is the first step when processing a workflow hook commands. @@ -22,7 +27,8 @@ type DefaultWorkflowHooksCommandRunner struct { GithubPullGetter GithubPullGetter AzureDevopsPullGetter AzureDevopsPullGetter GitlabMergeRequestGetter GitlabMergeRequestGetter - Logger logging.SimpleLogging + Logger *logging.SimpleLogger + WorkingDirLocker WorkingDirLocker WorkingDir WorkingDir GlobalCfg valid.GlobalCfg DB *db.BoltDB @@ -35,7 +41,112 @@ func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( headRepo models.Repo, pull models.PullRequest, user models.User, -) { - w.Logger.Info("Running Pre Hooks for repo: %s and pr: %d", baseRepo.ID(), pull.Num) - return +) error { + if opStarted := w.Drainer.StartOp(); !opStarted { + if commentErr := w.VCSClient.CreateComment(baseRepo, pull.Num, ShutdownComment, models.WorkflowHooksCommand.String()); commentErr != nil { + w.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr) + } + return nil + } + defer w.Drainer.OpDone() + + log := w.buildLogger(baseRepo.FullName, pull.Num) + defer w.logPanics(baseRepo, pull.Num, log) + + log.Info("Running Pre Hooks for repo: ") + + unlockFn, err := w.WorkingDirLocker.TryLock(baseRepo.FullName, pull.Num, DefaultWorkspace) + if err != nil { + log.Warn("workspace was locked") + return err + } + log.Debug("got workspace lock") + defer unlockFn() + + repoDir, _, err := w.WorkingDir.Clone(log, baseRepo, headRepo, pull, DefaultWorkspace) + if err != nil { + return err + } + + workflowHooks := make([]*valid.WorkflowHook, 0) + for _, repo := range w.GlobalCfg.Repos { + if repo.IDMatches(baseRepo.ID()) && len(repo.WorkflowHooks) > 0 { + workflowHooks = append(workflowHooks, repo.WorkflowHooks...) + } + } + + ctx := models.WorkflowHookCommandContext{ + BaseRepo: baseRepo, + HeadRepo: headRepo, + Log: log, + Pull: pull, + User: user, + Verbose: false, + } + + for _, hook := range workflowHooks { + _, err := w.runHooks(ctx, hook.RunCommand, repoDir) + if err != nil { + log.Err("executing hooks: %s", err) + return err + } + } + + return nil +} + +func (w *DefaultWorkflowHooksCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { + src := fmt.Sprintf("%s#%d", repoFullName, pullNum) + return w.Logger.NewLogger(src, true, w.Logger.GetLevel()) +} + +// logPanics logs and creates a comment on the pull request for panics. +func (w *DefaultWorkflowHooksCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logger logging.SimpleLogging) { + if err := recover(); err != nil { + stack := recovery.Stack(3) + logger.Err("PANIC: %s\n%s", err, stack) + if commentErr := w.VCSClient.CreateComment( + baseRepo, + pullNum, + fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack), + "", + ); commentErr != nil { + logger.Err("unable to comment: %s", commentErr) + } + } +} + +func (w *DefaultWorkflowHooksCommandRunner) runHooks(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, + "DIR": path, + "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, + "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, + } + + finalEnvVars := baseEnvVars + for key, val := range customEnvVars { + finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val)) + } + + cmd.Env = finalEnvVars + out, err := cmd.CombinedOutput() + + 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 + } + ctx.Log.Info("successfully ran %q in %q", command, path) + return string(out), nil } diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index b44766a413..f2e4695992 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -885,11 +885,11 @@ func TestParseGlobalCfg_NotExist(t *testing.T) { func TestParseGlobalCfg(t *testing.T) { defaultCfg := valid.NewGlobalCfg(false, false, false) - emptyWorkflowHooks := make([]valid.WorkflowHook, 0) - workflowHook := valid.WorkflowHook{ + workflowHook := &valid.WorkflowHook{ StepName: "run", RunCommand: "custom workflow command", } + workflowHooks := []*valid.WorkflowHook{workflowHook} customWorkflow1 := valid.Workflow{ Name: "custom1", @@ -1058,14 +1058,14 @@ workflows: { ID: "github.com/owner/repo", ApplyRequirements: []string{"approved", "mergeable"}, - WorkflowHooks: &[]valid.WorkflowHook{workflowHook}, + WorkflowHooks: workflowHooks, Workflow: &customWorkflow1, AllowedOverrides: []string{"apply_requirements", "workflow"}, AllowCustomWorkflows: Bool(true), }, { IDRegex: regexp.MustCompile(".*"), - WorkflowHooks: &[]valid.WorkflowHook{workflowHook}, + WorkflowHooks: workflowHooks, }, }, Workflows: map[string]valid.Workflow{ @@ -1084,7 +1084,7 @@ repos: defaultCfg.Repos[0], { IDRegex: regexp.MustCompile("github.com/"), - WorkflowHooks: &emptyWorkflowHooks, + WorkflowHooks: []*valid.WorkflowHook{}, }, }, Workflows: map[string]valid.Workflow{ @@ -1103,7 +1103,7 @@ repos: defaultCfg.Repos[0], { ID: "github.com/owner/repo", - WorkflowHooks: &emptyWorkflowHooks, + WorkflowHooks: []*valid.WorkflowHook{}, Workflow: defaultCfg.Repos[0].Workflow, }, }, @@ -1126,8 +1126,8 @@ workflows: Repos: []valid.Repo{ { IDRegex: regexp.MustCompile(".*"), + WorkflowHooks: []*valid.WorkflowHook{}, ApplyRequirements: []string{}, - WorkflowHooks: &emptyWorkflowHooks, Workflow: &valid.Workflow{ Name: "default", Apply: valid.Stage{ @@ -1197,7 +1197,6 @@ workflows: // Test that if we pass in JSON strings everything should parse fine. func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { - emptyWorkflowHooks := make([]valid.WorkflowHook, 0) customWorkflow := valid.Workflow{ Name: "custom", Plan: valid.Stage{ @@ -1278,7 +1277,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { { IDRegex: regexp.MustCompile(".*"), ApplyRequirements: []string{"mergeable", "approved"}, - WorkflowHooks: &emptyWorkflowHooks, + WorkflowHooks: []*valid.WorkflowHook{}, Workflow: &customWorkflow, AllowedWorkflows: []string{"custom"}, AllowedOverrides: []string{"workflow", "apply_requirements"}, @@ -1287,7 +1286,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { { ID: "github.com/owner/repo", IDRegex: nil, - AllowedWorkflows: nil, + WorkflowHooks: []*valid.WorkflowHook{}, ApplyRequirements: nil, AllowedOverrides: nil, AllowCustomWorkflows: nil, diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go index 803a3e4107..776335d7dc 100644 --- a/server/events/yaml/raw/global_cfg.go +++ b/server/events/yaml/raw/global_cfg.go @@ -170,7 +170,7 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo { workflow = &ptr } - workflowHooks := make([]valid.WorkflowHook, 0) + workflowHooks := make([]*valid.WorkflowHook, 0) if len(r.WorkflowHooks) > 0 { for _, hook := range r.WorkflowHooks { workflowHooks = append(workflowHooks, hook.ToValid()) @@ -181,7 +181,7 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo { ID: id, IDRegex: idRegex, ApplyRequirements: r.ApplyRequirements, - WorkflowHooks: &workflowHooks, + WorkflowHooks: workflowHooks, Workflow: workflow, AllowedWorkflows: r.AllowedWorkflows, AllowedOverrides: r.AllowedOverrides, diff --git a/server/events/yaml/raw/pre_workflow_step.go b/server/events/yaml/raw/pre_workflow_step.go index 34b2421c1b..848234a848 100644 --- a/server/events/yaml/raw/pre_workflow_step.go +++ b/server/events/yaml/raw/pre_workflow_step.go @@ -69,13 +69,13 @@ func (s WorkflowHook) Validate() error { return errors.New("step element is empty") } -func (s WorkflowHook) ToValid() valid.WorkflowHook { +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{ + return &valid.WorkflowHook{ StepName: RunStepName, RunCommand: v, } diff --git a/server/events/yaml/raw/pre_workflow_step_test.go b/server/events/yaml/raw/pre_workflow_step_test.go index 5245bb1ca6..9d69ed1df4 100644 --- a/server/events/yaml/raw/pre_workflow_step_test.go +++ b/server/events/yaml/raw/pre_workflow_step_test.go @@ -124,7 +124,7 @@ func TestWorkflowHook_ToValid(t *testing.T) { cases := []struct { description string input raw.WorkflowHook - exp valid.WorkflowHook + exp *valid.WorkflowHook }{ { description: "run step", @@ -133,7 +133,7 @@ func TestWorkflowHook_ToValid(t *testing.T) { "run": "my 'run command'", }, }, - exp: valid.WorkflowHook{ + exp: &valid.WorkflowHook{ StepName: "run", RunCommand: "my 'run command'", }, diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index 3f1504f802..749f4f018a 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -40,7 +40,7 @@ type Repo struct { // If ID is set then this will be nil. IDRegex *regexp.Regexp ApplyRequirements []string - WorkflowHooks *[]WorkflowHook + WorkflowHooks []*WorkflowHook Workflow *Workflow AllowedWorkflows []string AllowedOverrides []string @@ -96,7 +96,7 @@ func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) Global // we treat nil slices differently. applyReqs := []string{} allowedOverrides := []string{} - workflowHooks := []WorkflowHook{} + workflowHooks := make([]*WorkflowHook, 0) if mergeableReq { applyReqs = append(applyReqs, MergeableApplyReq) } @@ -115,7 +115,7 @@ func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) Global { IDRegex: regexp.MustCompile(".*"), ApplyRequirements: applyReqs, - WorkflowHooks: &workflowHooks, + WorkflowHooks: workflowHooks, Workflow: &defaultWorkflow, AllowedWorkflows: allowedWorkflows, AllowedOverrides: allowedOverrides, diff --git a/server/server.go b/server/server.go index 560cece892..08752eada2 100644 --- a/server/server.go +++ b/server/server.go @@ -363,6 +363,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { AzureDevopsPullGetter: azuredevopsClient, GlobalCfg: globalCfg, Logger: logger, + WorkingDirLocker: workingDirLocker, WorkingDir: workingDir, DB: boltdb, Drainer: drainer, @@ -449,6 +450,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { DeleteLockCommand: deleteLockCommand, } eventsController := &EventsController{ + WorkflowHooksCommandRunner: workflowHooksCommandRunner, CommandRunner: commandRunner, PullCleaner: pullClosedExecutor, Parser: eventParser, From ad34d3e35015a7ee5bca5d169a8940d34c60eadc Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Thu, 15 Oct 2020 13:03:25 -0700 Subject: [PATCH 04/11] Wrapping pre workflow hooks into a struct --- server/events/models/models.go | 26 ++---- .../events/workflow_hooks_command_result.go | 20 +++++ server/events/workflow_hooks_runner.go | 85 ++++++++++++------- 3 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 server/events/workflow_hooks_command_result.go diff --git a/server/events/models/models.go b/server/events/models/models.go index 74b16918a1..ec60e16b5f 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -559,27 +559,15 @@ type WorkflowHookCommandContext struct { Verbose bool } -// WorkflowHookCommandResult is the result of executing a plan/apply for a specific project. -type WorkflowHookCommandResult struct { - Command CommandName - RepoRelDir string - Error error - Failure string - Success bool -} - -// CommitStatus returns the vcs commit status of this project result. -func (w WorkflowHookCommandResult) CommitStatus() CommitStatus { - if w.Error != nil { - return FailedCommitStatus - } - if w.Failure != "" { - return FailedCommitStatus - } - return SuccessCommitStatus +// WorkflowHookResult is the result of executing a pre workflow hook for a repository. +type WorkflowHookResult struct { + Command CommandName + Output string + Error error + Success bool } // IsSuccessful returns true if this project result had no errors. -func (w WorkflowHookCommandResult) IsSuccessful() bool { +func (w WorkflowHookResult) IsSuccessful() bool { return w.Success } diff --git a/server/events/workflow_hooks_command_result.go b/server/events/workflow_hooks_command_result.go new file mode 100644 index 0000000000..5ca01d8950 --- /dev/null +++ b/server/events/workflow_hooks_command_result.go @@ -0,0 +1,20 @@ +package events + +import "github.com/runatlantis/atlantis/server/events/models" + +// WorkflowHookCommandResult is the result of executing pre workflow hooks for a +// repository. +type WorkflowHookCommandResult struct { + WorkflowHookResults []models.WorkflowHookResult +} + +// HasErrors returns true if there were any errors during the execution, +// even if it was only in one project. +func (w WorkflowHookCommandResult) HasErrors() bool { + for _, r := range w.WorkflowHookResults { + if !r.IsSuccessful() { + return true + } + } + return false +} diff --git a/server/events/workflow_hooks_runner.go b/server/events/workflow_hooks_runner.go index 68bde56550..1eca17fefa 100644 --- a/server/events/workflow_hooks_runner.go +++ b/server/events/workflow_hooks_runner.go @@ -13,10 +13,6 @@ import ( "github.com/runatlantis/atlantis/server/recovery" ) -// type WorkflowHookRunner interface { -// Run(ctx models.WorkflowHookCommandContext) -// } - type WorkflowHooksCommandRunner interface { RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) error } @@ -41,12 +37,12 @@ func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( headRepo models.Repo, pull models.PullRequest, user models.User, -) error { +) (*WorkflowHookCommandResult, error) { if opStarted := w.Drainer.StartOp(); !opStarted { if commentErr := w.VCSClient.CreateComment(baseRepo, pull.Num, ShutdownComment, models.WorkflowHooksCommand.String()); commentErr != nil { w.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr) } - return nil + return nil, nil } defer w.Drainer.OpDone() @@ -58,14 +54,14 @@ func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( unlockFn, err := w.WorkingDirLocker.TryLock(baseRepo.FullName, pull.Num, DefaultWorkspace) if err != nil { log.Warn("workspace was locked") - return err + return nil, err } log.Debug("got workspace lock") defer unlockFn() repoDir, _, err := w.WorkingDir.Clone(log, baseRepo, headRepo, pull, DefaultWorkspace) if err != nil { - return err + return nil, err } workflowHooks := make([]*valid.WorkflowHook, 0) @@ -84,39 +80,43 @@ func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( Verbose: false, } + outputs := w.runHooks(ctx, workflowHooks, repoDir) + return &WorkflowHookCommandResult{ + WorkflowHookResults: outputs, + }, nil +} + +func (w *DefaultWorkflowHooksCommandRunner) runHooks( + ctx models.WorkflowHookCommandContext, + workflowHooks []*valid.WorkflowHook, + repoDir string, +) (outputs []models.WorkflowHookResult) { for _, hook := range workflowHooks { - _, err := w.runHooks(ctx, hook.RunCommand, repoDir) - if err != nil { - log.Err("executing hooks: %s", err) - return err + out, err := w.runCmd(ctx, hook.RunCommand, repoDir) + + res := models.WorkflowHookResult{ + Command: models.WorkflowHooksCommand, + Output: out, } - } - return nil -} + if err != nil { + res.Error = err + res.Success = false + } else { + res.Success = true + } -func (w *DefaultWorkflowHooksCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { - src := fmt.Sprintf("%s#%d", repoFullName, pullNum) - return w.Logger.NewLogger(src, true, w.Logger.GetLevel()) -} + outputs = append(outputs, res) -// logPanics logs and creates a comment on the pull request for panics. -func (w *DefaultWorkflowHooksCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logger logging.SimpleLogging) { - if err := recover(); err != nil { - stack := recovery.Stack(3) - logger.Err("PANIC: %s\n%s", err, stack) - if commentErr := w.VCSClient.CreateComment( - baseRepo, - pullNum, - fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack), - "", - ); commentErr != nil { - logger.Err("unable to comment: %s", commentErr) + if !res.IsSuccessful() { + return } } + + return } -func (w *DefaultWorkflowHooksCommandRunner) runHooks(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) { +func (w *DefaultWorkflowHooksCommandRunner) runCmd(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) { cmd := exec.Command("sh", "-c", command) // #nosec cmd.Dir = path @@ -150,3 +150,24 @@ func (w *DefaultWorkflowHooksCommandRunner) runHooks(ctx models.WorkflowHookComm ctx.Log.Info("successfully ran %q in %q", command, path) return string(out), nil } + +func (w *DefaultWorkflowHooksCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { + src := fmt.Sprintf("%s#%d", repoFullName, pullNum) + return w.Logger.NewLogger(src, true, w.Logger.GetLevel()) +} + +// logPanics logs and creates a comment on the pull request for panics. +func (w *DefaultWorkflowHooksCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logger logging.SimpleLogging) { + if err := recover(); err != nil { + stack := recovery.Stack(3) + logger.Err("PANIC: %s\n%s", err, stack) + if commentErr := w.VCSClient.CreateComment( + baseRepo, + pullNum, + fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack), + "", + ); commentErr != nil { + logger.Err("unable to comment: %s", commentErr) + } + } +} From 00f22013e9379f7576d0d14d28e9d8594bf34324 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Thu, 15 Oct 2020 16:38:28 -0700 Subject: [PATCH 05/11] update interface --- server/events/workflow_hooks_runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/events/workflow_hooks_runner.go b/server/events/workflow_hooks_runner.go index 1eca17fefa..8652018a22 100644 --- a/server/events/workflow_hooks_runner.go +++ b/server/events/workflow_hooks_runner.go @@ -14,7 +14,7 @@ import ( ) type WorkflowHooksCommandRunner interface { - RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) error + RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) (*WorkflowHookCommandResult, error) } // DefaultWorkflowHooksCommandRunner is the first step when processing a workflow hook commands. From 9aab490247a1104c063bc3c8cc7998cda153dc77 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Fri, 16 Oct 2020 17:59:13 -0700 Subject: [PATCH 06/11] Refactoring some names and adding some tests --- ...tr_to_events_workflowhookscommandresult.go | 20 +++ .../mock_workflows_hooks_command_runner.go | 122 ++++++++++++++++++ server/events/runtime/workflow_hook_runner.go | 46 +++++++ .../events/workflow_hooks_command_result.go | 21 ++- ...er.go => workflow_hooks_command_runner.go} | 67 ++-------- .../workflow_hooks_command_runner_test.go | 86 ++++++++++++ server/events/yaml/valid/global_cfg.go | 12 +- server/events_controller.go | 13 +- server/events_controller_e2e_test.go | 13 +- server/events_controller_test.go | 76 ++++++----- server/server.go | 18 +-- .../test-repos/server-side-cfg/repos.yaml | 2 +- 12 files changed, 383 insertions(+), 113 deletions(-) create mode 100644 server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go create mode 100644 server/events/mocks/mock_workflows_hooks_command_runner.go create mode 100644 server/events/runtime/workflow_hook_runner.go rename server/events/{workflow_hooks_runner.go => workflow_hooks_command_runner.go} (64%) create mode 100644 server/events/workflow_hooks_command_runner_test.go diff --git a/server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go b/server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go new file mode 100644 index 0000000000..138288bc4c --- /dev/null +++ b/server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + events "github.com/runatlantis/atlantis/server/events" +) + +func AnyPtrToEventsWorkflowHooksCommandResult() *events.WorkflowHooksCommandResult { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*events.WorkflowHooksCommandResult))(nil)).Elem())) + var nullValue *events.WorkflowHooksCommandResult + return nullValue +} + +func EqPtrToEventsWorkflowHooksCommandResult(value *events.WorkflowHooksCommandResult) *events.WorkflowHooksCommandResult { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue *events.WorkflowHooksCommandResult + return nullValue +} diff --git a/server/events/mocks/mock_workflows_hooks_command_runner.go b/server/events/mocks/mock_workflows_hooks_command_runner.go new file mode 100644 index 0000000000..cee55845d2 --- /dev/null +++ b/server/events/mocks/mock_workflows_hooks_command_runner.go @@ -0,0 +1,122 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events (interfaces: WorkflowHooksCommandRunner) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + events "github.com/runatlantis/atlantis/server/events" + models "github.com/runatlantis/atlantis/server/events/models" + "reflect" + "time" +) + +type MockWorkflowHooksCommandRunner struct { + fail func(message string, callerSkip ...int) +} + +func NewMockWorkflowHooksCommandRunner(options ...pegomock.Option) *MockWorkflowHooksCommandRunner { + mock := &MockWorkflowHooksCommandRunner{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockWorkflowHooksCommandRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockWorkflowHooksCommandRunner) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockWorkflowHooksCommandRunner) RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) (*events.WorkflowHooksCommandResult, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockWorkflowHooksCommandRunner().") + } + params := []pegomock.Param{baseRepo, headRepo, pull, user} + result := pegomock.GetGenericMockFrom(mock).Invoke("RunPreHooks", params, []reflect.Type{reflect.TypeOf((**events.WorkflowHooksCommandResult)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 *events.WorkflowHooksCommandResult + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(*events.WorkflowHooksCommandResult) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalledOnce() *VerifierMockWorkflowHooksCommandRunner { + return &VerifierMockWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockWorkflowHooksCommandRunner { + return &VerifierMockWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockWorkflowHooksCommandRunner { + return &VerifierMockWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockWorkflowHooksCommandRunner { + return &VerifierMockWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockWorkflowHooksCommandRunner struct { + mock *MockWorkflowHooksCommandRunner + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockWorkflowHooksCommandRunner) RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) *MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification { + params := []pegomock.Param{baseRepo, headRepo, pull, user} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunPreHooks", params, verifier.timeout) + return &MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification struct { + mock *MockWorkflowHooksCommandRunner + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetCapturedArguments() (models.Repo, models.Repo, models.PullRequest, models.User) { + baseRepo, headRepo, pull, user := c.GetAllCapturedArguments() + return baseRepo[len(baseRepo)-1], headRepo[len(headRepo)-1], pull[len(pull)-1], user[len(user)-1] +} + +func (c *MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.User) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.Repo) + } + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(models.Repo) + } + _param2 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range params[2] { + _param2[u] = param.(models.PullRequest) + } + _param3 = make([]models.User, len(c.methodInvocations)) + for u, param := range params[3] { + _param3[u] = param.(models.User) + } + } + return +} diff --git a/server/events/runtime/workflow_hook_runner.go b/server/events/runtime/workflow_hook_runner.go new file mode 100644 index 0000000000..ae27961c28 --- /dev/null +++ b/server/events/runtime/workflow_hook_runner.go @@ -0,0 +1,46 @@ +package runtime + +import ( + "fmt" + "os" + "os/exec" + + "github.com/runatlantis/atlantis/server/events/models" +) + +type WorkflowHookRunner struct{} + +func (wh *WorkflowHookRunner) 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, + "DIR": path, + "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, + "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, + } + + finalEnvVars := baseEnvVars + for key, val := range customEnvVars { + finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val)) + } + + cmd.Env = finalEnvVars + out, err := cmd.CombinedOutput() + + 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 + } + ctx.Log.Info("successfully ran %q in %q", command, path) + return string(out), nil +} diff --git a/server/events/workflow_hooks_command_result.go b/server/events/workflow_hooks_command_result.go index 5ca01d8950..821ad53e30 100644 --- a/server/events/workflow_hooks_command_result.go +++ b/server/events/workflow_hooks_command_result.go @@ -1,16 +1,20 @@ package events -import "github.com/runatlantis/atlantis/server/events/models" +import ( + "strings" -// WorkflowHookCommandResult is the result of executing pre workflow hooks for a + "github.com/runatlantis/atlantis/server/events/models" +) + +// WorkflowHooksCommandResult is the result of executing pre workflow hooks for a // repository. -type WorkflowHookCommandResult struct { +type WorkflowHooksCommandResult struct { WorkflowHookResults []models.WorkflowHookResult } // HasErrors returns true if there were any errors during the execution, // even if it was only in one project. -func (w WorkflowHookCommandResult) HasErrors() bool { +func (w WorkflowHooksCommandResult) HasErrors() bool { for _, r := range w.WorkflowHookResults { if !r.IsSuccessful() { return true @@ -18,3 +22,12 @@ func (w WorkflowHookCommandResult) HasErrors() bool { } return false } + +func (w WorkflowHooksCommandResult) Errors() string { + errors := make([]string, 0) + for _, r := range w.WorkflowHookResults { + errors = append(errors, r.Error.Error()) + } + + return strings.Join(errors, "\n") +} diff --git a/server/events/workflow_hooks_runner.go b/server/events/workflow_hooks_command_runner.go similarity index 64% rename from server/events/workflow_hooks_runner.go rename to server/events/workflow_hooks_command_runner.go index 8652018a22..62424ca1b6 100644 --- a/server/events/workflow_hooks_runner.go +++ b/server/events/workflow_hooks_command_runner.go @@ -2,34 +2,30 @@ package events import ( "fmt" - "os" - "os/exec" - "github.com/runatlantis/atlantis/server/events/db" "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/runtime" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/events/yaml/valid" "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/recovery" ) +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_workflows_hooks_command_runner.go WorkflowHooksCommandRunner + type WorkflowHooksCommandRunner interface { - RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) (*WorkflowHookCommandResult, error) + RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) (*WorkflowHooksCommandResult, error) } // DefaultWorkflowHooksCommandRunner is the first step when processing a workflow hook commands. type DefaultWorkflowHooksCommandRunner struct { - VCSClient vcs.Client - GithubPullGetter GithubPullGetter - AzureDevopsPullGetter AzureDevopsPullGetter - GitlabMergeRequestGetter GitlabMergeRequestGetter - Logger *logging.SimpleLogger - WorkingDirLocker WorkingDirLocker - WorkingDir WorkingDir - GlobalCfg valid.GlobalCfg - DB *db.BoltDB - Drainer *Drainer - DeleteLockCommand DeleteLockCommand + VCSClient vcs.Client + Logger logging.SimpleLogging + WorkingDirLocker WorkingDirLocker + WorkingDir WorkingDir + GlobalCfg valid.GlobalCfg + Drainer *Drainer + WorkflowHookRunner runtime.WorkflowHookRunner } func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( @@ -37,7 +33,7 @@ func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( headRepo models.Repo, pull models.PullRequest, user models.User, -) (*WorkflowHookCommandResult, error) { +) (*WorkflowHooksCommandResult, error) { if opStarted := w.Drainer.StartOp(); !opStarted { if commentErr := w.VCSClient.CreateComment(baseRepo, pull.Num, ShutdownComment, models.WorkflowHooksCommand.String()); commentErr != nil { w.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr) @@ -81,7 +77,7 @@ func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( } outputs := w.runHooks(ctx, workflowHooks, repoDir) - return &WorkflowHookCommandResult{ + return &WorkflowHooksCommandResult{ WorkflowHookResults: outputs, }, nil } @@ -92,7 +88,7 @@ func (w *DefaultWorkflowHooksCommandRunner) runHooks( repoDir string, ) (outputs []models.WorkflowHookResult) { for _, hook := range workflowHooks { - out, err := w.runCmd(ctx, hook.RunCommand, repoDir) + out, err := w.WorkflowHookRunner.Run(ctx, hook.RunCommand, repoDir) res := models.WorkflowHookResult{ Command: models.WorkflowHooksCommand, @@ -116,41 +112,6 @@ func (w *DefaultWorkflowHooksCommandRunner) runHooks( return } -func (w *DefaultWorkflowHooksCommandRunner) runCmd(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, - "DIR": path, - "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, - "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, - } - - finalEnvVars := baseEnvVars - for key, val := range customEnvVars { - finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val)) - } - - cmd.Env = finalEnvVars - out, err := cmd.CombinedOutput() - - 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 - } - ctx.Log.Info("successfully ran %q in %q", command, path) - return string(out), nil -} - func (w *DefaultWorkflowHooksCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { src := fmt.Sprintf("%s#%d", repoFullName, pullNum) return w.Logger.NewLogger(src, true, w.Logger.GetLevel()) diff --git a/server/events/workflow_hooks_command_runner_test.go b/server/events/workflow_hooks_command_runner_test.go new file mode 100644 index 0000000000..3c96d259b1 --- /dev/null +++ b/server/events/workflow_hooks_command_runner_test.go @@ -0,0 +1,86 @@ +package events_test + +import ( + "fmt" + "strings" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/matchers" + "github.com/runatlantis/atlantis/server/events/mocks" + "github.com/runatlantis/atlantis/server/events/models/fixtures" + vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" + "github.com/runatlantis/atlantis/server/logging" + logmocks "github.com/runatlantis/atlantis/server/logging/mocks" + . "github.com/runatlantis/atlantis/testing" +) + +var wh events.DefaultWorkflowHooksCommandRunner +var whWorkingDir *mocks.MockWorkingDir +var whWorkingDirLocker *mocks.MockWorkingDirLocker +var whDrainer *events.Drainer + +func workflow_hooks_setup(t *testing.T) *vcsmocks.MockClient { + RegisterMockTestingT(t) + vcsClient := vcsmocks.NewMockClient() + logger := logmocks.NewMockSimpleLogging() + whWorkingDir = mocks.NewMockWorkingDir() + whWorkingDirLocker = mocks.NewMockWorkingDirLocker() + whDrainer = &events.Drainer{} + + wh = events.DefaultWorkflowHooksCommandRunner{ + VCSClient: vcsClient, + Logger: logger, + WorkingDirLocker: whWorkingDirLocker, + WorkingDir: whWorkingDir, + Drainer: whDrainer, + } + return vcsClient +} + +func TestWorkflowHooksCommand_LogPanics(t *testing.T) { + t.Log("if there is a panic it is commented back on the pull request") + vcsClient := workflow_hooks_setup(t) + logger := wh.Logger.NewLogger("log", false, logging.LogLevel(1)) + + When(whWorkingDir.Clone( + logger, + fixtures.GithubRepo, + fixtures.GithubRepo, + fixtures.Pull, + events.DefaultWorkspace, + )).ThenPanic("panic test - if you're seeing this in a test failure this isn't the failing test") + + wh.RunPreHooks(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) + _, _, comment, _ := vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()).GetCapturedArguments() + Assert(t, strings.Contains(comment, "Error: goroutine panic"), fmt.Sprintf("comment should be about a goroutine panic but was %q", comment)) +} + +// Test that if one plan fails and we are using automerge, that +// we delete the plans. +func TestRunPreHooks_Clone(t *testing.T) { + workflow_hooks_setup(t) + logger := wh.Logger.NewLogger("log", false, logging.LogLevel(1)) + + When(whWorkingDir.Clone(logger, fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace)). + ThenReturn("path/to/repo", false, nil) + wh.RunPreHooks(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) +} + +func TestRunPreHooks_DrainOngoing(t *testing.T) { + t.Log("if drain is ongoing then a message should be displayed") + vcsClient := workflow_hooks_setup(t) + whDrainer.ShutdownBlocking() + wh.RunPreHooks(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) + vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, "Atlantis server is shutting down, please try again later.", "pre_workflow_hooks") +} + +func TestRunPreHooks_DrainNotOngoing(t *testing.T) { + t.Log("if drain is not ongoing then remove ongoing operation must be called even if panic occured") + workflow_hooks_setup(t) + When(whWorkingDir.Clone(logger, fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace)).ThenPanic("panic test - if you're seeing this in a test failure this isn't the failing test") + wh.RunPreHooks(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) + whWorkingDir.VerifyWasCalledOnce().Clone(logger, fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace) + Equals(t, 0, whDrainer.GetStatus().InProgressOps) +} diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index 749f4f018a..cb7ec42983 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -25,12 +25,6 @@ type GlobalCfg struct { Workflows map[string]Workflow } -// WorkflowHook is a map of custom run commands to run before workflows. -type WorkflowHook struct { - StepName string - RunCommand string -} - // Repo is the final parsed version of server-side repo config. type Repo struct { // ID is the exact match id of this config. @@ -59,6 +53,12 @@ type MergedProjectCfg struct { RepoCfgVersion int } +// WorkflowHook is a map of custom run commands to run before workflows. +type WorkflowHook struct { + StepName string + RunCommand string +} + // DefaultApplyStage is the Atlantis default apply stage. var DefaultApplyStage = Stage{ Steps: []Step{ diff --git a/server/events_controller.go b/server/events_controller.go index 4d73ab3144..2e35e67125 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -344,8 +344,17 @@ func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRep // closed. fmt.Fprintln(w, "Processing...") - e.Logger.Info("running pre workflow hooks") - e.WorkflowHooksCommandRunner.RunPreHooks(baseRepo, headRepo, pull, user) + e.Logger.Info("running pre workflow hooks if present") + preHookResults, err := e.WorkflowHooksCommandRunner.RunPreHooks(baseRepo, headRepo, pull, user) + if err != nil { + e.Logger.Err("unable to run pre workflow hooks: %s", err) + } + + // If pre workflow produced error. log it and continue workflow execution. + // I decided this should not be blocking, but maybe it should? + if preHookResults.HasErrors() { + e.Logger.Err("pre workflow hook run error results: %s", preHookResults.Errors()) + } e.Logger.Info("executing autoplan") if !e.TestingMode { diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index d3f51d9735..5603b884f6 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -442,6 +442,14 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks. Ok(t, err) } drainer := &events.Drainer{} + workflowHooksCommandRunner := &events.DefaultWorkflowHooksCommandRunner{ + VCSClient: e2eVCSClient, + GlobalCfg: globalCfg, + Logger: logger, + WorkingDirLocker: locker, + WorkingDir: workingDir, + Drainer: drainer, + } commandRunner := &events.DefaultCommandRunner{ ProjectCommandRunner: &events.DefaultProjectCommandRunner{ Locker: projectLocker, @@ -497,8 +505,9 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks. Ok(t, err) ctrl := server.EventsController{ - TestingMode: true, - CommandRunner: commandRunner, + TestingMode: true, + WorkflowHooksCommandRunner: workflowHooksCommandRunner, + CommandRunner: commandRunner, PullCleaner: &events.PullClosedExecutor{ Locker: lockingClient, VCSClient: e2eVCSClient, diff --git a/server/events_controller_test.go b/server/events_controller_test.go index fb50c102d8..5a56a1fa27 100644 --- a/server/events_controller_test.go +++ b/server/events_controller_test.go @@ -45,7 +45,7 @@ var secret = []byte("secret") func TestPost_NotGithubOrGitlab(t *testing.T) { t.Log("when the request is not for gitlab or github a 400 is returned") - e, _, _, _, _, _, _, _ := setup(t) + e, _, _, _, _, _, _, _, _ := setup(t) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) e.Post(w, req) @@ -54,7 +54,7 @@ func TestPost_NotGithubOrGitlab(t *testing.T) { func TestPost_UnsupportedVCSGithub(t *testing.T) { t.Log("when the request is for an unsupported vcs a 400 is returned") - e, _, _, _, _, _, _, _ := setup(t) + e, _, _, _, _, _, _, _, _ := setup(t) e.SupportedVCSHosts = nil req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "value") @@ -65,7 +65,7 @@ func TestPost_UnsupportedVCSGithub(t *testing.T) { func TestPost_UnsupportedVCSGitlab(t *testing.T) { t.Log("when the request is for an unsupported vcs a 400 is returned") - e, _, _, _, _, _, _, _ := setup(t) + e, _, _, _, _, _, _, _, _ := setup(t) e.SupportedVCSHosts = nil req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") @@ -76,7 +76,7 @@ func TestPost_UnsupportedVCSGitlab(t *testing.T) { func TestPost_InvalidGithubSecret(t *testing.T) { t.Log("when the github payload can't be validated a 400 is returned") - e, v, _, _, _, _, _, _ := setup(t) + e, v, _, _, _, _, _, _, _ := setup(t) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "value") @@ -87,7 +87,7 @@ func TestPost_InvalidGithubSecret(t *testing.T) { func TestPost_InvalidGitlabSecret(t *testing.T) { t.Log("when the gitlab payload can't be validated a 400 is returned") - e, _, gl, _, _, _, _, _ := setup(t) + e, _, gl, _, _, _, _, _, _ := setup(t) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") @@ -98,7 +98,7 @@ func TestPost_InvalidGitlabSecret(t *testing.T) { func TestPost_UnsupportedGithubEvent(t *testing.T) { t.Log("when the event type is an unsupported github event we ignore it") - e, v, _, _, _, _, _, _ := setup(t) + e, v, _, _, _, _, _, _, _ := setup(t) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "value") @@ -109,7 +109,7 @@ func TestPost_UnsupportedGithubEvent(t *testing.T) { func TestPost_UnsupportedGitlabEvent(t *testing.T) { t.Log("when the event type is an unsupported gitlab event we ignore it") - e, _, gl, _, _, _, _, _ := setup(t) + e, _, gl, _, _, _, _, _, _ := setup(t) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") @@ -121,7 +121,7 @@ func TestPost_UnsupportedGitlabEvent(t *testing.T) { // Test that if the comment comes from a commit rather than a merge request, // we give an error and ignore it. func TestPost_GitlabCommentOnCommit(t *testing.T) { - e, _, gl, _, _, _, _, _ := setup(t) + e, _, gl, _, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() req.Header.Set(gitlabHeader, "value") @@ -132,7 +132,7 @@ func TestPost_GitlabCommentOnCommit(t *testing.T) { func TestPost_GithubCommentNotCreated(t *testing.T) { t.Log("when the event is a github comment but it's not a created event we ignore it") - e, v, _, _, _, _, _, _ := setup(t) + e, v, _, _, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "issue_comment") // comment action is deleted, not created @@ -145,7 +145,7 @@ func TestPost_GithubCommentNotCreated(t *testing.T) { func TestPost_GithubInvalidComment(t *testing.T) { t.Log("when the event is a github comment without all expected data we return a 400") - e, v, _, p, _, _, _, _ := setup(t) + e, v, _, p, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "issue_comment") event := `{"action": "created"}` @@ -158,7 +158,7 @@ func TestPost_GithubInvalidComment(t *testing.T) { func TestPost_GitlabCommentInvalidCommand(t *testing.T) { t.Log("when the event is a gitlab comment with an invalid command we ignore it") - e, _, gl, _, _, _, _, cp := setup(t) + e, _, gl, _, _, _, _, _, cp := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil) @@ -170,7 +170,7 @@ func TestPost_GitlabCommentInvalidCommand(t *testing.T) { func TestPost_GithubCommentInvalidCommand(t *testing.T) { t.Log("when the event is a github comment with an invalid command we ignore it") - e, v, _, p, _, _, _, cp := setup(t) + e, v, _, p, _, _, _, _, cp := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "issue_comment") event := `{"action": "created"}` @@ -299,7 +299,7 @@ func TestPost_GithubCommentNotAllowlistedWithSilenceErrors(t *testing.T) { func TestPost_GitlabCommentResponse(t *testing.T) { // When the event is a gitlab comment that warrants a comment response we comment back. - e, _, gl, _, _, _, vcsClient, cp := setup(t) + e, _, gl, _, _, _, _, vcsClient, cp := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil) @@ -312,7 +312,7 @@ func TestPost_GitlabCommentResponse(t *testing.T) { func TestPost_GithubCommentResponse(t *testing.T) { t.Log("when the event is a github comment that warrants a comment response we comment back") - e, v, _, p, _, _, vcsClient, cp := setup(t) + e, v, _, p, _, _, _, vcsClient, cp := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "issue_comment") event := `{"action": "created"}` @@ -330,7 +330,7 @@ func TestPost_GithubCommentResponse(t *testing.T) { func TestPost_GitlabCommentSuccess(t *testing.T) { t.Log("when the event is a gitlab comment with a valid command we call the command handler") - e, _, gl, _, cr, _, _, _ := setup(t) + e, _, gl, _, cr, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil) @@ -343,7 +343,7 @@ func TestPost_GitlabCommentSuccess(t *testing.T) { func TestPost_GithubCommentSuccess(t *testing.T) { t.Log("when the event is a github comment with a valid command we call the command handler") - e, v, _, p, cr, _, _, cp := setup(t) + e, v, _, p, cr, _, _, _, cp := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "issue_comment") event := `{"action": "created"}` @@ -362,7 +362,7 @@ func TestPost_GithubCommentSuccess(t *testing.T) { func TestPost_GithubPullRequestInvalid(t *testing.T) { t.Log("when the event is a github pull request with invalid data we return a 400") - e, v, _, p, _, _, _, _ := setup(t) + e, v, _, p, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "pull_request") @@ -376,7 +376,7 @@ func TestPost_GithubPullRequestInvalid(t *testing.T) { func TestPost_GitlabMergeRequestInvalid(t *testing.T) { t.Log("when the event is a gitlab merge request with invalid data we return a 400") - e, _, gl, p, _, _, _, _ := setup(t) + e, _, gl, p, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil) @@ -390,7 +390,7 @@ func TestPost_GitlabMergeRequestInvalid(t *testing.T) { func TestPost_GithubPullRequestNotAllowlisted(t *testing.T) { t.Log("when the event is a github pull request to a non-allowlisted repo we return a 400") - e, v, _, _, _, _, _, _ := setup(t) + e, v, _, _, _, _, _, _, _ := setup(t) var err error e.RepoAllowlistChecker, err = events.NewRepoAllowlistChecker("github.com/nevermatch") Ok(t, err) @@ -406,7 +406,7 @@ func TestPost_GithubPullRequestNotAllowlisted(t *testing.T) { func TestPost_GitlabMergeRequestNotAllowlisted(t *testing.T) { t.Log("when the event is a gitlab merge request to a non-allowlisted repo we return a 400") - e, _, gl, p, _, _, _, _ := setup(t) + e, _, gl, p, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") @@ -425,7 +425,7 @@ func TestPost_GitlabMergeRequestNotAllowlisted(t *testing.T) { func TestPost_GithubPullRequestUnsupportedAction(t *testing.T) { t.Skip("relies too much on mocks, should use real event parser") - e, v, _, _, _, _, _, _ := setup(t) + e, v, _, _, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "pull_request") @@ -440,7 +440,7 @@ func TestPost_GithubPullRequestUnsupportedAction(t *testing.T) { func TestPost_GitlabMergeRequestUnsupportedAction(t *testing.T) { t.Skip("relies too much on mocks, should use real event parser") t.Log("when the event is a gitlab merge request to a non-allowlisted repo we return a 400") - e, _, gl, p, _, _, _, _ := setup(t) + e, _, gl, p, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") var event gitlab.MergeEvent @@ -537,7 +537,7 @@ func TestPost_GithubPullRequestClosedErrCleaningPull(t *testing.T) { t.Skip("relies too much on mocks, should use real event parser") t.Log("when the event is a closed pull request and we have an error calling CleanUpPull we return a 503") RegisterMockTestingT(t) - e, v, _, p, _, c, _, _ := setup(t) + e, v, _, p, _, _, c, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "pull_request") @@ -555,7 +555,7 @@ func TestPost_GithubPullRequestClosedErrCleaningPull(t *testing.T) { func TestPost_GitlabMergeRequestClosedErrCleaningPull(t *testing.T) { t.Skip("relies too much on mocks, should use real event parser") t.Log("when the event is a closed gitlab merge request and an error occurs calling CleanUpPull we return a 500") - e, _, gl, p, _, c, _, _ := setup(t) + e, _, gl, p, _, _, c, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") var event gitlab.MergeEvent @@ -573,7 +573,7 @@ func TestPost_GitlabMergeRequestClosedErrCleaningPull(t *testing.T) { func TestPost_GithubClosedPullRequestSuccess(t *testing.T) { t.Skip("relies too much on mocks, should use real event parser") t.Log("when the event is a pull request and everything works we return a 200") - e, v, _, p, _, c, _, _ := setup(t) + e, v, _, p, _, _, c, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "pull_request") @@ -591,7 +591,7 @@ func TestPost_GithubClosedPullRequestSuccess(t *testing.T) { func TestPost_GitlabMergeRequestSuccess(t *testing.T) { t.Skip("relies too much on mocks, should use real event parser") t.Log("when the event is a gitlab merge request and the cleanup works we return a 200") - e, _, gl, p, _, _, _, _ := setup(t) + e, _, gl, p, _, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil) @@ -710,39 +710,46 @@ func TestPost_PullOpenedOrUpdated(t *testing.T) { for _, c := range cases { t.Run(c.Description, func(t *testing.T) { - e, v, gl, p, cr, _, _, _ := setup(t) + e, v, gl, p, cr, wh, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) + var pullRequest models.PullRequest + var repo models.Repo + switch c.HostType { case models.Gitlab: req.Header.Set(gitlabHeader, "value") var event gitlab.MergeEvent event.ObjectAttributes.Action = c.Action When(gl.ParseAndValidate(req, secret)).ThenReturn(event, nil) - repo := models.Repo{} - pullRequest := models.PullRequest{State: models.ClosedPullState} + repo = models.Repo{} + pullRequest = models.PullRequest{State: models.ClosedPullState} When(p.ParseGitlabMergeRequestEvent(event)).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil) case models.Github: req.Header.Set(githubHeader, "pull_request") event := fmt.Sprintf(`{"action": "%s"}`, c.Action) When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) - repo := models.Repo{} - pull := models.PullRequest{State: models.ClosedPullState} - When(p.ParseGithubPullEvent(matchers.AnyPtrToGithubPullRequestEvent())).ThenReturn(pull, models.OpenedPullEvent, repo, repo, models.User{}, nil) + repo = models.Repo{} + pullRequest = models.PullRequest{State: models.ClosedPullState} + When(p.ParseGithubPullEvent(matchers.AnyPtrToGithubPullRequestEvent())).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil) } + When(wh.RunPreHooks(repo, repo, pullRequest, models.User{})).ThenReturn(&events.WorkflowHooksCommandResult{}, nil) + w := httptest.NewRecorder() e.Post(w, req) responseContains(t, w, http.StatusOK, "Processing...") + wh.VerifyWasCalledOnce().RunPreHooks(models.Repo{}, models.Repo{}, models.PullRequest{State: models.ClosedPullState}, models.User{}) cr.VerifyWasCalledOnce().RunAutoplanCommand(models.Repo{}, models.Repo{}, models.PullRequest{State: models.ClosedPullState}, models.User{}) }) } } -func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParserValidator, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockPullCleaner, *vcsmocks.MockClient, *emocks.MockCommentParsing) { +func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParserValidator, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockWorkflowHooksRunner, *emocks.MockPullCleaner, *vcsmocks.MockClient, *emocks.MockCommentParsing) { RegisterMockTestingT(t) v := mocks.NewMockGithubRequestValidator() gl := mocks.NewMockGitlabRequestParserValidator() p := emocks.NewMockEventParsing() cp := emocks.NewMockCommentParsing() + wh := emocks.NewMockWorkflowHooksCommandRunner() cr := emocks.NewMockCommandRunner() c := emocks.NewMockPullCleaner() vcsmock := vcsmocks.NewMockClient() @@ -755,6 +762,7 @@ func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValid Parser: p, CommentParser: cp, CommandRunner: cr, + WorkflowHooksCommandRunner: wh, PullCleaner: c, GithubWebhookSecret: secret, SupportedVCSHosts: []models.VCSHostType{models.Github, models.Gitlab}, @@ -763,5 +771,5 @@ func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValid RepoAllowlistChecker: repoAllowlistChecker, VCSClient: vcsmock, } - return e, v, gl, p, cr, c, vcsmock, cp + return e, v, gl, p, cr, wh, c, vcsmock, cp } diff --git a/server/server.go b/server/server.go index 08752eada2..9c8db83ef4 100644 --- a/server/server.go +++ b/server/server.go @@ -357,17 +357,13 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Drainer: drainer, } workflowHooksCommandRunner := &events.DefaultWorkflowHooksCommandRunner{ - VCSClient: vcsClient, - GithubPullGetter: githubClient, - GitlabMergeRequestGetter: gitlabClient, - AzureDevopsPullGetter: azuredevopsClient, - GlobalCfg: globalCfg, - Logger: logger, - WorkingDirLocker: workingDirLocker, - WorkingDir: workingDir, - DB: boltdb, - Drainer: drainer, - DeleteLockCommand: deleteLockCommand, + VCSClient: vcsClient, + GlobalCfg: globalCfg, + Logger: logger, + WorkingDirLocker: workingDirLocker, + WorkingDir: workingDir, + Drainer: drainer, + WorkflowHookRunner: &runtime.WorkflowHookRunner{}, } commandRunner := &events.DefaultCommandRunner{ VCSClient: vcsClient, diff --git a/server/testfixtures/test-repos/server-side-cfg/repos.yaml b/server/testfixtures/test-repos/server-side-cfg/repos.yaml index 2b1d564d47..5c550ba60e 100644 --- a/server/testfixtures/test-repos/server-side-cfg/repos.yaml +++ b/server/testfixtures/test-repos/server-side-cfg/repos.yaml @@ -1,6 +1,6 @@ repos: - id: /.*/ - workflow_hooks: + pre_workflow_hooks: - run: echo "hello" workflow: custom allowed_overrides: [workflow] From 795b45af94f20e7d2c92194ba3d9fbdfef48ae04 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Fri, 16 Oct 2020 18:12:47 -0700 Subject: [PATCH 07/11] Adding tests to workflow_hook_runner --- server/events/event_parser.go | 20 --- ...tr_to_events_workflowhookscommandresult.go | 20 --- ...mock_pre_workflows_hooks_command_runner.go | 112 +++++++++++++++ .../mock_workflows_hooks_command_runner.go | 122 ---------------- server/events/models/models.go | 23 +-- .../pre_workflow_hooks_command_runner.go | 129 +++++++++++++++++ ...pre_workflow_hooks_command_runner_test.go} | 35 ++--- ..._runner.go => pre_workflow_hook_runner.go} | 4 +- .../runtime/pre_workflow_hook_runner_test.go | 110 ++++++++++++++ .../events/workflow_hooks_command_result.go | 33 ----- .../events/workflow_hooks_command_runner.go | 134 ------------------ server/events/yaml/parser_validator_test.go | 26 ++-- server/events/yaml/raw/global_cfg.go | 22 +-- server/events/yaml/raw/pre_workflow_step.go | 24 ++-- .../events/yaml/raw/pre_workflow_step_test.go | 30 ++-- server/events/yaml/valid/global_cfg.go | 14 +- server/events/yaml/valid/global_cfg_test.go | 3 + server/events_controller.go | 29 ++-- server/events_controller_e2e_test.go | 21 +-- server/events_controller_test.go | 33 +++-- server/server.go | 86 +++++------ 21 files changed, 513 insertions(+), 517 deletions(-) delete mode 100644 server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go create mode 100644 server/events/mocks/mock_pre_workflows_hooks_command_runner.go delete mode 100644 server/events/mocks/mock_workflows_hooks_command_runner.go create mode 100644 server/events/pre_workflow_hooks_command_runner.go rename server/events/{workflow_hooks_command_runner_test.go => pre_workflow_hooks_command_runner_test.go} (73%) rename server/events/runtime/{workflow_hook_runner.go => pre_workflow_hook_runner.go} (87%) create mode 100644 server/events/runtime/pre_workflow_hook_runner_test.go delete mode 100644 server/events/workflow_hooks_command_result.go delete mode 100644 server/events/workflow_hooks_command_runner.go diff --git a/server/events/event_parser.go b/server/events/event_parser.go index d0c75870e9..90d63b0926 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -43,26 +43,6 @@ type PullCommand interface { IsAutoplan() bool } -// WorkflowHooksCommand is a command that runs pre workflow commands specified -// in the server config when pull request is opened or updated. -type WorkflowHooksCommand struct { -} - -// CommandName is Plan. -func (c WorkflowHooksCommand) CommandName() models.CommandName { - return models.WorkflowHooksCommand -} - -// IsVerbose is false for autoplan commands. -func (c WorkflowHooksCommand) IsVerbose() bool { - return false -} - -// IsAutoplan is false for non autoplan commands. -func (c WorkflowHooksCommand) IsAutoplan() bool { - return false -} - // AutoplanCommand is a plan command that is automatically triggered when a // pull request is opened or updated. type AutoplanCommand struct{} diff --git a/server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go b/server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go deleted file mode 100644 index 138288bc4c..0000000000 --- a/server/events/mocks/matchers/ptr_to_events_workflowhookscommandresult.go +++ /dev/null @@ -1,20 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -package matchers - -import ( - "reflect" - "github.com/petergtz/pegomock" - events "github.com/runatlantis/atlantis/server/events" -) - -func AnyPtrToEventsWorkflowHooksCommandResult() *events.WorkflowHooksCommandResult { - pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*events.WorkflowHooksCommandResult))(nil)).Elem())) - var nullValue *events.WorkflowHooksCommandResult - return nullValue -} - -func EqPtrToEventsWorkflowHooksCommandResult(value *events.WorkflowHooksCommandResult) *events.WorkflowHooksCommandResult { - pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) - var nullValue *events.WorkflowHooksCommandResult - return nullValue -} diff --git a/server/events/mocks/mock_pre_workflows_hooks_command_runner.go b/server/events/mocks/mock_pre_workflows_hooks_command_runner.go new file mode 100644 index 0000000000..3019cc1a79 --- /dev/null +++ b/server/events/mocks/mock_pre_workflows_hooks_command_runner.go @@ -0,0 +1,112 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events (interfaces: PreWorkflowHooksCommandRunner) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" + "reflect" + "time" +) + +type MockPreWorkflowHooksCommandRunner struct { + fail func(message string, callerSkip ...int) +} + +func NewMockPreWorkflowHooksCommandRunner(options ...pegomock.Option) *MockPreWorkflowHooksCommandRunner { + mock := &MockPreWorkflowHooksCommandRunner{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockPreWorkflowHooksCommandRunner) SetFailHandler(fh pegomock.FailHandler) { + mock.fail = fh +} +func (mock *MockPreWorkflowHooksCommandRunner) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockPreWorkflowHooksCommandRunner) RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockPreWorkflowHooksCommandRunner().") + } + params := []pegomock.Param{baseRepo, headRepo, pull, user} + pegomock.GetGenericMockFrom(mock).Invoke("RunPreHooks", params, []reflect.Type{}) +} + +func (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalledOnce() *VerifierMockPreWorkflowHooksCommandRunner { + return &VerifierMockPreWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockPreWorkflowHooksCommandRunner { + return &VerifierMockPreWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPreWorkflowHooksCommandRunner { + return &VerifierMockPreWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockPreWorkflowHooksCommandRunner { + return &VerifierMockPreWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockPreWorkflowHooksCommandRunner struct { + mock *MockPreWorkflowHooksCommandRunner + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockPreWorkflowHooksCommandRunner) RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) *MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification { + params := []pegomock.Param{baseRepo, headRepo, pull, user} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunPreHooks", params, verifier.timeout) + return &MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification struct { + mock *MockPreWorkflowHooksCommandRunner + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetCapturedArguments() (models.Repo, models.Repo, models.PullRequest, models.User) { + baseRepo, headRepo, pull, user := c.GetAllCapturedArguments() + return baseRepo[len(baseRepo)-1], headRepo[len(headRepo)-1], pull[len(pull)-1], user[len(user)-1] +} + +func (c *MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.User) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.Repo) + } + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(models.Repo) + } + _param2 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range params[2] { + _param2[u] = param.(models.PullRequest) + } + _param3 = make([]models.User, len(c.methodInvocations)) + for u, param := range params[3] { + _param3[u] = param.(models.User) + } + } + return +} diff --git a/server/events/mocks/mock_workflows_hooks_command_runner.go b/server/events/mocks/mock_workflows_hooks_command_runner.go deleted file mode 100644 index cee55845d2..0000000000 --- a/server/events/mocks/mock_workflows_hooks_command_runner.go +++ /dev/null @@ -1,122 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/runatlantis/atlantis/server/events (interfaces: WorkflowHooksCommandRunner) - -package mocks - -import ( - pegomock "github.com/petergtz/pegomock" - events "github.com/runatlantis/atlantis/server/events" - models "github.com/runatlantis/atlantis/server/events/models" - "reflect" - "time" -) - -type MockWorkflowHooksCommandRunner struct { - fail func(message string, callerSkip ...int) -} - -func NewMockWorkflowHooksCommandRunner(options ...pegomock.Option) *MockWorkflowHooksCommandRunner { - mock := &MockWorkflowHooksCommandRunner{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockWorkflowHooksCommandRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockWorkflowHooksCommandRunner) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockWorkflowHooksCommandRunner) RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) (*events.WorkflowHooksCommandResult, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockWorkflowHooksCommandRunner().") - } - params := []pegomock.Param{baseRepo, headRepo, pull, user} - result := pegomock.GetGenericMockFrom(mock).Invoke("RunPreHooks", params, []reflect.Type{reflect.TypeOf((**events.WorkflowHooksCommandResult)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *events.WorkflowHooksCommandResult - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*events.WorkflowHooksCommandResult) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalledOnce() *VerifierMockWorkflowHooksCommandRunner { - return &VerifierMockWorkflowHooksCommandRunner{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockWorkflowHooksCommandRunner { - return &VerifierMockWorkflowHooksCommandRunner{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockWorkflowHooksCommandRunner { - return &VerifierMockWorkflowHooksCommandRunner{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockWorkflowHooksCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockWorkflowHooksCommandRunner { - return &VerifierMockWorkflowHooksCommandRunner{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockWorkflowHooksCommandRunner struct { - mock *MockWorkflowHooksCommandRunner - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockWorkflowHooksCommandRunner) RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) *MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification { - params := []pegomock.Param{baseRepo, headRepo, pull, user} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunPreHooks", params, verifier.timeout) - return &MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification struct { - mock *MockWorkflowHooksCommandRunner - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetCapturedArguments() (models.Repo, models.Repo, models.PullRequest, models.User) { - baseRepo, headRepo, pull, user := c.GetAllCapturedArguments() - return baseRepo[len(baseRepo)-1], headRepo[len(headRepo)-1], pull[len(pull)-1], user[len(user)-1] -} - -func (c *MockWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.User) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(models.Repo) - } - _param1 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.Repo) - } - _param2 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(models.PullRequest) - } - _param3 = make([]models.User, len(c.methodInvocations)) - for u, param := range params[3] { - _param3[u] = param.(models.User) - } - } - return -} diff --git a/server/events/models/models.go b/server/events/models/models.go index ec60e16b5f..dd99305ad3 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -521,8 +521,6 @@ const ( // UnlockCommand is a command to discard previous plans as well as the atlantis locks. UnlockCommand // Adding more? Don't forget to update String() below - // WorkflowHooksCommand is a command to run pre workflow steps - WorkflowHooksCommand ) // String returns the string representation of c. @@ -534,15 +532,13 @@ func (c CommandName) String() string { return "plan" case UnlockCommand: return "unlock" - case WorkflowHooksCommand: - return "pre_workflow_hooks" } return "" } -// WorkflowHookCommandContext defines the context for a plan or apply stage that will +// PreWorkflowHookCommandContext defines the context for a plan or apply stage that will // be executed for a project. -type WorkflowHookCommandContext struct { +type PreWorkflowHookCommandContext struct { // BaseRepo is the repository that the pull request will be merged into. BaseRepo Repo // HeadRepo is the repository that is getting merged into the BaseRepo. @@ -550,7 +546,7 @@ type WorkflowHookCommandContext struct { // be the same as BaseRepo. HeadRepo Repo // Log is a logger that's been set up for this context. - Log *logging.SimpleLogger + Log logging.SimpleLogging // Pull is the pull request we're responding to. Pull PullRequest // User is the user that triggered this command. @@ -558,16 +554,3 @@ type WorkflowHookCommandContext struct { // Verbose is true when the user would like verbose output. Verbose bool } - -// WorkflowHookResult is the result of executing a pre workflow hook for a repository. -type WorkflowHookResult struct { - Command CommandName - Output string - Error error - Success bool -} - -// IsSuccessful returns true if this project result had no errors. -func (w WorkflowHookResult) IsSuccessful() bool { - return w.Success -} diff --git a/server/events/pre_workflow_hooks_command_runner.go b/server/events/pre_workflow_hooks_command_runner.go new file mode 100644 index 0000000000..739359b408 --- /dev/null +++ b/server/events/pre_workflow_hooks_command_runner.go @@ -0,0 +1,129 @@ +package events + +import ( + "fmt" + + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/runtime" + "github.com/runatlantis/atlantis/server/events/vcs" + "github.com/runatlantis/atlantis/server/events/yaml/valid" + "github.com/runatlantis/atlantis/server/logging" + "github.com/runatlantis/atlantis/server/recovery" +) + +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_pre_workflows_hooks_command_runner.go PreWorkflowHooksCommandRunner + +type PreWorkflowHooksCommandRunner interface { + RunPreHooks( + baseRepo models.Repo, + headRepo models.Repo, + pull models.PullRequest, + user models.User, + ) +} + +// DefaultPreWorkflowHooksCommandRunner is the first step when processing a workflow hook commands. +type DefaultPreWorkflowHooksCommandRunner struct { + VCSClient vcs.Client + Logger logging.SimpleLogging + WorkingDirLocker WorkingDirLocker + WorkingDir WorkingDir + GlobalCfg valid.GlobalCfg + Drainer *Drainer + PreWorkflowHookRunner *runtime.PreWorkflowHookRunner +} + +// RunPreHooks runs pre_workflow_hooks when PR is opened or updated. +func (w *DefaultPreWorkflowHooksCommandRunner) RunPreHooks( + baseRepo models.Repo, + headRepo models.Repo, + pull models.PullRequest, + user models.User, +) { + if opStarted := w.Drainer.StartOp(); !opStarted { + if commentErr := w.VCSClient.CreateComment(baseRepo, pull.Num, ShutdownComment, "pre_workflow_hooks"); commentErr != nil { + w.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr) + } + return + } + defer w.Drainer.OpDone() + + log := w.buildLogger(baseRepo.FullName, pull.Num) + defer w.logPanics(baseRepo, pull.Num, log) + + log.Info("running pre hooks") + + unlockFn, err := w.WorkingDirLocker.TryLock(baseRepo.FullName, pull.Num, DefaultWorkspace) + if err != nil { + log.Warn("workspace is locked") + return + } + log.Debug("got workspace lock") + defer unlockFn() + + repoDir, _, err := w.WorkingDir.Clone(log, headRepo, pull, DefaultWorkspace) + if err != nil { + log.Err("unable to run pre workflow hooks: %s", err) + return + } + + preWorkflowHooks := make([]*valid.PreWorkflowHook, 0) + for _, repo := range w.GlobalCfg.Repos { + if repo.IDMatches(baseRepo.ID()) && len(repo.PreWorkflowHooks) > 0 { + preWorkflowHooks = append(preWorkflowHooks, repo.PreWorkflowHooks...) + } + } + + ctx := models.PreWorkflowHookCommandContext{ + BaseRepo: baseRepo, + HeadRepo: headRepo, + Log: log, + Pull: pull, + User: user, + Verbose: false, + } + + err = w.runHooks(ctx, preWorkflowHooks, repoDir) + + if err != nil { + log.Err("pre workflow hook run error results: %s", err) + } +} + +func (w *DefaultPreWorkflowHooksCommandRunner) runHooks( + ctx models.PreWorkflowHookCommandContext, + preWorkflowHooks []*valid.PreWorkflowHook, + repoDir string, +) error { + + for _, hook := range preWorkflowHooks { + _, err := w.PreWorkflowHookRunner.Run(ctx, hook.RunCommand, repoDir) + + if err != nil { + return nil + } + } + + return nil +} + +func (w *DefaultPreWorkflowHooksCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { + src := fmt.Sprintf("%s#%d", repoFullName, pullNum) + return w.Logger.NewLogger(src, true, w.Logger.GetLevel()) +} + +// logPanics logs and creates a comment on the pull request for panics. +func (w *DefaultPreWorkflowHooksCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logger logging.SimpleLogging) { + if err := recover(); err != nil { + stack := recovery.Stack(3) + logger.Err("PANIC: %s\n%s", err, stack) + if commentErr := w.VCSClient.CreateComment( + baseRepo, + pullNum, + fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack), + "", + ); commentErr != nil { + logger.Err("unable to comment: %s", commentErr) + } + } +} diff --git a/server/events/workflow_hooks_command_runner_test.go b/server/events/pre_workflow_hooks_command_runner_test.go similarity index 73% rename from server/events/workflow_hooks_command_runner_test.go rename to server/events/pre_workflow_hooks_command_runner_test.go index 3c96d259b1..e8290359c9 100644 --- a/server/events/workflow_hooks_command_runner_test.go +++ b/server/events/pre_workflow_hooks_command_runner_test.go @@ -10,18 +10,19 @@ import ( "github.com/runatlantis/atlantis/server/events/matchers" "github.com/runatlantis/atlantis/server/events/mocks" "github.com/runatlantis/atlantis/server/events/models/fixtures" + "github.com/runatlantis/atlantis/server/events/runtime" vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" "github.com/runatlantis/atlantis/server/logging" logmocks "github.com/runatlantis/atlantis/server/logging/mocks" . "github.com/runatlantis/atlantis/testing" ) -var wh events.DefaultWorkflowHooksCommandRunner +var wh events.DefaultPreWorkflowHooksCommandRunner var whWorkingDir *mocks.MockWorkingDir var whWorkingDirLocker *mocks.MockWorkingDirLocker var whDrainer *events.Drainer -func workflow_hooks_setup(t *testing.T) *vcsmocks.MockClient { +func preWorkflowHooksSetup(t *testing.T) *vcsmocks.MockClient { RegisterMockTestingT(t) vcsClient := vcsmocks.NewMockClient() logger := logmocks.NewMockSimpleLogging() @@ -29,25 +30,25 @@ func workflow_hooks_setup(t *testing.T) *vcsmocks.MockClient { whWorkingDirLocker = mocks.NewMockWorkingDirLocker() whDrainer = &events.Drainer{} - wh = events.DefaultWorkflowHooksCommandRunner{ - VCSClient: vcsClient, - Logger: logger, - WorkingDirLocker: whWorkingDirLocker, - WorkingDir: whWorkingDir, - Drainer: whDrainer, + wh = events.DefaultPreWorkflowHooksCommandRunner{ + VCSClient: vcsClient, + Logger: logger, + WorkingDirLocker: whWorkingDirLocker, + WorkingDir: whWorkingDir, + Drainer: whDrainer, + PreWorkflowHookRunner: &runtime.PreWorkflowHookRunner{}, } return vcsClient } -func TestWorkflowHooksCommand_LogPanics(t *testing.T) { +func TestPreWorkflowHooksCommand_LogPanics(t *testing.T) { t.Log("if there is a panic it is commented back on the pull request") - vcsClient := workflow_hooks_setup(t) + vcsClient := preWorkflowHooksSetup(t) logger := wh.Logger.NewLogger("log", false, logging.LogLevel(1)) When(whWorkingDir.Clone( logger, fixtures.GithubRepo, - fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace, )).ThenPanic("panic test - if you're seeing this in a test failure this isn't the failing test") @@ -60,17 +61,17 @@ func TestWorkflowHooksCommand_LogPanics(t *testing.T) { // Test that if one plan fails and we are using automerge, that // we delete the plans. func TestRunPreHooks_Clone(t *testing.T) { - workflow_hooks_setup(t) + preWorkflowHooksSetup(t) logger := wh.Logger.NewLogger("log", false, logging.LogLevel(1)) - When(whWorkingDir.Clone(logger, fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace)). + When(whWorkingDir.Clone(logger, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace)). ThenReturn("path/to/repo", false, nil) wh.RunPreHooks(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) } func TestRunPreHooks_DrainOngoing(t *testing.T) { t.Log("if drain is ongoing then a message should be displayed") - vcsClient := workflow_hooks_setup(t) + vcsClient := preWorkflowHooksSetup(t) whDrainer.ShutdownBlocking() wh.RunPreHooks(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, "Atlantis server is shutting down, please try again later.", "pre_workflow_hooks") @@ -78,9 +79,9 @@ func TestRunPreHooks_DrainOngoing(t *testing.T) { func TestRunPreHooks_DrainNotOngoing(t *testing.T) { t.Log("if drain is not ongoing then remove ongoing operation must be called even if panic occured") - workflow_hooks_setup(t) - When(whWorkingDir.Clone(logger, fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace)).ThenPanic("panic test - if you're seeing this in a test failure this isn't the failing test") + preWorkflowHooksSetup(t) + When(whWorkingDir.Clone(logger, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace)).ThenPanic("panic test - if you're seeing this in a test failure this isn't the failing test") wh.RunPreHooks(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) - whWorkingDir.VerifyWasCalledOnce().Clone(logger, fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace) + whWorkingDir.VerifyWasCalledOnce().Clone(logger, fixtures.GithubRepo, fixtures.Pull, events.DefaultWorkspace) Equals(t, 0, whDrainer.GetStatus().InProgressOps) } diff --git a/server/events/runtime/workflow_hook_runner.go b/server/events/runtime/pre_workflow_hook_runner.go similarity index 87% rename from server/events/runtime/workflow_hook_runner.go rename to server/events/runtime/pre_workflow_hook_runner.go index ae27961c28..fbe6ee0007 100644 --- a/server/events/runtime/workflow_hook_runner.go +++ b/server/events/runtime/pre_workflow_hook_runner.go @@ -8,9 +8,9 @@ import ( "github.com/runatlantis/atlantis/server/events/models" ) -type WorkflowHookRunner struct{} +type PreWorkflowHookRunner struct{} -func (wh *WorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) { +func (wh *PreWorkflowHookRunner) Run(ctx models.PreWorkflowHookCommandContext, command string, path string) (string, error) { cmd := exec.Command("sh", "-c", command) // #nosec cmd.Dir = path diff --git a/server/events/runtime/pre_workflow_hook_runner_test.go b/server/events/runtime/pre_workflow_hook_runner_test.go new file mode 100644 index 0000000000..19f59b9994 --- /dev/null +++ b/server/events/runtime/pre_workflow_hook_runner_test.go @@ -0,0 +1,110 @@ +package runtime_test + +import ( + "strings" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/events/mocks/matchers" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/runtime" + "github.com/runatlantis/atlantis/server/events/terraform/mocks" + matchers2 "github.com/runatlantis/atlantis/server/events/terraform/mocks/matchers" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +func TestPreWorkflowHookRunner_Run(t *testing.T) { + cases := []struct { + Command string + ExpOut string + ExpErr string + }{ + { + Command: "", + ExpOut: "", + }, + { + Command: "echo hi", + ExpOut: "hi\n", + }, + { + Command: `printf \'your main.tf file does not provide default region.\\ncheck\'`, + ExpOut: `'your`, + }, + { + Command: `printf 'your main.tf file does not provide default region.\ncheck'`, + ExpOut: "your main.tf file does not provide default region.\ncheck", + }, + { + Command: "echo 'a", + ExpErr: "exit status 2: running \"echo 'a\" in", + }, + { + Command: "echo hi >> file && cat file", + ExpOut: "hi\n", + }, + { + Command: "lkjlkj", + ExpErr: "exit status 127: running \"lkjlkj\" in", + }, + { + Command: "echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_author=$PULL_AUTHOR", + ExpOut: "base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat base_branch_name=master pull_num=2 pull_author=acme\n", + }, + { + Command: "echo user_name=$USER_NAME", + ExpOut: "user_name=acme-user\n", + }, + } + + for _, c := range cases { + var err error + + Ok(t, err) + + RegisterMockTestingT(t) + terraform := mocks.NewMockClient() + When(terraform.EnsureVersion(matchers.AnyPtrToLoggingSimpleLogger(), matchers2.AnyPtrToGoVersionVersion())). + ThenReturn(nil) + + logger := logging.NewNoopLogger() + + r := runtime.PreWorkflowHookRunner{} + t.Run(c.Command, func(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + ctx := models.PreWorkflowHookCommandContext{ + BaseRepo: models.Repo{ + Name: "basename", + Owner: "baseowner", + }, + HeadRepo: models.Repo{ + Name: "headname", + Owner: "headowner", + }, + Pull: models.PullRequest{ + Num: 2, + HeadBranch: "add-feat", + BaseBranch: "master", + Author: "acme", + }, + User: models.User{ + Username: "acme-user", + }, + Log: logger, + } + out, err := r.Run(ctx, c.Command, tmpDir) + if c.ExpErr != "" { + ErrContains(t, c.ExpErr, err) + return + } + Ok(t, err) + // Replace $DIR in the exp with the actual temp dir. We do this + // here because when constructing the cases we don't yet know the + // temp dir. + expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1) + Equals(t, expOut, out) + }) + } +} diff --git a/server/events/workflow_hooks_command_result.go b/server/events/workflow_hooks_command_result.go deleted file mode 100644 index 821ad53e30..0000000000 --- a/server/events/workflow_hooks_command_result.go +++ /dev/null @@ -1,33 +0,0 @@ -package events - -import ( - "strings" - - "github.com/runatlantis/atlantis/server/events/models" -) - -// WorkflowHooksCommandResult is the result of executing pre workflow hooks for a -// repository. -type WorkflowHooksCommandResult struct { - WorkflowHookResults []models.WorkflowHookResult -} - -// HasErrors returns true if there were any errors during the execution, -// even if it was only in one project. -func (w WorkflowHooksCommandResult) HasErrors() bool { - for _, r := range w.WorkflowHookResults { - if !r.IsSuccessful() { - return true - } - } - return false -} - -func (w WorkflowHooksCommandResult) Errors() string { - errors := make([]string, 0) - for _, r := range w.WorkflowHookResults { - errors = append(errors, r.Error.Error()) - } - - return strings.Join(errors, "\n") -} diff --git a/server/events/workflow_hooks_command_runner.go b/server/events/workflow_hooks_command_runner.go deleted file mode 100644 index 62424ca1b6..0000000000 --- a/server/events/workflow_hooks_command_runner.go +++ /dev/null @@ -1,134 +0,0 @@ -package events - -import ( - "fmt" - - "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/atlantis/server/events/runtime" - "github.com/runatlantis/atlantis/server/events/vcs" - "github.com/runatlantis/atlantis/server/events/yaml/valid" - "github.com/runatlantis/atlantis/server/logging" - "github.com/runatlantis/atlantis/server/recovery" -) - -//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_workflows_hooks_command_runner.go WorkflowHooksCommandRunner - -type WorkflowHooksCommandRunner interface { - RunPreHooks(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) (*WorkflowHooksCommandResult, error) -} - -// DefaultWorkflowHooksCommandRunner is the first step when processing a workflow hook commands. -type DefaultWorkflowHooksCommandRunner struct { - VCSClient vcs.Client - Logger logging.SimpleLogging - WorkingDirLocker WorkingDirLocker - WorkingDir WorkingDir - GlobalCfg valid.GlobalCfg - Drainer *Drainer - WorkflowHookRunner runtime.WorkflowHookRunner -} - -func (w *DefaultWorkflowHooksCommandRunner) RunPreHooks( - baseRepo models.Repo, - headRepo models.Repo, - pull models.PullRequest, - user models.User, -) (*WorkflowHooksCommandResult, error) { - if opStarted := w.Drainer.StartOp(); !opStarted { - if commentErr := w.VCSClient.CreateComment(baseRepo, pull.Num, ShutdownComment, models.WorkflowHooksCommand.String()); commentErr != nil { - w.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr) - } - return nil, nil - } - defer w.Drainer.OpDone() - - log := w.buildLogger(baseRepo.FullName, pull.Num) - defer w.logPanics(baseRepo, pull.Num, log) - - log.Info("Running Pre Hooks for repo: ") - - unlockFn, err := w.WorkingDirLocker.TryLock(baseRepo.FullName, pull.Num, DefaultWorkspace) - if err != nil { - log.Warn("workspace was locked") - return nil, err - } - log.Debug("got workspace lock") - defer unlockFn() - - repoDir, _, err := w.WorkingDir.Clone(log, baseRepo, headRepo, pull, DefaultWorkspace) - if err != nil { - return nil, err - } - - workflowHooks := make([]*valid.WorkflowHook, 0) - for _, repo := range w.GlobalCfg.Repos { - if repo.IDMatches(baseRepo.ID()) && len(repo.WorkflowHooks) > 0 { - workflowHooks = append(workflowHooks, repo.WorkflowHooks...) - } - } - - ctx := models.WorkflowHookCommandContext{ - BaseRepo: baseRepo, - HeadRepo: headRepo, - Log: log, - Pull: pull, - User: user, - Verbose: false, - } - - outputs := w.runHooks(ctx, workflowHooks, repoDir) - return &WorkflowHooksCommandResult{ - WorkflowHookResults: outputs, - }, nil -} - -func (w *DefaultWorkflowHooksCommandRunner) runHooks( - ctx models.WorkflowHookCommandContext, - workflowHooks []*valid.WorkflowHook, - repoDir string, -) (outputs []models.WorkflowHookResult) { - for _, hook := range workflowHooks { - out, err := w.WorkflowHookRunner.Run(ctx, hook.RunCommand, repoDir) - - res := models.WorkflowHookResult{ - Command: models.WorkflowHooksCommand, - Output: out, - } - - if err != nil { - res.Error = err - res.Success = false - } else { - res.Success = true - } - - outputs = append(outputs, res) - - if !res.IsSuccessful() { - return - } - } - - return -} - -func (w *DefaultWorkflowHooksCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { - src := fmt.Sprintf("%s#%d", repoFullName, pullNum) - return w.Logger.NewLogger(src, true, w.Logger.GetLevel()) -} - -// logPanics logs and creates a comment on the pull request for panics. -func (w *DefaultWorkflowHooksCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logger logging.SimpleLogging) { - if err := recover(); err != nil { - stack := recovery.Stack(3) - logger.Err("PANIC: %s\n%s", err, stack) - if commentErr := w.VCSClient.CreateComment( - baseRepo, - pullNum, - fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack), - "", - ); commentErr != nil { - logger.Err("unable to comment: %s", commentErr) - } - } -} diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index f2e4695992..9339481e32 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -885,11 +885,11 @@ func TestParseGlobalCfg_NotExist(t *testing.T) { func TestParseGlobalCfg(t *testing.T) { defaultCfg := valid.NewGlobalCfg(false, false, false) - workflowHook := &valid.WorkflowHook{ + preWorkflowHook := &valid.PreWorkflowHook{ StepName: "run", RunCommand: "custom workflow command", } - workflowHooks := []*valid.WorkflowHook{workflowHook} + preWorkflowHooks := []*valid.PreWorkflowHook{preWorkflowHook} customWorkflow1 := valid.Workflow{ Name: "custom1", @@ -1058,14 +1058,14 @@ workflows: { ID: "github.com/owner/repo", ApplyRequirements: []string{"approved", "mergeable"}, - WorkflowHooks: workflowHooks, + PreWorkflowHooks: preWorkflowHooks, Workflow: &customWorkflow1, AllowedOverrides: []string{"apply_requirements", "workflow"}, AllowCustomWorkflows: Bool(true), }, { - IDRegex: regexp.MustCompile(".*"), - WorkflowHooks: workflowHooks, + IDRegex: regexp.MustCompile(".*"), + PreWorkflowHooks: preWorkflowHooks, }, }, Workflows: map[string]valid.Workflow{ @@ -1083,8 +1083,8 @@ repos: Repos: []valid.Repo{ defaultCfg.Repos[0], { - IDRegex: regexp.MustCompile("github.com/"), - WorkflowHooks: []*valid.WorkflowHook{}, + IDRegex: regexp.MustCompile("github.com/"), + PreWorkflowHooks: []*valid.PreWorkflowHook{}, }, }, Workflows: map[string]valid.Workflow{ @@ -1102,9 +1102,9 @@ repos: Repos: []valid.Repo{ defaultCfg.Repos[0], { - ID: "github.com/owner/repo", - WorkflowHooks: []*valid.WorkflowHook{}, - Workflow: defaultCfg.Repos[0].Workflow, + ID: "github.com/owner/repo", + PreWorkflowHooks: []*valid.PreWorkflowHook{}, + Workflow: defaultCfg.Repos[0].Workflow, }, }, Workflows: map[string]valid.Workflow{ @@ -1126,7 +1126,7 @@ workflows: Repos: []valid.Repo{ { IDRegex: regexp.MustCompile(".*"), - WorkflowHooks: []*valid.WorkflowHook{}, + PreWorkflowHooks: []*valid.PreWorkflowHook{}, ApplyRequirements: []string{}, Workflow: &valid.Workflow{ Name: "default", @@ -1277,7 +1277,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { { IDRegex: regexp.MustCompile(".*"), ApplyRequirements: []string{"mergeable", "approved"}, - WorkflowHooks: []*valid.WorkflowHook{}, + PreWorkflowHooks: []*valid.PreWorkflowHook{}, Workflow: &customWorkflow, AllowedWorkflows: []string{"custom"}, AllowedOverrides: []string{"workflow", "apply_requirements"}, @@ -1286,7 +1286,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { { ID: "github.com/owner/repo", IDRegex: nil, - WorkflowHooks: []*valid.WorkflowHook{}, + PreWorkflowHooks: []*valid.PreWorkflowHook{}, ApplyRequirements: nil, AllowedOverrides: nil, AllowCustomWorkflows: nil, diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go index 776335d7dc..b0ded8a594 100644 --- a/server/events/yaml/raw/global_cfg.go +++ b/server/events/yaml/raw/global_cfg.go @@ -18,12 +18,12 @@ type GlobalCfg struct { // Repo is the raw schema for repos in the server-side repo config. type Repo struct { - ID string `yaml:"id" json:"id"` - ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` - WorkflowHooks []WorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` - Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` - AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` - AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` + ID string `yaml:"id" json:"id"` + ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` + PreWorkflowHooks []PreWorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` + Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` + AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` + AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` } func (g GlobalCfg) Validate() error { @@ -170,10 +170,10 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo { workflow = &ptr } - workflowHooks := make([]*valid.WorkflowHook, 0) - if len(r.WorkflowHooks) > 0 { - for _, hook := range r.WorkflowHooks { - workflowHooks = append(workflowHooks, hook.ToValid()) + preWorkflowHooks := make([]*valid.PreWorkflowHook, 0) + if len(r.PreWorkflowHooks) > 0 { + for _, hook := range r.PreWorkflowHooks { + preWorkflowHooks = append(preWorkflowHooks, hook.ToValid()) } } @@ -181,7 +181,7 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo { ID: id, IDRegex: idRegex, ApplyRequirements: r.ApplyRequirements, - WorkflowHooks: workflowHooks, + PreWorkflowHooks: preWorkflowHooks, Workflow: workflow, AllowedWorkflows: r.AllowedWorkflows, AllowedOverrides: r.AllowedOverrides, diff --git a/server/events/yaml/raw/pre_workflow_step.go b/server/events/yaml/raw/pre_workflow_step.go index 848234a848..221d09207f 100644 --- a/server/events/yaml/raw/pre_workflow_step.go +++ b/server/events/yaml/raw/pre_workflow_step.go @@ -11,29 +11,29 @@ import ( "github.com/runatlantis/atlantis/server/events/yaml/valid" ) -// WorkflowHook represents a single action/command to perform. In YAML, +// PreWorkflowHook represents a single action/command to perform. In YAML, // it can be set as // A map for a custom run commands: // - run: my custom command -type WorkflowHook struct { +type PreWorkflowHook struct { StringVal map[string]string } -func (s *WorkflowHook) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (s *PreWorkflowHook) UnmarshalYAML(unmarshal func(interface{}) error) error { return s.unmarshalGeneric(unmarshal) } -func (s WorkflowHook) MarshalYAML() (interface{}, error) { +func (s PreWorkflowHook) MarshalYAML() (interface{}, error) { return s.marshalGeneric() } -func (s *WorkflowHook) UnmarshalJSON(data []byte) error { +func (s *PreWorkflowHook) UnmarshalJSON(data []byte) error { return s.unmarshalGeneric(func(i interface{}) error { return json.Unmarshal(data, i) }) } -func (s *WorkflowHook) MarshalJSON() ([]byte, error) { +func (s *PreWorkflowHook) MarshalJSON() ([]byte, error) { out, err := s.marshalGeneric() if err != nil { return nil, err @@ -41,7 +41,7 @@ func (s *WorkflowHook) MarshalJSON() ([]byte, error) { return json.Marshal(out) } -func (s WorkflowHook) Validate() error { +func (s PreWorkflowHook) Validate() error { runStep := func(value interface{}) error { elem := value.(map[string]string) var keys []string @@ -69,13 +69,13 @@ func (s WorkflowHook) Validate() error { return errors.New("step element is empty") } -func (s WorkflowHook) ToValid() *valid.WorkflowHook { - // This will trigger in case #4 (see WorkflowHook docs). +func (s PreWorkflowHook) ToValid() *valid.PreWorkflowHook { + // This will trigger in case #4 (see PreWorkflowHook 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{ + return &valid.PreWorkflowHook{ StepName: RunStepName, RunCommand: v, } @@ -89,7 +89,7 @@ func (s WorkflowHook) ToValid() *valid.WorkflowHook { // a step a custom run step: " - run: my custom command" // It takes a parameter unmarshal that is a function that tries to unmarshal // the current element into a given object. -func (s *WorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error { +func (s *PreWorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error { // Try to unmarshal as a custom run step, ex. // repo_config: // - run: my command @@ -104,7 +104,7 @@ func (s *WorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error return err } -func (s WorkflowHook) marshalGeneric() (interface{}, error) { +func (s PreWorkflowHook) marshalGeneric() (interface{}, error) { if len(s.StringVal) != 0 { return s.StringVal, nil } diff --git a/server/events/yaml/raw/pre_workflow_step_test.go b/server/events/yaml/raw/pre_workflow_step_test.go index 9d69ed1df4..babbc6a38f 100644 --- a/server/events/yaml/raw/pre_workflow_step_test.go +++ b/server/events/yaml/raw/pre_workflow_step_test.go @@ -9,11 +9,11 @@ import ( yaml "gopkg.in/yaml.v2" ) -func TestWorkflowHook_YAMLMarshalling(t *testing.T) { +func TestPreWorkflowHook_YAMLMarshalling(t *testing.T) { cases := []struct { description string input string - exp raw.WorkflowHook + exp raw.PreWorkflowHook expErr string }{ // Run-step style @@ -21,7 +21,7 @@ func TestWorkflowHook_YAMLMarshalling(t *testing.T) { description: "run step", input: ` run: my command`, - exp: raw.WorkflowHook{ + exp: raw.PreWorkflowHook{ StringVal: map[string]string{ "run": "my command", }, @@ -32,7 +32,7 @@ run: my command`, input: ` run: my command key: value`, - exp: raw.WorkflowHook{ + exp: raw.PreWorkflowHook{ StringVal: map[string]string{ "run": "my command", "key": "value", @@ -53,7 +53,7 @@ key: for _, c := range cases { t.Run(c.description, func(t *testing.T) { - var got raw.WorkflowHook + var got raw.PreWorkflowHook err := yaml.UnmarshalStrict([]byte(c.input), &got) if c.expErr != "" { ErrEquals(t, c.expErr, err) @@ -65,7 +65,7 @@ key: _, err = yaml.Marshal(got) Ok(t, err) - var got2 raw.WorkflowHook + var got2 raw.PreWorkflowHook err = yaml.UnmarshalStrict([]byte(c.input), &got2) Ok(t, err) Equals(t, got2, got) @@ -76,12 +76,12 @@ key: func TestGlobalConfigStep_Validate(t *testing.T) { cases := []struct { description string - input raw.WorkflowHook + input raw.PreWorkflowHook expErr string }{ { description: "run step", - input: raw.WorkflowHook{ + input: raw.PreWorkflowHook{ StringVal: map[string]string{ "run": "my command", }, @@ -90,7 +90,7 @@ func TestGlobalConfigStep_Validate(t *testing.T) { }, { description: "invalid key in string val", - input: raw.WorkflowHook{ + input: raw.PreWorkflowHook{ StringVal: map[string]string{ "invalid": "", }, @@ -101,7 +101,7 @@ func TestGlobalConfigStep_Validate(t *testing.T) { // For atlantis.yaml v2, this wouldn't parse, but now there should // be no error. description: "unparseable shell command", - input: raw.WorkflowHook{ + input: raw.PreWorkflowHook{ StringVal: map[string]string{ "run": "my 'c", }, @@ -120,20 +120,20 @@ func TestGlobalConfigStep_Validate(t *testing.T) { } } -func TestWorkflowHook_ToValid(t *testing.T) { +func TestPreWorkflowHook_ToValid(t *testing.T) { cases := []struct { description string - input raw.WorkflowHook - exp *valid.WorkflowHook + input raw.PreWorkflowHook + exp *valid.PreWorkflowHook }{ { description: "run step", - input: raw.WorkflowHook{ + input: raw.PreWorkflowHook{ StringVal: map[string]string{ "run": "my 'run command'", }, }, - exp: &valid.WorkflowHook{ + exp: &valid.PreWorkflowHook{ StepName: "run", RunCommand: "my 'run command'", }, diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index cb7ec42983..512f48daae 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -12,7 +12,7 @@ import ( const MergeableApplyReq = "mergeable" const ApprovedApplyReq = "approved" const ApplyRequirementsKey = "apply_requirements" -const WorkflowHooksKey = "pre_workflow_hooks" +const PreWorkflowHooksKey = "pre_workflow_hooks" const WorkflowKey = "workflow" const AllowedWorkflowsKey = "allowed_workflows" const AllowedOverridesKey = "allowed_overrides" @@ -34,7 +34,7 @@ type Repo struct { // If ID is set then this will be nil. IDRegex *regexp.Regexp ApplyRequirements []string - WorkflowHooks []*WorkflowHook + PreWorkflowHooks []*PreWorkflowHook Workflow *Workflow AllowedWorkflows []string AllowedOverrides []string @@ -53,8 +53,8 @@ type MergedProjectCfg struct { RepoCfgVersion int } -// WorkflowHook is a map of custom run commands to run before workflows. -type WorkflowHook struct { +// PreWorkflowHook is a map of custom run commands to run before workflows. +type PreWorkflowHook struct { StepName string RunCommand string } @@ -96,7 +96,7 @@ func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) Global // we treat nil slices differently. applyReqs := []string{} allowedOverrides := []string{} - workflowHooks := make([]*WorkflowHook, 0) + preWorkflowHooks := make([]*PreWorkflowHook, 0) if mergeableReq { applyReqs = append(applyReqs, MergeableApplyReq) } @@ -115,7 +115,7 @@ func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) Global { IDRegex: regexp.MustCompile(".*"), ApplyRequirements: applyReqs, - WorkflowHooks: workflowHooks, + PreWorkflowHooks: preWorkflowHooks, Workflow: &defaultWorkflow, AllowedWorkflows: allowedWorkflows, AllowedOverrides: allowedOverrides, @@ -327,7 +327,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } - for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, WorkflowHooksKey} { + for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, PreWorkflowHooksKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { switch key { diff --git a/server/events/yaml/valid/global_cfg_test.go b/server/events/yaml/valid/global_cfg_test.go index 85fcedfc14..61f39bdd65 100644 --- a/server/events/yaml/valid/global_cfg_test.go +++ b/server/events/yaml/valid/global_cfg_test.go @@ -108,6 +108,9 @@ func TestNewGlobalCfg(t *testing.T) { if c.approvedReq { exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "approved") } + if exp.Repos[0].PreWorkflowHooks == nil { + exp.Repos[0].PreWorkflowHooks = []*valid.PreWorkflowHook{} + } Equals(t, exp, act) diff --git a/server/events_controller.go b/server/events_controller.go index 2e35e67125..5dca5efaee 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -45,13 +45,13 @@ const bitbucketServerSignatureHeader = "X-Hub-Signature" // EventsController handles all webhook requests which signify 'events' in the // VCS host, ex. GitHub. type EventsController struct { - WorkflowHooksCommandRunner events.WorkflowHooksCommandRunner - CommandRunner events.CommandRunner - PullCleaner events.PullCleaner - Logger *logging.SimpleLogger - Parser events.EventParsing - CommentParser events.CommentParsing - ApplyDisabled bool + PreWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner + CommandRunner events.CommandRunner + PullCleaner events.PullCleaner + Logger *logging.SimpleLogger + Parser events.EventParsing + CommentParser events.CommentParsing + ApplyDisabled bool // GithubWebhookSecret is the secret added to this webhook via the GitHub // UI that identifies this call as coming from GitHub. If empty, no // request validation is done. @@ -345,16 +345,7 @@ func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRep fmt.Fprintln(w, "Processing...") e.Logger.Info("running pre workflow hooks if present") - preHookResults, err := e.WorkflowHooksCommandRunner.RunPreHooks(baseRepo, headRepo, pull, user) - if err != nil { - e.Logger.Err("unable to run pre workflow hooks: %s", err) - } - - // If pre workflow produced error. log it and continue workflow execution. - // I decided this should not be blocking, but maybe it should? - if preHookResults.HasErrors() { - e.Logger.Err("pre workflow hook run error results: %s", preHookResults.Errors()) - } + e.PreWorkflowHooksCommandRunner.RunPreHooks(baseRepo, headRepo, pull, user) e.Logger.Info("executing autoplan") if !e.TestingMode { @@ -449,10 +440,6 @@ func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo mo return } - // TODO: run pre workflow hooks - // e.Logger.Info("running pre workflow hooks") - // e.WorkflowHooksCommandRunner.RunPreHooks(baseRepo, headRepo, pull, user) - e.Logger.Debug("executing command") fmt.Fprintln(w, "Processing...") if !e.TestingMode { diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index 5603b884f6..2a396cc8d9 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -442,13 +442,14 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks. Ok(t, err) } drainer := &events.Drainer{} - workflowHooksCommandRunner := &events.DefaultWorkflowHooksCommandRunner{ - VCSClient: e2eVCSClient, - GlobalCfg: globalCfg, - Logger: logger, - WorkingDirLocker: locker, - WorkingDir: workingDir, - Drainer: drainer, + preWorkflowHooksCommandRunner := &events.DefaultPreWorkflowHooksCommandRunner{ + VCSClient: e2eVCSClient, + GlobalCfg: globalCfg, + Logger: logger, + WorkingDirLocker: locker, + WorkingDir: workingDir, + Drainer: drainer, + PreWorkflowHookRunner: &runtime.PreWorkflowHookRunner{}, } commandRunner := &events.DefaultCommandRunner{ ProjectCommandRunner: &events.DefaultProjectCommandRunner{ @@ -505,9 +506,9 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks. Ok(t, err) ctrl := server.EventsController{ - TestingMode: true, - WorkflowHooksCommandRunner: workflowHooksCommandRunner, - CommandRunner: commandRunner, + TestingMode: true, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + CommandRunner: commandRunner, PullCleaner: &events.PullClosedExecutor{ Locker: lockingClient, VCSClient: e2eVCSClient, diff --git a/server/events_controller_test.go b/server/events_controller_test.go index 5a56a1fa27..ebfa917ad7 100644 --- a/server/events_controller_test.go +++ b/server/events_controller_test.go @@ -732,7 +732,6 @@ func TestPost_PullOpenedOrUpdated(t *testing.T) { pullRequest = models.PullRequest{State: models.ClosedPullState} When(p.ParseGithubPullEvent(matchers.AnyPtrToGithubPullRequestEvent())).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil) } - When(wh.RunPreHooks(repo, repo, pullRequest, models.User{})).ThenReturn(&events.WorkflowHooksCommandResult{}, nil) w := httptest.NewRecorder() e.Post(w, req) @@ -743,33 +742,33 @@ func TestPost_PullOpenedOrUpdated(t *testing.T) { } } -func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParserValidator, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockWorkflowHooksRunner, *emocks.MockPullCleaner, *vcsmocks.MockClient, *emocks.MockCommentParsing) { +func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParserValidator, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockPreWorkflowHooksCommandRunner, *emocks.MockPullCleaner, *vcsmocks.MockClient, *emocks.MockCommentParsing) { RegisterMockTestingT(t) v := mocks.NewMockGithubRequestValidator() gl := mocks.NewMockGitlabRequestParserValidator() p := emocks.NewMockEventParsing() cp := emocks.NewMockCommentParsing() - wh := emocks.NewMockWorkflowHooksCommandRunner() + wh := emocks.NewMockPreWorkflowHooksCommandRunner() cr := emocks.NewMockCommandRunner() c := emocks.NewMockPullCleaner() vcsmock := vcsmocks.NewMockClient() repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") Ok(t, err) e := server.EventsController{ - TestingMode: true, - Logger: logging.NewNoopLogger(), - GithubRequestValidator: v, - Parser: p, - CommentParser: cp, - CommandRunner: cr, - WorkflowHooksCommandRunner: wh, - PullCleaner: c, - GithubWebhookSecret: secret, - SupportedVCSHosts: []models.VCSHostType{models.Github, models.Gitlab}, - GitlabWebhookSecret: secret, - GitlabRequestParserValidator: gl, - RepoAllowlistChecker: repoAllowlistChecker, - VCSClient: vcsmock, + TestingMode: true, + Logger: logging.NewNoopLogger(), + GithubRequestValidator: v, + Parser: p, + CommentParser: cp, + CommandRunner: cr, + PreWorkflowHooksCommandRunner: wh, + PullCleaner: c, + GithubWebhookSecret: secret, + SupportedVCSHosts: []models.VCSHostType{models.Github, models.Gitlab}, + GitlabWebhookSecret: secret, + GitlabRequestParserValidator: gl, + RepoAllowlistChecker: repoAllowlistChecker, + VCSClient: vcsmock, } return e, v, gl, p, cr, wh, c, vcsmock, cp } diff --git a/server/server.go b/server/server.go index 9c8db83ef4..a53fb8a8ae 100644 --- a/server/server.go +++ b/server/server.go @@ -66,23 +66,23 @@ const ( // Server runs the Atlantis web server. type Server struct { - AtlantisVersion string - AtlantisURL *url.URL - Router *mux.Router - Port int - WorkflowHooksCommandRunner *events.DefaultWorkflowHooksCommandRunner - CommandRunner *events.DefaultCommandRunner - Logger *logging.SimpleLogger - Locker locking.Locker - EventsController *EventsController - GithubAppController *GithubAppController - LocksController *LocksController - StatusController *StatusController - IndexTemplate TemplateWriter - LockDetailTemplate TemplateWriter - SSLCertFile string - SSLKeyFile string - Drainer *events.Drainer + AtlantisVersion string + AtlantisURL *url.URL + Router *mux.Router + Port int + PreWorkflowHooksCommandRunner *events.DefaultPreWorkflowHooksCommandRunner + CommandRunner *events.DefaultCommandRunner + Logger *logging.SimpleLogger + Locker locking.Locker + EventsController *EventsController + GithubAppController *GithubAppController + LocksController *LocksController + StatusController *StatusController + IndexTemplate TemplateWriter + LockDetailTemplate TemplateWriter + SSLCertFile string + SSLKeyFile string + Drainer *events.Drainer } // Config holds config for server that isn't passed in by the user. @@ -356,14 +356,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Logger: logger, Drainer: drainer, } - workflowHooksCommandRunner := &events.DefaultWorkflowHooksCommandRunner{ - VCSClient: vcsClient, - GlobalCfg: globalCfg, - Logger: logger, - WorkingDirLocker: workingDirLocker, - WorkingDir: workingDir, - Drainer: drainer, - WorkflowHookRunner: &runtime.WorkflowHookRunner{}, + preWorkflowHooksCommandRunner := &events.DefaultPreWorkflowHooksCommandRunner{ + VCSClient: vcsClient, + GlobalCfg: globalCfg, + Logger: logger, + WorkingDirLocker: workingDirLocker, + WorkingDir: workingDir, + Drainer: drainer, + PreWorkflowHookRunner: &runtime.PreWorkflowHookRunner{}, } commandRunner := &events.DefaultCommandRunner{ VCSClient: vcsClient, @@ -446,7 +446,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { DeleteLockCommand: deleteLockCommand, } eventsController := &EventsController{ - WorkflowHooksCommandRunner: workflowHooksCommandRunner, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, CommandRunner: commandRunner, PullCleaner: pullClosedExecutor, Parser: eventParser, @@ -475,23 +475,23 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } return &Server{ - AtlantisVersion: config.AtlantisVersion, - AtlantisURL: parsedURL, - Router: underlyingRouter, - Port: userConfig.Port, - WorkflowHooksCommandRunner: workflowHooksCommandRunner, - CommandRunner: commandRunner, - Logger: logger, - Locker: lockingClient, - EventsController: eventsController, - GithubAppController: githubAppController, - LocksController: locksController, - StatusController: statusController, - IndexTemplate: indexTemplate, - LockDetailTemplate: lockTemplate, - SSLKeyFile: userConfig.SSLKeyFile, - SSLCertFile: userConfig.SSLCertFile, - Drainer: drainer, + AtlantisVersion: config.AtlantisVersion, + AtlantisURL: parsedURL, + Router: underlyingRouter, + Port: userConfig.Port, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + CommandRunner: commandRunner, + Logger: logger, + Locker: lockingClient, + EventsController: eventsController, + GithubAppController: githubAppController, + LocksController: locksController, + StatusController: statusController, + IndexTemplate: indexTemplate, + LockDetailTemplate: lockTemplate, + SSLKeyFile: userConfig.SSLKeyFile, + SSLCertFile: userConfig.SSLCertFile, + Drainer: drainer, }, nil } From 1df6b6025caad42a3db921ae7cf32b7a28e31105 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Mon, 9 Nov 2020 14:02:25 -0800 Subject: [PATCH 08/11] Update comments --- server/events/models/models.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/events/models/models.go b/server/events/models/models.go index dd99305ad3..f54d086692 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -536,8 +536,8 @@ func (c CommandName) String() string { return "" } -// PreWorkflowHookCommandContext defines the context for a plan or apply stage that will -// be executed for a project. +// PreWorkflowHookCommandContext defines the context for a pre_worklfow_hooks that will +// be executed before workflows. type PreWorkflowHookCommandContext struct { // BaseRepo is the repository that the pull request will be merged into. BaseRepo Repo From a1e1f04e77943c5fa5dc9b7174a1096fd0b29aa5 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Thu, 10 Dec 2020 17:16:21 -0800 Subject: [PATCH 09/11] Updated runatlantis.io/docs to have `pre-workflow-hooks` use cases and examples --- runatlantis.io/.vuepress/config.js | 1 + runatlantis.io/docs/pre-workflow-hooks.md | 52 +++++++++++++++++++ .../docs/server-side-repo-config.md | 22 +++++++- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 runatlantis.io/docs/pre-workflow-hooks.md diff --git a/runatlantis.io/.vuepress/config.js b/runatlantis.io/.vuepress/config.js index cd1f76488d..9730c990cc 100644 --- a/runatlantis.io/.vuepress/config.js +++ b/runatlantis.io/.vuepress/config.js @@ -64,6 +64,7 @@ module.exports = { ['configuring-atlantis', 'Overview'], 'server-configuration', 'server-side-repo-config', + 'pre-workflow-hooks', 'custom-workflows', 'repo-level-atlantis-yaml', 'upgrading-atlantis-yaml', diff --git a/runatlantis.io/docs/pre-workflow-hooks.md b/runatlantis.io/docs/pre-workflow-hooks.md new file mode 100644 index 0000000000..cbea3af913 --- /dev/null +++ b/runatlantis.io/docs/pre-workflow-hooks.md @@ -0,0 +1,52 @@ +# Pre Workflow Hooks + +Pre workflow hooks can be defined to run scripts right before default or custom +workflows are executed. + +[[toc]] + +## Usage +Pre workflow hooks can only be specified in the Server-Side Repo Config under +`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. + +```yaml +repos: + - id: /.*/ + pre_workflow_hooks: + - run: ./repo-config-genarator.sh +``` +### Reference +#### Custom `run` Command +This is very similar to [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 | + +::: tip Notes +* `run` commands are executed with the following environment variables: + * `BASE_REPO_NAME` - Name of the repository that the pull request will be merged into, ex. `atlantis`. + * `BASE_REPO_OWNER` - Owner of the repository that the pull request will be merged into, ex. `runatlantis`. + * `HEAD_REPO_NAME` - Name of the repository that is getting merged into the base repository, ex. `atlantis`. + * `HEAD_REPO_OWNER` - Owner of the repository that is getting merged into the base repository, ex. `acme-corp`. + * `HEAD_BRANCH_NAME` - Name of the head branch of the pull request (the branch that is getting merged into the base) + * `BASE_BRANCH_NAME` - Name of the base branch of the pull request (the branch that the pull request is getting merged into) + * `PULL_NUM` - Pull request number or ID, ex. `2`. + * `PULL_AUTHOR` - Username of the pull request author, ex. `acme-user`. + * `DIR` - The absolute path to the root of the cloned repository. + * `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`. +::: + diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 7133cb0a57..8aa8444d73 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -18,7 +18,7 @@ If you don't wish to write a config file to disk, you can use the `--repo-config-json` flag or `ATLANTIS_REPO_CONFIG_JSON` environment variable to specify your config as JSON. See [--repo-config-json](server-configuration.html#repo-config-json) for an example. - + ## Example Server Side Repo ```yaml # repos lists the config for specific repos. @@ -48,6 +48,10 @@ repos: # workflows. If false (default), the repo can only use server-side defined # workflows. allow_custom_workflows: true + + # pre_workflow_hooks defines arbitrary list of scripts to execute before workflow execution. + pre_workflow_hooks: + - run: my-pre-workflow-hook-command arg1 # id can also be an exact match. - id: github.com/myorg/specific-repo @@ -153,6 +157,22 @@ projects: apply_requirements: [] ``` +### Running Scripts Before Atlantis Workflows +If you want to run scripts that would execute before Atlantis can run default or +custom workflows, you can create a `pre-workflow-hooks`: + +```yaml +repos: + - id: /.*/ + pre_workflow_hooks: + +- run: my custom command + - run: | + my bash script inline +``` +See [Pre Workflow Hooks](pre-workflow-hooks.html) for more details on writing +pre workflow hooks. + ### Change The Default Atlantis Workflow If you want to change the default commands that Atlantis runs during `plan` and `apply` phases, you can create a new `workflow`. From 795d350b661cf3f01b6ebec59a9736ab9080fd2d Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Thu, 10 Dec 2020 17:23:08 -0800 Subject: [PATCH 10/11] Fixing spacing in the docs --- runatlantis.io/docs/pre-workflow-hooks.md | 4 ++-- runatlantis.io/docs/server-side-repo-config.md | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/runatlantis.io/docs/pre-workflow-hooks.md b/runatlantis.io/docs/pre-workflow-hooks.md index cbea3af913..61765aa50e 100644 --- a/runatlantis.io/docs/pre-workflow-hooks.md +++ b/runatlantis.io/docs/pre-workflow-hooks.md @@ -22,8 +22,8 @@ right before Atlantis can parse it. ```yaml repos: - id: /.*/ - pre_workflow_hooks: - - run: ./repo-config-genarator.sh + pre_workflow_hooks: + - run: ./repo-config-genarator.sh ``` ### Reference #### Custom `run` Command diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 8aa8444d73..76c6c8bd62 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -165,9 +165,8 @@ custom workflows, you can create a `pre-workflow-hooks`: repos: - id: /.*/ pre_workflow_hooks: - -- run: my custom command - - run: | + - run: my custom command + - run: | my bash script inline ``` See [Pre Workflow Hooks](pre-workflow-hooks.html) for more details on writing From c25d57c413450a5ab604c6c4b944c6e6d9e2bee2 Mon Sep 17 00:00:00 2001 From: Sarvar Muminov Date: Mon, 14 Dec 2020 10:54:29 -0800 Subject: [PATCH 11/11] Fixing broken tests after rebase. --- server/events/yaml/raw/global_cfg.go | 1 + server/events/yaml/valid/global_cfg.go | 1 + 2 files changed, 2 insertions(+) diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go index b0ded8a594..f53cdbcd98 100644 --- a/server/events/yaml/raw/global_cfg.go +++ b/server/events/yaml/raw/global_cfg.go @@ -22,6 +22,7 @@ type Repo struct { ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` PreWorkflowHooks []PreWorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` + AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"` AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` } diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index 512f48daae..b019556b5e 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -96,6 +96,7 @@ func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) Global // we treat nil slices differently. applyReqs := []string{} allowedOverrides := []string{} + allowedWorkflows := []string{} preWorkflowHooks := make([]*PreWorkflowHook, 0) if mergeableReq { applyReqs = append(applyReqs, MergeableApplyReq)