diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 6756e0b6c0..8eb89439f2 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -35,6 +35,7 @@ import ( "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/command/apply" + "github.com/runatlantis/atlantis/server/vcs/markdown" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" @@ -831,7 +832,7 @@ func setupE2E(t *testing.T, repoFixtureDir string, userConfig *server.UserConfig pullUpdater := &events.PullOutputUpdater{ HidePrevPlanComments: false, VCSClient: vcsClient, - MarkdownRenderer: &events.MarkdownRenderer{}, + MarkdownRenderer: &markdown.Renderer{}, } deleteLockCommand := &events.DefaultDeleteLockCommand{ diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 0d975e4108..ff420bd2a7 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -38,6 +38,7 @@ import ( "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/models/fixtures" vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" + "github.com/runatlantis/atlantis/server/vcs/markdown" . "github.com/runatlantis/atlantis/testing" ) @@ -92,7 +93,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { pullUpdater = &events.PullOutputUpdater{ HidePrevPlanComments: false, VCSClient: vcsClient, - MarkdownRenderer: &events.MarkdownRenderer{}, + MarkdownRenderer: &markdown.Renderer{}, } parallelPoolSize := 1 diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go deleted file mode 100644 index 6d327c439f..0000000000 --- a/server/events/markdown_renderer.go +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright 2017 HootSuite Media Inc. -// -// Licensed under the Apache License, Version 2.0 (the License); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an AS IS BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// Modified hereafter by contributors to runatlantis/atlantis. - -package events - -import ( - "bytes" - "fmt" - "io/ioutil" - "strings" - "text/template" - - _ "embed" - - "github.com/Masterminds/sprig/v3" - "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/models" -) - -var ( - planCommandTitle = command.Plan.TitleString() - applyCommandTitle = command.Apply.TitleString() - policyCheckCommandTitle = command.PolicyCheck.TitleString() - approvePoliciesCommandTitle = command.ApprovePolicies.TitleString() - versionCommandTitle = command.Version.TitleString() - // maxUnwrappedLines is the maximum number of lines the Terraform output - // can be before we wrap it in an expandable template. - maxUnwrappedLines = 12 -) - -// MarkdownRenderer renders responses as markdown. -type MarkdownRenderer struct { - // GitlabSupportsCommonMark is true if the version of GitLab we're - // using supports the CommonMark markdown format. - // If we're not configured with a GitLab client, this will be false. - GitlabSupportsCommonMark bool - DisableApplyAll bool - DisableApply bool - DisableMarkdownFolding bool - EnableDiffMarkdownFormat bool -} - -// commonData is data that all responses have. -type commonData struct { - Command string - DisableApplyAll bool - DisableApply bool - EnableDiffMarkdownFormat bool -} - -// errData is data about an error response. -type errData struct { - Error string - commonData -} - -// failureData is data about a failure response. -type failureData struct { - Failure string - commonData -} - -// resultData is data about a successful response. -type resultData struct { - Results []projectResultTmplData - commonData -} - -type planSuccessData struct { - models.PlanSuccess - PlanSummary string - PlanWasDeleted bool - DisableApply bool - EnableDiffMarkdownFormat bool -} - -type policyCheckSuccessData struct { - models.PolicyCheckSuccess -} - -type projectResultTmplData struct { - Workspace string - RepoRelDir string - ProjectName string - Rendered string -} - -// Render formats the data into a markdown string. -// nolint: interfacer -func (m *MarkdownRenderer) Render(res command.Result, cmdName command.Name, vcsHost models.VCSHostType, templateOverrides map[string]string) string { - commandStr := strings.Title(strings.Replace(cmdName.String(), "_", " ", -1)) - common := commonData{ - Command: commandStr, - DisableApplyAll: m.DisableApplyAll || m.DisableApply, - DisableApply: m.DisableApply, - EnableDiffMarkdownFormat: m.EnableDiffMarkdownFormat, - } - if res.Error != nil { - return m.renderTemplate(template.Must(template.New("").Parse(unwrappedErrWithLogTmpl)), errData{res.Error.Error(), common}) - } - if res.Failure != "" { - return m.renderTemplate(template.Must(template.New("").Parse(failureWithLogTmpl)), failureData{res.Failure, common}) - } - return m.renderProjectResults(res.ProjectResults, common, vcsHost, templateOverrides) -} - -func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, common commonData, vcsHost models.VCSHostType, templateOverrides map[string]string) string { - var resultsTmplData []projectResultTmplData - numPlanSuccesses := 0 - numPolicyCheckSuccesses := 0 - numVersionSuccesses := 0 - - for _, result := range results { - resultData := projectResultTmplData{ - Workspace: result.Workspace, - RepoRelDir: result.RepoRelDir, - ProjectName: result.ProjectName, - } - if result.Error != nil { - tmpl := m.getProjectErrTmpl(templateOverrides, vcsHost, result.Error.Error()) - resultData.Rendered = m.renderTemplate(tmpl, struct { - Command string - Error string - }{ - Command: common.Command, - Error: result.Error.Error(), - }) - } else if result.Failure != "" { - resultData.Rendered = m.renderTemplate(m.getProjectFailureTmpl(templateOverrides), struct { - Command string - Failure string - }{ - Command: common.Command, - Failure: result.Failure, - }) - } else if result.PlanSuccess != nil { - resultData.Rendered = m.renderTemplate(m.getProjectPlanSuccessTmpl(templateOverrides, vcsHost, result.PlanSuccess.TerraformOutput), planSuccessData{PlanSuccess: *result.PlanSuccess, PlanSummary: result.PlanSuccess.Summary(), DisableApply: common.DisableApply, EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat}) - numPlanSuccesses++ - } else if result.PolicyCheckSuccess != nil { - resultData.Rendered = m.renderTemplate(m.getProjectPolicyCheckSuccessTmpl(templateOverrides, vcsHost, result.PolicyCheckSuccess.PolicyCheckOutput), policyCheckSuccessData{PolicyCheckSuccess: *result.PolicyCheckSuccess}) - numPolicyCheckSuccesses++ - } else if result.ApplySuccess != "" { - resultData.Rendered = m.renderTemplate(m.getProjectApplySuccessTmpl(templateOverrides, vcsHost, result.ApplySuccess), struct{ Output string }{result.ApplySuccess}) - } else if result.VersionSuccess != "" { - resultData.Rendered = m.renderTemplate(m.getProjectVersionSuccessTmpl(templateOverrides, vcsHost, result.VersionSuccess), struct{ Output string }{result.VersionSuccess}) - numVersionSuccesses++ - } else { - resultData.Rendered = "Found no template. This is a bug!" - } - resultsTmplData = append(resultsTmplData, resultData) - } - - var tmpl *template.Template - switch { - case common.Command == planCommandTitle, - common.Command == policyCheckCommandTitle: - tmpl = m.getPlanTmpl(templateOverrides, resultsTmplData, common, numPlanSuccesses, numPolicyCheckSuccesses) - case common.Command == approvePoliciesCommandTitle: - tmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(approveAllProjectsTmpl)) - case common.Command == applyCommandTitle: - tmpl = m.getApplyTmpl(templateOverrides, resultsTmplData) - case common.Command == versionCommandTitle: - tmpl = m.getVersionTmpl(templateOverrides, resultsTmplData, common, numVersionSuccesses) - default: - return "no template matched–this is a bug" - } - return m.renderTemplate(tmpl, resultData{resultsTmplData, common}) -} - -// shouldUseWrappedTmpl returns true if we should use the wrapped markdown -// templates that collapse the output to make the comment smaller on initial -// load. Some VCS providers or versions of VCS providers don't support this -// syntax. -func (m *MarkdownRenderer) shouldUseWrappedTmpl(vcsHost models.VCSHostType, output string) bool { - if m.DisableMarkdownFolding { - return false - } - - // Bitbucket Cloud and Server don't support the folding markdown syntax. - if vcsHost == models.BitbucketServer || vcsHost == models.BitbucketCloud { - return false - } - - if vcsHost == models.Gitlab && !m.GitlabSupportsCommonMark { - return false - } - - return strings.Count(output, "\n") > maxUnwrappedLines -} - -func (m *MarkdownRenderer) renderTemplate(tmpl *template.Template, data interface{}) string { - buf := &bytes.Buffer{} - if err := tmpl.Execute(buf, data); err != nil { - return fmt.Sprintf("Failed to render template, this is a bug: %v", err) - } - return buf.String() -} - -func (m *MarkdownRenderer) getProjectErrTmpl(templateOverrides map[string]string, vcsHost models.VCSHostType, output string) *template.Template { - if val, ok := templateOverrides["project_err"]; ok { - return template.Must(template.ParseFiles(val)) - } else if m.shouldUseWrappedTmpl(vcsHost, output) { - return template.Must(template.New("").Parse(wrappedErrTmpl)) - } else { - return template.Must(template.New("").Parse(unwrappedErrTmpl)) - } -} - -func (m *MarkdownRenderer) getProjectFailureTmpl(templateOverrides map[string]string) *template.Template { - if val, ok := templateOverrides["project_failure"]; ok { - return template.Must(template.ParseFiles(val)) - } - return template.Must(template.New("").Parse(failureTmpl)) -} - -func (m *MarkdownRenderer) getProjectPlanSuccessTmpl(templateOverrides map[string]string, vcsHost models.VCSHostType, output string) *template.Template { - if val, ok := templateOverrides["project_plan_success"]; ok { - return template.Must(template.ParseFiles(val)) - } else if m.shouldUseWrappedTmpl(vcsHost, output) { - return template.Must(template.New("").Parse(planSuccessWrappedTmpl)) - } else { - return template.Must(template.New("").Parse(planSuccessUnwrappedTmpl)) - } -} - -func (m *MarkdownRenderer) getProjectPolicyCheckSuccessTmpl(templateOverrides map[string]string, vcsHost models.VCSHostType, output string) *template.Template { - if val, ok := templateOverrides["project_policy_check_success"]; ok { - return template.Must(template.ParseFiles(val)) - } else if m.shouldUseWrappedTmpl(vcsHost, output) { - return template.Must(template.New("").Parse(policyCheckSuccessWrappedTmpl)) - } else { - return template.Must(template.New("").Parse(policyCheckSuccessUnwrappedTmpl)) - } -} - -func (m *MarkdownRenderer) getProjectApplySuccessTmpl(templateOverrides map[string]string, vcsHost models.VCSHostType, output string) *template.Template { - if val, ok := templateOverrides["project_apply_success"]; ok { - return template.Must(template.ParseFiles(val)) - } else if m.shouldUseWrappedTmpl(vcsHost, output) { - return template.Must(template.New("").Parse(applyWrappedSuccessTmpl)) - } else { - return template.Must(template.New("").Parse(applyUnwrappedSuccessTmpl)) - } -} - -func (m *MarkdownRenderer) getProjectVersionSuccessTmpl(templateOverrides map[string]string, vcsHost models.VCSHostType, output string) *template.Template { - if val, ok := templateOverrides["project_version_success"]; ok { - return template.Must(template.ParseFiles(val)) - } else if m.shouldUseWrappedTmpl(vcsHost, output) { - return template.Must(template.New("").Parse(versionWrappedSuccessTmpl)) - } else { - return template.Must(template.New("").Parse(versionUnwrappedSuccessTmpl)) - } -} - -func (m *MarkdownRenderer) getPlanTmpl(templateOverrides map[string]string, resultsTmplData []projectResultTmplData, common commonData, numPlanSuccesses int, numPolicyCheckSuccesses int) *template.Template { - if file_name, ok := templateOverrides["plan"]; ok { - if content, err := ioutil.ReadFile(file_name); err == nil { - return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(string(content))) - } - } - switch { - case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses > 0: - return template.Must(template.New("").Parse(singleProjectPlanSuccessTmpl)) - case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses == 0: - return template.Must(template.New("").Parse(singleProjectPlanUnsuccessfulTmpl)) - case len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses > 0: - return template.Must(template.New("").Parse(singleProjectPlanSuccessTmpl)) - case len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses == 0: - return template.Must(template.New("").Parse(singleProjectPlanUnsuccessfulTmpl)) - default: - return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(multiProjectPlanTmpl)) - } -} - -func (m *MarkdownRenderer) getApplyTmpl(templateOverrides map[string]string, resultsTmplData []projectResultTmplData) *template.Template { - if file_name, ok := templateOverrides["apply"]; ok { - if content, err := ioutil.ReadFile(file_name); err == nil { - return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(string(content))) - } - } - if len(resultsTmplData) == 1 { - return template.Must(template.New("").Parse(singleProjectApplyTmpl)) - } else { - return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(multiProjectApplyTmpl)) - } -} - -func (m *MarkdownRenderer) getVersionTmpl(templateOverrides map[string]string, resultsTmplData []projectResultTmplData, common commonData, numVersionSuccesses int) *template.Template { - if file_name, ok := templateOverrides["version"]; ok { - if content, err := ioutil.ReadFile(file_name); err == nil { - return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(string(content))) - } - } - switch { - case len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses > 0: - return template.Must(template.New("").Parse(singleProjectVersionSuccessTmpl)) - case len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses == 0: - return template.Must(template.New("").Parse(singleProjectVersionUnsuccessfulTmpl)) - default: - return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(multiProjectVersionTmpl)) - } -} - -//go:embed templates/singleProjectApply.tmpl -var singleProjectApplyTmpl string - -//go:embed templates/singleProjectPlanSuccess.tmpl -var singleProjectPlanSuccessTmpl string - -//go:embed templates/singleProjectPlanUnsuccessful.tmpl -var singleProjectPlanUnsuccessfulTmpl string - -//go:embed templates/singleProjectVersionSuccess.tmpl -var singleProjectVersionSuccessTmpl string - -//go:embed templates/singleProjectVersionUnsuccessful.tmpl -var singleProjectVersionUnsuccessfulTmpl string - -//go:embed templates/approveAllProjects.tmpl -var approveAllProjectsTmpl string - -//go:embed templates/multiProjectPlan.tmpl -var multiProjectPlanTmpl string - -//go:embed templates/multiProjectApply.tmpl -var multiProjectApplyTmpl string - -//go:embed templates/multiProjectApply.tmpl -var multiProjectVersionTmpl string - -//go:embed templates/planSuccessUnwrapped.tmpl -var planSuccessUnwrappedTmpl string - -//go:embed templates/planSuccessWrapped.tmpl -var planSuccessWrappedTmpl string - -//go:embed templates/policyCheckSuccessUnwrapped.tmpl -var policyCheckSuccessUnwrappedTmpl string - -//go:embed templates/policyCheckSuccessWrapped.tmpl -var policyCheckSuccessWrappedTmpl string - -//go:embed templates/applyUnwrappedSuccess.tmpl -var applyUnwrappedSuccessTmpl string - -//go:embed templates/applyWrappedSuccess.tmpl -var applyWrappedSuccessTmpl string - -//go:embed templates/versionUnwrappedSuccess.tmpl -var versionUnwrappedSuccessTmpl string - -//go:embed templates/versionWrappedSuccess.tmpl -var versionWrappedSuccessTmpl string - -//go:embed templates/unwrappedErr.tmpl -var unwrappedErrTmpl string - -//go:embed templates/unwrappedErrWithLog.tmpl -var unwrappedErrWithLogTmpl string - -//go:embed templates/wrappedErr.tmpl -var wrappedErrTmpl string - -//go:embed templates/failure.tmpl -var failureTmpl string - -//go:embed templates/failureWithLog.tmpl -var failureWithLogTmpl string diff --git a/server/events/output_updater.go b/server/events/output_updater.go index 72424779dd..1570cc86f6 100644 --- a/server/events/output_updater.go +++ b/server/events/output_updater.go @@ -3,13 +3,13 @@ package events import ( "fmt" - "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/events/vcs/types" "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/lyft/feature" + "github.com/runatlantis/atlantis/server/vcs/markdown" ) type OutputUpdater interface { @@ -43,19 +43,11 @@ func (c *FeatureAwareChecksOutputUpdater) UpdateOutput(ctx *command.Context, cmd // Used to support checks type output (Github checks for example) type ChecksOutputUpdater struct { VCSClient vcs.Client - MarkdownRenderer *MarkdownRenderer + MarkdownRenderer *markdown.Renderer TitleBuilder vcs.StatusTitleBuilder - GlobalCfg valid.GlobalCfg } func (c *ChecksOutputUpdater) UpdateOutput(ctx *command.Context, cmd PullCommand, res command.Result) { - var templateOverrides map[string]string - - // retrieve template override if configured - repoCfg := c.GlobalCfg.MatchingRepo(ctx.Pull.BaseRepo.ID()) - if repoCfg != nil { - templateOverrides = repoCfg.TemplateOverrides - } // iterate through all project results and the update the github check for _, projectResult := range res.ProjectResults { @@ -63,7 +55,7 @@ func (c *ChecksOutputUpdater) UpdateOutput(ctx *command.Context, cmd PullCommand ProjectName: projectResult.ProjectName, }) - output := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo.VCSHost.Type, templateOverrides) + output := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo) updateStatusReq := types.UpdateStatusRequest{ Repo: ctx.HeadRepo, Ref: ctx.Pull.HeadCommit, @@ -85,8 +77,7 @@ func (c *ChecksOutputUpdater) UpdateOutput(ctx *command.Context, cmd PullCommand type PullOutputUpdater struct { HidePrevPlanComments bool VCSClient vcs.Client - MarkdownRenderer *MarkdownRenderer - GlobalCfg valid.GlobalCfg + MarkdownRenderer *markdown.Renderer } func (c *PullOutputUpdater) UpdateOutput(ctx *command.Context, cmd PullCommand, res command.Result) { @@ -112,13 +103,7 @@ func (c *PullOutputUpdater) UpdateOutput(ctx *command.Context, cmd PullCommand, } } - var templateOverrides map[string]string - repoCfg := c.GlobalCfg.MatchingRepo(ctx.Pull.BaseRepo.ID()) - if repoCfg != nil { - templateOverrides = repoCfg.TemplateOverrides - } - - comment := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo.VCSHost.Type, templateOverrides) + comment := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo) if err := c.VCSClient.CreateComment(ctx.Pull.BaseRepo, ctx.Pull.Num, comment, cmd.CommandName().String()); err != nil { ctx.Log.Error("unable to comment", map[string]interface{}{ "error": err.Error(), diff --git a/server/events/pull_updater.go b/server/events/pull_updater.go deleted file mode 100644 index 881ed5c53d..0000000000 --- a/server/events/pull_updater.go +++ /dev/null @@ -1,44 +0,0 @@ -package events - -import ( - "fmt" - "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/vcs" -) - -type PullUpdater struct { - HidePrevPlanComments bool - VCSClient vcs.Client - MarkdownRenderer *MarkdownRenderer - GlobalCfg valid.GlobalCfg -} - -func (c *PullUpdater) UpdatePull(ctx *command.Context, cmd PullCommand, res command.Result) { - // Log if we got any errors or failures. - if res.Error != nil { - ctx.Log.ErrorContext(ctx.RequestCtx, res.Error.Error()) - } else if res.Failure != "" { - ctx.Log.WarnContext(ctx.RequestCtx, res.Failure) - } - - // HidePrevCommandComments will hide old comments left from previous runs to reduce - // clutter in a pull/merge request. This will not delete the comment, since the - // comment trail may be useful in auditing or backtracing problems. - if c.HidePrevPlanComments { - if err := c.VCSClient.HidePrevCommandComments(ctx.Pull.BaseRepo, ctx.Pull.Num, cmd.CommandName().TitleString()); err != nil { - ctx.Log.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to hide old comments: %s", err)) - } - } - - var templateOverrides map[string]string - repoCfg := c.GlobalCfg.MatchingRepo(ctx.Pull.BaseRepo.ID()) - if repoCfg != nil { - templateOverrides = repoCfg.TemplateOverrides - } - - comment := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo.VCSHost.Type, templateOverrides) - if err := c.VCSClient.CreateComment(ctx.Pull.BaseRepo, ctx.Pull.Num, comment, cmd.CommandName().String()); err != nil { - ctx.Log.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to comment: %s", err)) - } -} diff --git a/server/lyft/command/feature_runner_test.go b/server/lyft/command/feature_runner_test.go index 4cd989f65d..d1c0d6bf83 100644 --- a/server/lyft/command/feature_runner_test.go +++ b/server/lyft/command/feature_runner_test.go @@ -16,7 +16,7 @@ import ( ) var dbUpdater *events.DBUpdater -var pullUpdater *events.PullUpdater +var pullUpdater *events.PullOutputUpdater var policyCheckCommandRunner *events.PolicyCheckCommandRunner var planCommandRunner *events.PlanCommandRunner var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner diff --git a/server/lyft/gateway/autoplan_builder_test.go b/server/lyft/gateway/autoplan_builder_test.go index 72d8eaccc8..e32d218253 100644 --- a/server/lyft/gateway/autoplan_builder_test.go +++ b/server/lyft/gateway/autoplan_builder_test.go @@ -16,6 +16,7 @@ import ( "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/lyft/gateway" "github.com/runatlantis/atlantis/server/metrics" + "github.com/runatlantis/atlantis/server/vcs/markdown" . "github.com/runatlantis/atlantis/testing" ) @@ -38,7 +39,7 @@ func setupAutoplan(t *testing.T) *vcsmocks.MockClient { pullUpdater := &events.PullOutputUpdater{ HidePrevPlanComments: false, VCSClient: vcsClient, - MarkdownRenderer: &events.MarkdownRenderer{}, + MarkdownRenderer: &markdown.Renderer{}, } preWorkflowHooksCommandRunner = mocks.NewMockPreWorkflowHooksCommandRunner() When(preWorkflowHooksCommandRunner.RunPreHooks(matchers.AnyContextContext(), matchers.AnyPtrToEventsCommandContext())).ThenReturn(nil) diff --git a/server/server.go b/server/server.go index 6f3dfda116..df98844102 100644 --- a/server/server.go +++ b/server/server.go @@ -75,6 +75,7 @@ import ( "github.com/runatlantis/atlantis/server/events/webhooks" "github.com/runatlantis/atlantis/server/logging" lyft_checks "github.com/runatlantis/atlantis/server/lyft/checks" + "github.com/runatlantis/atlantis/server/vcs/markdown" "github.com/urfave/cli" "github.com/urfave/negroni" ) @@ -409,12 +410,17 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if err != nil { return nil, errors.Wrap(err, "initializing terraform") } - markdownRenderer := &events.MarkdownRenderer{ + + templateResolver := markdown.TemplateResolver{ + DisableMarkdownFolding: userConfig.DisableMarkdownFolding, GitlabSupportsCommonMark: gitlabClient.SupportsCommonMark(), + GlobalCfg: globalCfg, + } + markdownRenderer := &markdown.Renderer{ DisableApplyAll: userConfig.DisableApplyAll, - DisableMarkdownFolding: userConfig.DisableMarkdownFolding, DisableApply: userConfig.DisableApply, EnableDiffMarkdownFormat: userConfig.EnableDiffMarkdownFormat, + TemplateResolver: templateResolver, } boltdb, err := db.New(userConfig.DataDir) @@ -624,7 +630,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { pullOutputUpdater := events.PullOutputUpdater{ VCSClient: vcsClient, MarkdownRenderer: markdownRenderer, - GlobalCfg: globalCfg, HidePrevPlanComments: userConfig.HidePrevPlanComments, } @@ -632,7 +637,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { VCSClient: vcsClient, MarkdownRenderer: markdownRenderer, TitleBuilder: vcs.StatusTitleBuilder{TitlePrefix: userConfig.VCSStatusName}, - GlobalCfg: globalCfg, } // [WENGINES-4643] TODO: Remove pullOutputUpdater once github checks is stable diff --git a/server/vcs/markdown/markdown_renderer.go b/server/vcs/markdown/markdown_renderer.go new file mode 100644 index 0000000000..403d4c8204 --- /dev/null +++ b/server/vcs/markdown/markdown_renderer.go @@ -0,0 +1,132 @@ +// Copyright 2017 HootSuite Media Inc. +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Modified hereafter by contributors to runatlantis/atlantis. + +package markdown + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + _ "embed" + + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" +) + +// Renderer renders responses as markdown. +type Renderer struct { + TemplateResolver TemplateResolver + + DisableApplyAll bool + DisableApply bool + EnableDiffMarkdownFormat bool +} + +// commonData is data that all responses have. +type commonData struct { + Command string + DisableApplyAll bool + DisableApply bool + EnableDiffMarkdownFormat bool +} + +// errData is data about an error response. +type errData struct { + Error string + commonData +} + +// failureData is data about a failure response. +type failureData struct { + Failure string + commonData +} + +// resultData is data about a successful response. +type resultData struct { + Results []projectResultTmplData + commonData +} + +type projectResultTmplData struct { + Workspace string + RepoRelDir string + ProjectName string + Rendered string +} + +// Render formats the data into a markdown string. +// nolint: interfacer +func (m *Renderer) Render(res command.Result, cmdName command.Name, baseRepo models.Repo) string { + commandStr := strings.Title(strings.Replace(cmdName.String(), "_", " ", -1)) + common := commonData{ + Command: commandStr, + DisableApplyAll: m.DisableApplyAll || m.DisableApply, + DisableApply: m.DisableApply, + EnableDiffMarkdownFormat: m.EnableDiffMarkdownFormat, + } + if res.Error != nil { + return m.renderTemplate(template.Must(template.New("").Parse(unwrappedErrWithLogTmpl)), errData{res.Error.Error(), common}) + } + if res.Failure != "" { + return m.renderTemplate(template.Must(template.New("").Parse(failureWithLogTmpl)), failureData{res.Failure, common}) + } + return m.renderProjectResults(res.ProjectResults, common, baseRepo) +} + +func (m *Renderer) renderProjectResults(results []command.ProjectResult, common commonData, baseRepo models.Repo) string { + // render project results + var prjResultTmplData []projectResultTmplData + for _, result := range results { + template, templateData := m.TemplateResolver.ResolveProject(result, baseRepo, common) + renderedOutput := m.renderTemplate(template, templateData) + prjResultTmplData = append(prjResultTmplData, projectResultTmplData{ + Workspace: result.Workspace, + RepoRelDir: result.RepoRelDir, + ProjectName: result.ProjectName, + Rendered: renderedOutput, + }) + } + + // render aggregate operation result + numPlanSuccesses, numPolicyCheckSuccesses, numVersionSuccesses := m.countSuccesses(results) + tmpl := m.TemplateResolver.Resolve(common, baseRepo, len(results), numPlanSuccesses, numPolicyCheckSuccesses, numVersionSuccesses) + if tmpl == nil { + return "no template matched–this is a bug" + } + return m.renderTemplate(tmpl, resultData{prjResultTmplData, common}) +} + +func (m *Renderer) countSuccesses(results []command.ProjectResult) (numPlanSuccesses, numPolicyCheckSuccesses, numVersionSuccesses int) { + for _, result := range results { + switch { + case result.PlanSuccess != nil: + numPlanSuccesses += 1 + case result.PolicyCheckSuccess != nil: + numPolicyCheckSuccesses += 1 + case result.VersionSuccess != "": + numVersionSuccesses += 1 + } + } + return +} + +func (m *Renderer) renderTemplate(tmpl *template.Template, data interface{}) string { + buf := &bytes.Buffer{} + if err := tmpl.Execute(buf, data); err != nil { + return fmt.Sprintf("Failed to render template, this is a bug: %v", err) + } + return buf.String() +} diff --git a/server/events/markdown_renderer_test.go b/server/vcs/markdown/markdown_renderer_test.go similarity index 96% rename from server/events/markdown_renderer_test.go rename to server/vcs/markdown/markdown_renderer_test.go index 2957f9d3ef..37175a31d6 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/vcs/markdown/markdown_renderer_test.go @@ -11,7 +11,7 @@ // limitations under the License. // Modified hereafter by contributors to runatlantis/atlantis. -package events_test +package markdown_test import ( "errors" @@ -19,12 +19,20 @@ import ( "strings" "testing" - "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" + . "github.com/runatlantis/atlantis/server/vcs/markdown" . "github.com/runatlantis/atlantis/testing" ) +var testRepo = models.Repo{ + VCSHost: models.VCSHost{ + Hostname: models.Github.String(), + }, + FullName: "test-repo", +} + func TestCustomTemplates(t *testing.T) { cases := []struct { Description string @@ -162,14 +170,27 @@ $$$ map[string]string{"project_plan_success": "testdata/custom_template.tmpl"}, }, } - r := events.MarkdownRenderer{} for _, c := range cases { + + templateResolver := TemplateResolver{ + GlobalCfg: valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testRepo.ID(), + TemplateOverrides: c.TemplateOverrides, + }, + }, + }, + } + r := Renderer{ + TemplateResolver: templateResolver, + } res := command.Result{ ProjectResults: c.ProjectResults, } expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) t.Run(fmt.Sprintf("%s_%t", c.Description, false), func(t *testing.T) { - s := r.Render(res, c.Command, models.Github, c.TemplateOverrides) + s := r.Render(res, c.Command, testRepo) Equals(t, expWithBackticks, s) }) } @@ -203,14 +224,13 @@ func TestRenderErrorf(t *testing.T) { "**Policy Check Error**\n```\nerr\n```\n", }, } - - r := events.MarkdownRenderer{} + r := Renderer{} for _, c := range cases { res := command.Result{ Error: c.Error, } t.Run(c.Description, func(t *testing.T) { - s := r.Render(res, c.Command, models.Github, make(map[string]string)) + s := r.Render(res, c.Command, testRepo) Equals(t, c.Expected, s) }) } @@ -243,26 +263,25 @@ func TestRenderFailure(t *testing.T) { "\n* :heavy_check_mark: To **approve** failing policies either request an approval from approvers or address the failure by modifying the codebase.\n\n", }, } - - r := events.MarkdownRenderer{} + r := Renderer{} for _, c := range cases { res := command.Result{ Failure: c.Failure, } t.Run(c.Description, func(t *testing.T) { - s := r.Render(res, c.Command, models.Github, make(map[string]string)) + s := r.Render(res, c.Command, testRepo) Equals(t, c.Expected, s) }) } } func TestRenderErrAndFailure(t *testing.T) { - r := events.MarkdownRenderer{} + r := Renderer{} res := command.Result{ Error: errors.New("error"), Failure: "failure", } - s := r.Render(res, command.Plan, models.Github, make(map[string]string)) + s := r.Render(res, command.Plan, testRepo) Equals(t, "**Plan Error**\n```\nerror\n```\n", s) } @@ -897,14 +916,14 @@ $$$ }, } - r := events.MarkdownRenderer{} + r := Renderer{} for _, c := range cases { t.Run(c.Description, func(t *testing.T) { res := command.Result{ ProjectResults: c.ProjectResults, } t.Run(c.Description, func(t *testing.T) { - s := r.Render(res, c.Command, c.VCSHost, make(map[string]string)) + s := r.Render(res, c.Command, testRepo) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) Equals(t, expWithBackticks, s) @@ -1047,7 +1066,7 @@ $$$ `, }, } - r := events.MarkdownRenderer{ + r := Renderer{ DisableApplyAll: true, } for _, c := range cases { @@ -1056,7 +1075,7 @@ $$$ ProjectResults: c.ProjectResults, } t.Run(c.Description, func(t *testing.T) { - s := r.Render(res, c.Command, c.VCSHost, make(map[string]string)) + s := r.Render(res, c.Command, testRepo) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) Equals(t, expWithBackticks, s) }) @@ -1190,7 +1209,7 @@ $$$ `, }, } - r := events.MarkdownRenderer{ + r := Renderer{ DisableApplyAll: true, DisableApply: true, } @@ -1200,7 +1219,7 @@ $$$ ProjectResults: c.ProjectResults, } t.Run(c.Description, func(t *testing.T) { - s := r.Render(res, c.Command, c.VCSHost, make(map[string]string)) + s := r.Render(res, c.Command, testRepo) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) Equals(t, expWithBackticks, s) }) @@ -1210,8 +1229,10 @@ $$$ // Test that if folding is disabled that it's not used. func TestRenderProjectResults_DisableFolding(t *testing.T) { - mr := events.MarkdownRenderer{ - DisableMarkdownFolding: true, + mr := Renderer{ + TemplateResolver: TemplateResolver{ + DisableMarkdownFolding: true, + }, } rendered := mr.Render(command.Result{ @@ -1222,7 +1243,7 @@ func TestRenderProjectResults_DisableFolding(t *testing.T) { Error: errors.New(strings.Repeat("line\n", 13)), }, }, - }, command.Plan, models.Github, make(map[string]string)) + }, command.Plan, testRepo) Equals(t, false, strings.Contains(rendered, "
")) } @@ -1294,8 +1315,10 @@ func TestRenderProjectResults_WrappedErrorf(t *testing.T) { for _, c := range cases { t.Run(fmt.Sprintf("%s_%v", c.VCSHost.String(), c.ShouldWrap), func(t *testing.T) { - mr := events.MarkdownRenderer{ - GitlabSupportsCommonMark: c.GitlabCommonMarkSupport, + mr := Renderer{ + TemplateResolver: TemplateResolver{ + GitlabSupportsCommonMark: c.GitlabCommonMarkSupport, + }, } rendered := mr.Render(command.Result{ @@ -1306,7 +1329,11 @@ func TestRenderProjectResults_WrappedErrorf(t *testing.T) { Error: errors.New(c.Output), }, }, - }, command.Plan, c.VCSHost, make(map[string]string)) + }, command.Plan, models.Repo{ + VCSHost: models.VCSHost{ + Type: c.VCSHost, + }, + }) var exp string if c.ShouldWrap { exp = `Ran Plan for dir: $.$ workspace: $default$ @@ -1404,8 +1431,10 @@ func TestRenderProjectResults_WrapSingleProject(t *testing.T) { for _, cmd := range []command.Name{command.Plan, command.Apply} { t.Run(fmt.Sprintf("%s_%s_%v", c.VCSHost.String(), cmd.String(), c.ShouldWrap), func(t *testing.T) { - mr := events.MarkdownRenderer{ - GitlabSupportsCommonMark: c.GitlabCommonMarkSupport, + mr := Renderer{ + TemplateResolver: TemplateResolver{ + GitlabSupportsCommonMark: c.GitlabCommonMarkSupport, + }, } var pr command.ProjectResult switch cmd { @@ -1429,7 +1458,11 @@ func TestRenderProjectResults_WrapSingleProject(t *testing.T) { } rendered := mr.Render(command.Result{ ProjectResults: []command.ProjectResult{pr}, - }, cmd, c.VCSHost, make(map[string]string)) + }, cmd, models.Repo{ + VCSHost: models.VCSHost{ + Type: c.VCSHost, + }, + }) // Check result. var exp string @@ -1509,7 +1542,7 @@ $$$ } func TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) { - mr := events.MarkdownRenderer{} + mr := Renderer{} tfOut := strings.Repeat("line\n", 13) rendered := mr.Render(command.Result{ ProjectResults: []command.ProjectResult{ @@ -1524,7 +1557,7 @@ func TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) { ApplySuccess: tfOut, }, }, - }, command.Apply, models.Github, make(map[string]string)) + }, command.Apply, testRepo) exp := `Ran Apply for 2 projects: 1. dir: $.$ workspace: $staging$ @@ -1555,7 +1588,7 @@ $$$ } func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) { - mr := events.MarkdownRenderer{} + mr := Renderer{} tfOut := strings.Repeat("line\n", 13) + "Plan: 1 to add, 0 to change, 0 to destroy." rendered := mr.Render(command.Result{ ProjectResults: []command.ProjectResult{ @@ -1580,7 +1613,7 @@ func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) { }, }, }, - }, command.Plan, models.Github, make(map[string]string)) + }, command.Plan, testRepo) exp := `Ran Plan for 2 projects: 1. dir: $.$ workspace: $staging$ @@ -1887,7 +1920,7 @@ Plan: 1 to add, 1 to change, 1 to destroy. `, }, } - r := events.MarkdownRenderer{ + r := Renderer{ DisableApplyAll: true, DisableApply: true, EnableDiffMarkdownFormat: true, @@ -1898,7 +1931,11 @@ Plan: 1 to add, 1 to change, 1 to destroy. ProjectResults: c.ProjectResults, } t.Run(c.Description, func(t *testing.T) { - s := r.Render(res, c.Command, c.VCSHost, make(map[string]string)) + s := r.Render(res, c.Command, models.Repo{ + VCSHost: models.VCSHost{ + Type: c.VCSHost, + }, + }) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) Equals(t, expWithBackticks, s) }) diff --git a/server/vcs/markdown/template_resolver.go b/server/vcs/markdown/template_resolver.go new file mode 100644 index 0000000000..48314e8b2a --- /dev/null +++ b/server/vcs/markdown/template_resolver.go @@ -0,0 +1,317 @@ +package markdown + +import ( + "io/ioutil" + "strings" + "text/template" + + _ "embed" + + "github.com/Masterminds/sprig/v3" + "github.com/runatlantis/atlantis/server/core/config/valid" + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" +) + +var ( + planCommandTitle = command.Plan.TitleString() + applyCommandTitle = command.Apply.TitleString() + policyCheckCommandTitle = command.PolicyCheck.TitleString() + approvePoliciesCommandTitle = command.ApprovePolicies.TitleString() + versionCommandTitle = command.Version.TitleString() + // maxUnwrappedLines is the maximum number of lines the Terraform output + // can be before we wrap it in an expandable template. + maxUnwrappedLines = 12 +) + +type planSuccessData struct { + models.PlanSuccess + PlanSummary string + PlanWasDeleted bool + DisableApply bool + EnableDiffMarkdownFormat bool +} + +type policyCheckSuccessData struct { + models.PolicyCheckSuccess +} + +type ProjectOutputType int + +const ( + Failure ProjectOutputType = iota + Error + PlanSuccess + PolicyCheckSuccess + ApplySuccess + VersionSuccess +) + +func (p ProjectOutputType) String() string { + switch p { + case Failure: + return "project_failure" + case Error: + return "project_err" + case PlanSuccess: + return "project_plan_success" + case PolicyCheckSuccess: + return "project_policy_check_success" + case ApplySuccess: + return "project_apply_success" + case VersionSuccess: + return "project_version_success" + } + return "" +} + +// Uses template overrides and server configs to resolve template +type TemplateResolver struct { + // GitlabSupportsCommonMark is true if the version of GitLab we're + // using supports the CommonMark markdown format. + // If we're not configured with a GitLab client, this will be false. + GitlabSupportsCommonMark bool + DisableMarkdownFolding bool + GlobalCfg valid.GlobalCfg +} + +// Resolves templates for commands +func (t *TemplateResolver) Resolve(common commonData, baseRepo models.Repo, numPrjResults int, numPlanSuccesses int, numPolicyCheckSuccesses int, numVersionSuccesses int) *template.Template { + // Build template override for this repo + var templateOverrides map[string]string + repoCfg := t.GlobalCfg.MatchingRepo(baseRepo.ID()) + if repoCfg != nil { + templateOverrides = repoCfg.TemplateOverrides + } + + var tmpl *template.Template + switch { + case common.Command == approvePoliciesCommandTitle: + tmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(approveAllProjectsTmpl)) + case common.Command == planCommandTitle, common.Command == policyCheckCommandTitle: + tmpl = t.getPlanTmpl(common, baseRepo, templateOverrides, numPrjResults, numPlanSuccesses, numPolicyCheckSuccesses) + case common.Command == applyCommandTitle: + tmpl = t.getApplyTmpl(templateOverrides, numPrjResults) + case common.Command == versionCommandTitle: + tmpl = t.getVersionTmpl(templateOverrides, common, numPrjResults, numVersionSuccesses) + } + + return tmpl +} + +// Resolves templates for project commands +func (t *TemplateResolver) ResolveProject(result command.ProjectResult, baseRepo models.Repo, common commonData) (*template.Template, interface{}) { + + // Build template override for this repo + var templateOverrides map[string]string + repoCfg := t.GlobalCfg.MatchingRepo(baseRepo.ID()) + if repoCfg != nil { + templateOverrides = repoCfg.TemplateOverrides + } + + var tmpl *template.Template + var templateData interface{} + + switch { + case result.Failure != "": + // use template override if specified + if val, ok := templateOverrides["project_failure"]; ok { + tmpl = template.Must(template.ParseFiles(val)) + } else { + tmpl = template.Must(template.New("").Parse(failureTmpl)) + } + + templateData = struct { + Command string + Failure string + }{ + Command: common.Command, + Failure: result.Failure, + } + case result.Error != nil: + tmpl = t.buildTemplate(Error, baseRepo.VCSHost.Type, wrappedErrTmpl, unwrappedErrTmpl, result.Error.Error(), templateOverrides) + templateData = struct { + Command string + Error string + }{ + Command: common.Command, + Error: result.Error.Error(), + } + case result.PlanSuccess != nil: + tmpl = t.buildTemplate(PlanSuccess, baseRepo.VCSHost.Type, planSuccessWrappedTmpl, planSuccessUnwrappedTmpl, result.PlanSuccess.TerraformOutput, templateOverrides) + templateData = planSuccessData{ + PlanSuccess: *result.PlanSuccess, + PlanSummary: result.PlanSuccess.Summary(), + DisableApply: common.DisableApply, + EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat, + } + case result.PolicyCheckSuccess != nil: + tmpl = t.buildTemplate(PolicyCheckSuccess, baseRepo.VCSHost.Type, policyCheckSuccessWrappedTmpl, policyCheckSuccessUnwrappedTmpl, result.PolicyCheckSuccess.PolicyCheckOutput, templateOverrides) + templateData = policyCheckSuccessData{ + PolicyCheckSuccess: *result.PolicyCheckSuccess, + } + case result.ApplySuccess != "": + tmpl = t.buildTemplate(ApplySuccess, baseRepo.VCSHost.Type, applyWrappedSuccessTmpl, applyUnwrappedSuccessTmpl, result.ApplySuccess, templateOverrides) + templateData = struct { + Output string + }{ + Output: result.ApplySuccess, + } + case result.VersionSuccess != "": + tmpl = t.buildTemplate(VersionSuccess, baseRepo.VCSHost.Type, versionWrappedSuccessTmpl, versionUnwrappedSuccessTmpl, result.VersionSuccess, templateOverrides) + templateData = struct { + Output string + }{ + Output: result.VersionSuccess, + } + } + return tmpl, templateData + +} + +func (t *TemplateResolver) buildTemplate(projectOutputType ProjectOutputType, vcsHost models.VCSHostType, wrappedTmpl string, unwrappedTmpl string, output string, templateOverrides map[string]string) *template.Template { + // use template override is specified + if val, ok := templateOverrides[projectOutputType.String()]; ok { + return template.Must(template.ParseFiles(val)) + } else if t.shouldUseWrappedTmpl(vcsHost, output) { + return template.Must(template.New("").Parse(wrappedTmpl)) + } else { + return template.Must(template.New("").Parse(unwrappedTmpl)) + } +} + +// shouldUseWrappedTmpl returns true if we should use the wrapped markdown +// templates that collapse the output to make the comment smaller on initial +// load. Some VCS providers or versions of VCS providers don't support this +// syntax. +func (m *TemplateResolver) shouldUseWrappedTmpl(vcsHost models.VCSHostType, output string) bool { + if m.DisableMarkdownFolding { + return false + } + + // Bitbucket Cloud and Server don't support the folding markdown syntax. + if vcsHost == models.BitbucketServer || vcsHost == models.BitbucketCloud { + return false + } + + if vcsHost == models.Gitlab && !m.GitlabSupportsCommonMark { + return false + } + + return strings.Count(output, "\n") > maxUnwrappedLines +} + +func (m *TemplateResolver) getPlanTmpl(common commonData, baseRepo models.Repo, templateOverrides map[string]string, numPrjResults int, numPlanSuccesses int, numPolicyCheckSuccesses int) *template.Template { + if file_name, ok := templateOverrides["plan"]; ok { + if content, err := ioutil.ReadFile(file_name); err == nil { + return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(string(content))) + } + } + switch { + case numPrjResults == 1 && common.Command == planCommandTitle && numPlanSuccesses > 0: + return template.Must(template.New("").Parse(singleProjectPlanSuccessTmpl)) + case numPrjResults == 1 && common.Command == planCommandTitle && numPlanSuccesses == 0: + return template.Must(template.New("").Parse(singleProjectPlanUnsuccessfulTmpl)) + case numPrjResults == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses > 0: + return template.Must(template.New("").Parse(singleProjectPlanSuccessTmpl)) + case numPrjResults == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses == 0: + return template.Must(template.New("").Parse(singleProjectPlanUnsuccessfulTmpl)) + default: + return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(multiProjectPlanTmpl)) + } +} + +func (m *TemplateResolver) getApplyTmpl(templateOverrides map[string]string, numPrjResults int) *template.Template { + if file_name, ok := templateOverrides["apply"]; ok { + if content, err := ioutil.ReadFile(file_name); err == nil { + return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(string(content))) + } + } + if numPrjResults == 1 { + return template.Must(template.New("").Parse(singleProjectApplyTmpl)) + } else { + return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(multiProjectApplyTmpl)) + } +} + +func (m *TemplateResolver) getVersionTmpl(templateOverrides map[string]string, common commonData, numPrjResults int, numVersionSuccesses int) *template.Template { + if file_name, ok := templateOverrides["version"]; ok { + if content, err := ioutil.ReadFile(file_name); err == nil { + return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(string(content))) + } + } + switch { + case numPrjResults == 1 && common.Command == versionCommandTitle && numVersionSuccesses > 0: + return template.Must(template.New("").Parse(singleProjectVersionSuccessTmpl)) + case numPrjResults == 1 && common.Command == versionCommandTitle && numVersionSuccesses == 0: + return template.Must(template.New("").Parse(singleProjectVersionUnsuccessfulTmpl)) + default: + return template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(multiProjectVersionTmpl)) + } +} + +//go:embed templates/singleProjectApply.tmpl +var singleProjectApplyTmpl string + +//go:embed templates/singleProjectPlanSuccess.tmpl +var singleProjectPlanSuccessTmpl string + +//go:embed templates/singleProjectPlanUnsuccessful.tmpl +var singleProjectPlanUnsuccessfulTmpl string + +//go:embed templates/singleProjectVersionSuccess.tmpl +var singleProjectVersionSuccessTmpl string + +//go:embed templates/singleProjectVersionUnsuccessful.tmpl +var singleProjectVersionUnsuccessfulTmpl string + +//go:embed templates/approveAllProjects.tmpl +var approveAllProjectsTmpl string + +//go:embed templates/multiProjectPlan.tmpl +var multiProjectPlanTmpl string + +//go:embed templates/multiProjectApply.tmpl +var multiProjectApplyTmpl string + +//go:embed templates/multiProjectApply.tmpl +var multiProjectVersionTmpl string + +//go:embed templates/planSuccessUnwrapped.tmpl +var planSuccessUnwrappedTmpl string + +//go:embed templates/planSuccessWrapped.tmpl +var planSuccessWrappedTmpl string + +//go:embed templates/policyCheckSuccessUnwrapped.tmpl +var policyCheckSuccessUnwrappedTmpl string + +//go:embed templates/policyCheckSuccessWrapped.tmpl +var policyCheckSuccessWrappedTmpl string + +//go:embed templates/applyUnwrappedSuccess.tmpl +var applyUnwrappedSuccessTmpl string + +//go:embed templates/applyWrappedSuccess.tmpl +var applyWrappedSuccessTmpl string + +//go:embed templates/versionUnwrappedSuccess.tmpl +var versionUnwrappedSuccessTmpl string + +//go:embed templates/versionWrappedSuccess.tmpl +var versionWrappedSuccessTmpl string + +//go:embed templates/unwrappedErr.tmpl +var unwrappedErrTmpl string + +//go:embed templates/unwrappedErrWithLog.tmpl +var unwrappedErrWithLogTmpl string + +//go:embed templates/wrappedErr.tmpl +var wrappedErrTmpl string + +//go:embed templates/failure.tmpl +var failureTmpl string + +//go:embed templates/failureWithLog.tmpl +var failureWithLogTmpl string diff --git a/server/events/templates/applyUnwrappedSuccess.tmpl b/server/vcs/markdown/templates/applyUnwrappedSuccess.tmpl similarity index 100% rename from server/events/templates/applyUnwrappedSuccess.tmpl rename to server/vcs/markdown/templates/applyUnwrappedSuccess.tmpl diff --git a/server/events/templates/applyWrappedSuccess.tmpl b/server/vcs/markdown/templates/applyWrappedSuccess.tmpl similarity index 100% rename from server/events/templates/applyWrappedSuccess.tmpl rename to server/vcs/markdown/templates/applyWrappedSuccess.tmpl diff --git a/server/events/templates/approveAllProjects.tmpl b/server/vcs/markdown/templates/approveAllProjects.tmpl similarity index 100% rename from server/events/templates/approveAllProjects.tmpl rename to server/vcs/markdown/templates/approveAllProjects.tmpl diff --git a/server/events/templates/failure.tmpl b/server/vcs/markdown/templates/failure.tmpl similarity index 100% rename from server/events/templates/failure.tmpl rename to server/vcs/markdown/templates/failure.tmpl diff --git a/server/events/templates/failureWithLog.tmpl b/server/vcs/markdown/templates/failureWithLog.tmpl similarity index 100% rename from server/events/templates/failureWithLog.tmpl rename to server/vcs/markdown/templates/failureWithLog.tmpl diff --git a/server/events/templates/multiProjectApply.tmpl b/server/vcs/markdown/templates/multiProjectApply.tmpl similarity index 100% rename from server/events/templates/multiProjectApply.tmpl rename to server/vcs/markdown/templates/multiProjectApply.tmpl diff --git a/server/events/templates/multiProjectPlan.tmpl b/server/vcs/markdown/templates/multiProjectPlan.tmpl similarity index 100% rename from server/events/templates/multiProjectPlan.tmpl rename to server/vcs/markdown/templates/multiProjectPlan.tmpl diff --git a/server/events/templates/multiProjectVersion.tmpl b/server/vcs/markdown/templates/multiProjectVersion.tmpl similarity index 100% rename from server/events/templates/multiProjectVersion.tmpl rename to server/vcs/markdown/templates/multiProjectVersion.tmpl diff --git a/server/events/templates/planSuccessUnwrapped.tmpl b/server/vcs/markdown/templates/planSuccessUnwrapped.tmpl similarity index 100% rename from server/events/templates/planSuccessUnwrapped.tmpl rename to server/vcs/markdown/templates/planSuccessUnwrapped.tmpl diff --git a/server/events/templates/planSuccessWrapped.tmpl b/server/vcs/markdown/templates/planSuccessWrapped.tmpl similarity index 100% rename from server/events/templates/planSuccessWrapped.tmpl rename to server/vcs/markdown/templates/planSuccessWrapped.tmpl diff --git a/server/events/templates/policyCheckSuccessUnwrapped.tmpl b/server/vcs/markdown/templates/policyCheckSuccessUnwrapped.tmpl similarity index 100% rename from server/events/templates/policyCheckSuccessUnwrapped.tmpl rename to server/vcs/markdown/templates/policyCheckSuccessUnwrapped.tmpl diff --git a/server/events/templates/policyCheckSuccessWrapped.tmpl b/server/vcs/markdown/templates/policyCheckSuccessWrapped.tmpl similarity index 100% rename from server/events/templates/policyCheckSuccessWrapped.tmpl rename to server/vcs/markdown/templates/policyCheckSuccessWrapped.tmpl diff --git a/server/events/templates/singleProjectApply.tmpl b/server/vcs/markdown/templates/singleProjectApply.tmpl similarity index 100% rename from server/events/templates/singleProjectApply.tmpl rename to server/vcs/markdown/templates/singleProjectApply.tmpl diff --git a/server/events/templates/singleProjectPlanSuccess.tmpl b/server/vcs/markdown/templates/singleProjectPlanSuccess.tmpl similarity index 100% rename from server/events/templates/singleProjectPlanSuccess.tmpl rename to server/vcs/markdown/templates/singleProjectPlanSuccess.tmpl diff --git a/server/events/templates/singleProjectPlanUnsuccessful.tmpl b/server/vcs/markdown/templates/singleProjectPlanUnsuccessful.tmpl similarity index 100% rename from server/events/templates/singleProjectPlanUnsuccessful.tmpl rename to server/vcs/markdown/templates/singleProjectPlanUnsuccessful.tmpl diff --git a/server/events/templates/singleProjectVersionSuccess.tmpl b/server/vcs/markdown/templates/singleProjectVersionSuccess.tmpl similarity index 100% rename from server/events/templates/singleProjectVersionSuccess.tmpl rename to server/vcs/markdown/templates/singleProjectVersionSuccess.tmpl diff --git a/server/events/templates/singleProjectVersionUnsuccessful.tmpl b/server/vcs/markdown/templates/singleProjectVersionUnsuccessful.tmpl similarity index 100% rename from server/events/templates/singleProjectVersionUnsuccessful.tmpl rename to server/vcs/markdown/templates/singleProjectVersionUnsuccessful.tmpl diff --git a/server/events/templates/unwrappedErr.tmpl b/server/vcs/markdown/templates/unwrappedErr.tmpl similarity index 100% rename from server/events/templates/unwrappedErr.tmpl rename to server/vcs/markdown/templates/unwrappedErr.tmpl diff --git a/server/events/templates/unwrappedErrWithLog.tmpl b/server/vcs/markdown/templates/unwrappedErrWithLog.tmpl similarity index 100% rename from server/events/templates/unwrappedErrWithLog.tmpl rename to server/vcs/markdown/templates/unwrappedErrWithLog.tmpl diff --git a/server/events/templates/versionUnwrappedSuccess.tmpl b/server/vcs/markdown/templates/versionUnwrappedSuccess.tmpl similarity index 100% rename from server/events/templates/versionUnwrappedSuccess.tmpl rename to server/vcs/markdown/templates/versionUnwrappedSuccess.tmpl diff --git a/server/events/templates/versionWrappedSuccess.tmpl b/server/vcs/markdown/templates/versionWrappedSuccess.tmpl similarity index 100% rename from server/events/templates/versionWrappedSuccess.tmpl rename to server/vcs/markdown/templates/versionWrappedSuccess.tmpl diff --git a/server/events/templates/wrappedErr.tmpl b/server/vcs/markdown/templates/wrappedErr.tmpl similarity index 100% rename from server/events/templates/wrappedErr.tmpl rename to server/vcs/markdown/templates/wrappedErr.tmpl diff --git a/server/events/testdata/custom_template.tmpl b/server/vcs/markdown/testdata/custom_template.tmpl similarity index 100% rename from server/events/testdata/custom_template.tmpl rename to server/vcs/markdown/testdata/custom_template.tmpl