From d620d5de98b0178a16429fa89095507109b4a815 Mon Sep 17 00:00:00 2001 From: Ross Strickland Date: Fri, 11 Nov 2022 09:56:33 -0600 Subject: [PATCH] Add custom templating support (#2647) * added custom templates support * made default templates DRY * Change init to anonymous function. * Initialize anonymous function inside var * - Add flag/cfg param for custom template dir - Add test for custom template * Add documentation. * Address PR comments. Co-authored-by: Ricard Bejarano --- cmd/server.go | 198 ++++++++------- cmd/server_test.go | 138 ++++++----- runatlantis.io/docs/server-configuration.md | 15 ++ .../events/events_controller_e2e_test.go | 2 +- server/events/command_runner_test.go | 2 +- server/events/markdown_renderer.go | 228 +++++------------- server/events/markdown_renderer_test.go | 164 ++++++++++--- .../templates/apply_unwrapped_success.tmpl | 5 + .../templates/apply_wrapped_success.tmpl | 6 + .../templates/approve_all_projects.tmpl | 7 + server/events/templates/diverged.tmpl | 5 + server/events/templates/failure.tmpl | 3 + server/events/templates/failure_with_log.tmpl | 3 + server/events/templates/log.tmpl | 9 + .../events/templates/multi_project_apply.tmpl | 8 + .../templates/multi_project_header.tmpl | 6 + .../events/templates/multi_project_plan.tmpl | 11 + .../templates/multi_project_version.tmpl | 3 + .../templates/plan_success_unwrapped.tmpl | 11 + .../templates/plan_success_wrapped.tmpl | 15 ++ .../policy_check_success_unwrapped.tmpl | 11 + .../policy_check_success_wrapped.tmpl | 14 ++ .../templates/single_project_apply.tmpl | 6 + .../single_project_plan_success.tmpl | 11 + .../single_project_plan_unsuccessful.tmpl | 6 + .../single_project_version_success.tmpl | 6 + .../single_project_version_unsuccessful.tmpl | 3 + server/events/templates/unwrapped_err.tmpl | 10 + .../templates/unwrapped_err_with_log.tmpl | 3 + .../templates/version_unwrapped_success.tmpl | 5 + .../templates/version_wrapped_success.tmpl | 6 + server/events/templates/wrapped_err.tmpl | 9 + server/server.go | 17 +- server/user_config.go | 1 + 34 files changed, 593 insertions(+), 354 deletions(-) create mode 100644 server/events/templates/apply_unwrapped_success.tmpl create mode 100644 server/events/templates/apply_wrapped_success.tmpl create mode 100644 server/events/templates/approve_all_projects.tmpl create mode 100644 server/events/templates/diverged.tmpl create mode 100644 server/events/templates/failure.tmpl create mode 100644 server/events/templates/failure_with_log.tmpl create mode 100644 server/events/templates/log.tmpl create mode 100644 server/events/templates/multi_project_apply.tmpl create mode 100644 server/events/templates/multi_project_header.tmpl create mode 100644 server/events/templates/multi_project_plan.tmpl create mode 100644 server/events/templates/multi_project_version.tmpl create mode 100644 server/events/templates/plan_success_unwrapped.tmpl create mode 100644 server/events/templates/plan_success_wrapped.tmpl create mode 100644 server/events/templates/policy_check_success_unwrapped.tmpl create mode 100644 server/events/templates/policy_check_success_wrapped.tmpl create mode 100644 server/events/templates/single_project_apply.tmpl create mode 100644 server/events/templates/single_project_plan_success.tmpl create mode 100644 server/events/templates/single_project_plan_unsuccessful.tmpl create mode 100644 server/events/templates/single_project_version_success.tmpl create mode 100644 server/events/templates/single_project_version_unsuccessful.tmpl create mode 100644 server/events/templates/unwrapped_err.tmpl create mode 100644 server/events/templates/unwrapped_err_with_log.tmpl create mode 100644 server/events/templates/version_unwrapped_success.tmpl create mode 100644 server/events/templates/version_wrapped_success.tmpl create mode 100644 server/events/templates/wrapped_err.tmpl diff --git a/cmd/server.go b/cmd/server.go index d74af5890c..96eda6d5ac 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -37,63 +37,64 @@ import ( // 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices. const ( // Flag names. - ADWebhookPasswordFlag = "azuredevops-webhook-password" // nolint: gosec - ADWebhookUserFlag = "azuredevops-webhook-user" - ADTokenFlag = "azuredevops-token" // nolint: gosec - ADUserFlag = "azuredevops-user" - ADHostnameFlag = "azuredevops-hostname" - AllowForkPRsFlag = "allow-fork-prs" - AllowRepoConfigFlag = "allow-repo-config" - AtlantisURLFlag = "atlantis-url" - AutomergeFlag = "automerge" - AutoplanFileListFlag = "autoplan-file-list" - BitbucketBaseURLFlag = "bitbucket-base-url" - BitbucketTokenFlag = "bitbucket-token" - BitbucketUserFlag = "bitbucket-user" - BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" - ConfigFlag = "config" - CheckoutStrategyFlag = "checkout-strategy" - DataDirFlag = "data-dir" - DefaultTFVersionFlag = "default-tf-version" - DisableApplyAllFlag = "disable-apply-all" - DisableApplyFlag = "disable-apply" - DisableAutoplanFlag = "disable-autoplan" - DisableMarkdownFoldingFlag = "disable-markdown-folding" - DisableRepoLockingFlag = "disable-repo-locking" - EnablePolicyChecksFlag = "enable-policy-checks" - EnableRegExpCmdFlag = "enable-regexp-cmd" - EnableDiffMarkdownFormat = "enable-diff-markdown-format" - GHHostnameFlag = "gh-hostname" - GHTeamAllowlistFlag = "gh-team-allowlist" - GHTokenFlag = "gh-token" - GHUserFlag = "gh-user" - GHAppIDFlag = "gh-app-id" - GHAppKeyFlag = "gh-app-key" - GHAppKeyFileFlag = "gh-app-key-file" - GHAppSlugFlag = "gh-app-slug" - GHOrganizationFlag = "gh-org" - GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec - GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec - GitlabHostnameFlag = "gitlab-hostname" - GitlabTokenFlag = "gitlab-token" - GitlabUserFlag = "gitlab-user" - GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec - APISecretFlag = "api-secret" - HidePrevPlanComments = "hide-prev-plan-comments" - LockingDBType = "locking-db-type" - LogLevelFlag = "log-level" - ParallelPoolSize = "parallel-pool-size" - StatsNamespace = "stats-namespace" - AllowDraftPRs = "allow-draft-prs" - PortFlag = "port" - RedisDB = "redis-db" - RedisHost = "redis-host" - RedisPassword = "redis-password" - RedisPort = "redis-port" - RedisTLSEnabled = "redis-tls-enabled" - RedisInsecureSkipVerify = "redis-insecure-skip-verify" - RepoConfigFlag = "repo-config" - RepoConfigJSONFlag = "repo-config-json" + ADWebhookPasswordFlag = "azuredevops-webhook-password" // nolint: gosec + ADWebhookUserFlag = "azuredevops-webhook-user" + ADTokenFlag = "azuredevops-token" // nolint: gosec + ADUserFlag = "azuredevops-user" + ADHostnameFlag = "azuredevops-hostname" + AllowForkPRsFlag = "allow-fork-prs" + AllowRepoConfigFlag = "allow-repo-config" + AtlantisURLFlag = "atlantis-url" + AutomergeFlag = "automerge" + AutoplanFileListFlag = "autoplan-file-list" + BitbucketBaseURLFlag = "bitbucket-base-url" + BitbucketTokenFlag = "bitbucket-token" + BitbucketUserFlag = "bitbucket-user" + BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" + ConfigFlag = "config" + CheckoutStrategyFlag = "checkout-strategy" + DataDirFlag = "data-dir" + DefaultTFVersionFlag = "default-tf-version" + DisableApplyAllFlag = "disable-apply-all" + DisableApplyFlag = "disable-apply" + DisableAutoplanFlag = "disable-autoplan" + DisableMarkdownFoldingFlag = "disable-markdown-folding" + DisableRepoLockingFlag = "disable-repo-locking" + EnablePolicyChecksFlag = "enable-policy-checks" + EnableRegExpCmdFlag = "enable-regexp-cmd" + EnableDiffMarkdownFormat = "enable-diff-markdown-format" + GHHostnameFlag = "gh-hostname" + GHTeamAllowlistFlag = "gh-team-allowlist" + GHTokenFlag = "gh-token" + GHUserFlag = "gh-user" + GHAppIDFlag = "gh-app-id" + GHAppKeyFlag = "gh-app-key" + GHAppKeyFileFlag = "gh-app-key-file" + GHAppSlugFlag = "gh-app-slug" + GHOrganizationFlag = "gh-org" + GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec + GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec + GitlabHostnameFlag = "gitlab-hostname" + GitlabTokenFlag = "gitlab-token" + GitlabUserFlag = "gitlab-user" + GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec + APISecretFlag = "api-secret" + HidePrevPlanComments = "hide-prev-plan-comments" + LockingDBType = "locking-db-type" + LogLevelFlag = "log-level" + MarkdownTemplateOverridesDirFlag = "markdown-template-overrides-dir" + ParallelPoolSize = "parallel-pool-size" + StatsNamespace = "stats-namespace" + AllowDraftPRs = "allow-draft-prs" + PortFlag = "port" + RedisDB = "redis-db" + RedisHost = "redis-host" + RedisPassword = "redis-password" + RedisPort = "redis-port" + RedisTLSEnabled = "redis-tls-enabled" + RedisInsecureSkipVerify = "redis-insecure-skip-verify" + RepoConfigFlag = "repo-config" + RepoConfigJSONFlag = "repo-config-json" // RepoWhitelistFlag is deprecated for RepoAllowlistFlag. RepoWhitelistFlag = "repo-whitelist" RepoAllowlistFlag = "repo-allowlist" @@ -122,30 +123,31 @@ const ( WebsocketCheckOrigin = "websocket-check-origin" // NOTE: Must manually set these as defaults in the setDefaults function. - DefaultADBasicUser = "" - DefaultADBasicPassword = "" - DefaultADHostname = "dev.azure.com" - DefaultAutoplanFileList = "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl" - DefaultCheckoutStrategy = "branch" - DefaultBitbucketBaseURL = bitbucketcloud.BaseURL - DefaultDataDir = "~/.atlantis" - DefaultGHHostname = "github.com" - DefaultGitlabHostname = "gitlab.com" - DefaultLockingDBType = "boltdb" - DefaultLogLevel = "info" - DefaultParallelPoolSize = 15 - DefaultStatsNamespace = "atlantis" - DefaultPort = 4141 - DefaultRedisDB = 0 - DefaultRedisPort = 6379 - DefaultRedisTLSEnabled = false - DefaultRedisInsecureSkipVerify = false - DefaultTFDownloadURL = "https://releases.hashicorp.com" - DefaultTFEHostname = "app.terraform.io" - DefaultVCSStatusName = "atlantis" - DefaultWebBasicAuth = false - DefaultWebUsername = "atlantis" - DefaultWebPassword = "atlantis" + DefaultADBasicUser = "" + DefaultADBasicPassword = "" + DefaultADHostname = "dev.azure.com" + DefaultAutoplanFileList = "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl" + DefaultCheckoutStrategy = "branch" + DefaultBitbucketBaseURL = bitbucketcloud.BaseURL + DefaultDataDir = "~/.atlantis" + DefaultMarkdownTemplateOverridesDir = "~/.markdown_templates" + DefaultGHHostname = "github.com" + DefaultGitlabHostname = "gitlab.com" + DefaultLockingDBType = "boltdb" + DefaultLogLevel = "info" + DefaultParallelPoolSize = 15 + DefaultStatsNamespace = "atlantis" + DefaultPort = 4141 + DefaultRedisDB = 0 + DefaultRedisPort = 6379 + DefaultRedisTLSEnabled = false + DefaultRedisInsecureSkipVerify = false + DefaultTFDownloadURL = "https://releases.hashicorp.com" + DefaultTFEHostname = "app.terraform.io" + DefaultVCSStatusName = "atlantis" + DefaultWebBasicAuth = false + DefaultWebUsername = "atlantis" + DefaultWebPassword = "atlantis" ) var stringFlags = map[string]stringFlag{ @@ -285,6 +287,10 @@ var stringFlags = map[string]stringFlag{ description: "Log level. Either debug, info, warn, or error.", defaultValue: DefaultLogLevel, }, + MarkdownTemplateOverridesDirFlag: { + description: "Directory for custom overrides to the markdown templates used for comments.", + defaultValue: DefaultMarkdownTemplateOverridesDir, + }, StatsNamespace: { description: "Namespace for aggregating stats.", defaultValue: DefaultStatsNamespace, @@ -671,6 +677,9 @@ func (s *ServerCmd) run() error { if err := s.setDataDir(&userConfig); err != nil { return err } + if err := s.setMarkdownTemplateOverridesDir(&userConfig); err != nil { + return err + } s.setVarFileAllowlist(&userConfig) if err := s.deprecationWarnings(&userConfig); err != nil { return err @@ -722,6 +731,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) { if c.LogLevel == "" { c.LogLevel = DefaultLogLevel } + if c.MarkdownTemplateOverridesDir == "" { + c.MarkdownTemplateOverridesDir = DefaultMarkdownTemplateOverridesDir + } if c.ParallelPoolSize == 0 { c.ParallelPoolSize = DefaultParallelPoolSize } @@ -887,6 +899,30 @@ func (s *ServerCmd) setDataDir(userConfig *server.UserConfig) error { return nil } +// setMarkdownTemplateOverridesDir checks if ~ was used in markdown-template-overrides-dir and converts it to the actual +// home directory. If we don't do this, we'll create a directory called "~" +// instead of actually using home. It also converts relative paths to absolute. +func (s *ServerCmd) setMarkdownTemplateOverridesDir(userConfig *server.UserConfig) error { + finalPath := userConfig.MarkdownTemplateOverridesDir + + // Convert ~ to the actual home dir. + if strings.HasPrefix(finalPath, "~/") { + var err error + finalPath, err = homedir.Expand(finalPath) + if err != nil { + return errors.Wrap(err, "determining home directory") + } + } + + // Convert relative paths to absolute. + finalPath, err := filepath.Abs(finalPath) + if err != nil { + return errors.Wrap(err, "making markdown-template-overrides-dir absolute") + } + userConfig.MarkdownTemplateOverridesDir = finalPath + return nil +} + // setVarFileAllowlist checks if var-file-allowlist is unassigned and makes it default to data-dir for better backward // compatibility. func (s *ServerCmd) setVarFileAllowlist(userConfig *server.UserConfig) { diff --git a/cmd/server_test.go b/cmd/server_test.go index c8110dfa48..09941e4aba 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -51,66 +51,67 @@ func (s *ServerStarterMock) Start() error { // Adding a new flag? Add it to this slice for testing in alphabetical // order. var testFlags = map[string]interface{}{ - ADTokenFlag: "ad-token", - ADUserFlag: "ad-user", - ADWebhookPasswordFlag: "ad-wh-pass", - ADWebhookUserFlag: "ad-wh-user", - AtlantisURLFlag: "url", - AllowForkPRsFlag: true, - AllowRepoConfigFlag: true, - AutomergeFlag: true, - AutoplanFileListFlag: "**/*.tf,**/*.yml", - BitbucketBaseURLFlag: "https://bitbucket-base-url.com", - BitbucketTokenFlag: "bitbucket-token", - BitbucketUserFlag: "bitbucket-user", - BitbucketWebhookSecretFlag: "bitbucket-secret", - CheckoutStrategyFlag: "merge", - DataDirFlag: "/path", - DefaultTFVersionFlag: "v0.11.0", - DisableApplyAllFlag: true, - DisableApplyFlag: true, - DisableMarkdownFoldingFlag: true, - DisableRepoLockingFlag: true, - GHHostnameFlag: "ghhostname", - GHTokenFlag: "token", - GHUserFlag: "user", - GHAppIDFlag: int64(0), - GHAppKeyFlag: "", - GHAppKeyFileFlag: "", - GHAppSlugFlag: "atlantis", - GHOrganizationFlag: "", - GHWebhookSecretFlag: "secret", - GitlabHostnameFlag: "gitlab-hostname", - GitlabTokenFlag: "gitlab-token", - GitlabUserFlag: "gitlab-user", - GitlabWebhookSecretFlag: "gitlab-secret", - LockingDBType: "boltdb", - LogLevelFlag: "debug", - StatsNamespace: "atlantis", - AllowDraftPRs: true, - PortFlag: 8181, - ParallelPoolSize: 100, - RepoAllowlistFlag: "github.com/runatlantis/atlantis", - RequireApprovalFlag: true, - RequireMergeableFlag: true, - SilenceNoProjectsFlag: false, - SilenceForkPRErrorsFlag: true, - SilenceAllowlistErrorsFlag: true, - SilenceVCSStatusNoPlans: true, - SkipCloneNoChanges: true, - SlackTokenFlag: "slack-token", - SSLCertFileFlag: "cert-file", - SSLKeyFileFlag: "key-file", - TFDownloadURLFlag: "https://my-hostname.com", - TFEHostnameFlag: "my-hostname", - TFELocalExecutionModeFlag: true, - TFETokenFlag: "my-token", - VCSStatusName: "my-status", - WriteGitCredsFlag: true, - DisableAutoplanFlag: true, - EnablePolicyChecksFlag: false, - EnableRegExpCmdFlag: false, - EnableDiffMarkdownFormat: false, + ADTokenFlag: "ad-token", + ADUserFlag: "ad-user", + ADWebhookPasswordFlag: "ad-wh-pass", + ADWebhookUserFlag: "ad-wh-user", + AtlantisURLFlag: "url", + AllowForkPRsFlag: true, + AllowRepoConfigFlag: true, + AutomergeFlag: true, + AutoplanFileListFlag: "**/*.tf,**/*.yml", + BitbucketBaseURLFlag: "https://bitbucket-base-url.com", + BitbucketTokenFlag: "bitbucket-token", + BitbucketUserFlag: "bitbucket-user", + BitbucketWebhookSecretFlag: "bitbucket-secret", + CheckoutStrategyFlag: "merge", + DataDirFlag: "/path", + DefaultTFVersionFlag: "v0.11.0", + DisableApplyAllFlag: true, + DisableApplyFlag: true, + DisableMarkdownFoldingFlag: true, + DisableRepoLockingFlag: true, + GHHostnameFlag: "ghhostname", + GHTokenFlag: "token", + GHUserFlag: "user", + GHAppIDFlag: int64(0), + GHAppKeyFlag: "", + GHAppKeyFileFlag: "", + GHAppSlugFlag: "atlantis", + GHOrganizationFlag: "", + GHWebhookSecretFlag: "secret", + GitlabHostnameFlag: "gitlab-hostname", + GitlabTokenFlag: "gitlab-token", + GitlabUserFlag: "gitlab-user", + GitlabWebhookSecretFlag: "gitlab-secret", + LockingDBType: "boltdb", + LogLevelFlag: "debug", + MarkdownTemplateOverridesDirFlag: "/path2", + StatsNamespace: "atlantis", + AllowDraftPRs: true, + PortFlag: 8181, + ParallelPoolSize: 100, + RepoAllowlistFlag: "github.com/runatlantis/atlantis", + RequireApprovalFlag: true, + RequireMergeableFlag: true, + SilenceNoProjectsFlag: false, + SilenceForkPRErrorsFlag: true, + SilenceAllowlistErrorsFlag: true, + SilenceVCSStatusNoPlans: true, + SkipCloneNoChanges: true, + SlackTokenFlag: "slack-token", + SSLCertFileFlag: "cert-file", + SSLKeyFileFlag: "key-file", + TFDownloadURLFlag: "https://my-hostname.com", + TFEHostnameFlag: "my-hostname", + TFELocalExecutionModeFlag: true, + TFETokenFlag: "my-token", + VCSStatusName: "my-status", + WriteGitCredsFlag: true, + DisableAutoplanFlag: true, + EnablePolicyChecksFlag: false, + EnableRegExpCmdFlag: false, + EnableDiffMarkdownFormat: false, } func TestExecute_Defaults(t *testing.T) { @@ -128,17 +129,20 @@ func TestExecute_Defaults(t *testing.T) { hostname, err := os.Hostname() Ok(t, err) - // Get our home dir since that's what data-dir defaulted to. + // Get our home dir since that's what data-dir and markdown-template-overrides-dir defaulted to. dataDir, err := homedir.Expand("~/.atlantis") Ok(t, err) + markdownTemplateOverridesDir, err := homedir.Expand("~/.markdown_templates") + Ok(t, err) strExceptions := map[string]string{ - GHUserFlag: "user", - GHTokenFlag: "token", - DataDirFlag: dataDir, - AtlantisURLFlag: "http://" + hostname + ":4141", - RepoAllowlistFlag: "*", - VarFileAllowlistFlag: dataDir, + GHUserFlag: "user", + GHTokenFlag: "token", + DataDirFlag: dataDir, + MarkdownTemplateOverridesDirFlag: markdownTemplateOverridesDir, + AtlantisURLFlag: "http://" + hostname + ":4141", + RepoAllowlistFlag: "*", + VarFileAllowlistFlag: dataDir, } strIgnore := map[string]bool{ "config": true, diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 16a9402887..304cbafb27 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -456,6 +456,21 @@ Values are chosen in this order: ``` Log level. Defaults to `info`. +### `--markdown-template-overrides-dir` + ```bash + atlantis server --markdown-template-overrides-dir="path/to/templates/" + ``` + Directory where Atlantis will read in overrides for markdown templates used to render comments on pull requests. + Markdown template overrides may be specified either in individual files, or all together in a single file. All template + override files _must_ have the `.tmpl` extension, otherwise they will not be parsed. + + Markdown templates which may have overrides can be found [here](https://github.com/runatlantis/atlantis/tree/master/server/events/templates) + + Please be mindful that settings like `--enable-diff-markdown-format` depend on logic defined in the templates. It is + possible to diverge from expected behavior, if care is not taken when overriding default templates. + + Defaults to the atlantis home directory `/home/atlantis/.markdown_templates/` in `/$HOME/.markdown_templates`. + ### `--parallel-pool-size` ```bash atlantis server --parallel-pool-size=100 diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 400174bd8e..6ec7f164de 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1036,7 +1036,7 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl pullUpdater := &events.PullUpdater{ HidePrevPlanComments: false, VCSClient: e2eVCSClient, - MarkdownRenderer: &events.MarkdownRenderer{}, + MarkdownRenderer: events.GetMarkdownRenderer(false, false, false, false, false, false, ""), } autoMerger := &events.AutoMerger{ diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 64faaf2ef8..d39b97b123 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -99,7 +99,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { pullUpdater = &events.PullUpdater{ HidePrevPlanComments: false, VCSClient: vcsClient, - MarkdownRenderer: &events.MarkdownRenderer{}, + MarkdownRenderer: events.GetMarkdownRenderer(false, false, false, false, false, false, ""), } autoMerger = &events.AutoMerger{ diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go index c9605004cf..74960dec49 100644 --- a/server/events/markdown_renderer.go +++ b/server/events/markdown_renderer.go @@ -15,6 +15,7 @@ package events import ( "bytes" + "embed" "fmt" "strings" "text/template" @@ -33,6 +34,9 @@ var ( // maxUnwrappedLines is the maximum number of lines the Terraform output // can be before we wrap it in an expandable template. maxUnwrappedLines = 12 + + //go:embed templates/* + templatesFS embed.FS ) // MarkdownRenderer renders responses as markdown. @@ -46,6 +50,7 @@ type MarkdownRenderer struct { DisableMarkdownFolding bool DisableRepoLocking bool EnableDiffMarkdownFormat bool + MarkdownTemplates *template.Template } // commonData is data that all responses have. @@ -98,6 +103,33 @@ type projectResultTmplData struct { Rendered string } +// Initialize templates +func GetMarkdownRenderer( + GitlabSupportsCommonMark bool, + DisableApplyAll bool, + DisableApply bool, + DisableMarkdownFolding bool, + DisableRepoLocking bool, + EnableDiffMarkdownFormat bool, + MarkdownTemplateOverridesDir string, +) *MarkdownRenderer { + var templates *template.Template + templates, _ = template.New("").Funcs(sprig.TxtFuncMap()).ParseFS(templatesFS, "templates/*.tmpl") + if overrides, err := templates.ParseGlob(fmt.Sprintf("%s/*.tmpl", MarkdownTemplateOverridesDir)); err == nil { + // doesn't override if templates directory doesn't exist + templates = overrides + } + return &MarkdownRenderer{ + GitlabSupportsCommonMark: GitlabSupportsCommonMark, + DisableApplyAll: DisableApplyAll, + DisableMarkdownFolding: DisableMarkdownFolding, + DisableApply: DisableApply, + DisableRepoLocking: DisableRepoLocking, + EnableDiffMarkdownFormat: EnableDiffMarkdownFormat, + MarkdownTemplates: templates, + } +} + // Render formats the data into a markdown string. // nolint: interfacer func (m *MarkdownRenderer) Render(res command.Result, cmdName command.Name, log string, verbose bool, vcsHost models.VCSHostType) string { @@ -112,11 +144,14 @@ func (m *MarkdownRenderer) Render(res command.Result, cmdName command.Name, log DisableRepoLocking: m.DisableRepoLocking, EnableDiffMarkdownFormat: m.EnableDiffMarkdownFormat, } + + templates := m.MarkdownTemplates + if res.Error != nil { - return m.renderTemplate(unwrappedErrWithLogTmpl, errData{res.Error.Error(), common}) + return m.renderTemplate(templates.Lookup("unwrappedErrWithLog"), errData{res.Error.Error(), common}) } if res.Failure != "" { - return m.renderTemplate(failureWithLogTmpl, failureData{res.Failure, common}) + return m.renderTemplate(templates.Lookup("failureWithLog"), failureData{res.Failure, common}) } return m.renderProjectResults(res.ProjectResults, common, vcsHost) } @@ -127,6 +162,8 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, numPolicyCheckSuccesses := 0 numVersionSuccesses := 0 + templates := m.MarkdownTemplates + for _, result := range results { resultData := projectResultTmplData{ Workspace: result.Workspace, @@ -134,9 +171,9 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, ProjectName: result.ProjectName, } if result.Error != nil { - tmpl := unwrappedErrTmpl + tmpl := templates.Lookup("unwrappedErr") if m.shouldUseWrappedTmpl(vcsHost, result.Error.Error()) { - tmpl = wrappedErrTmpl + tmpl = templates.Lookup("wrappedErr") } resultData.Rendered = m.renderTemplate(tmpl, struct { Command string @@ -146,7 +183,7 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, Error: result.Error.Error(), }) } else if result.Failure != "" { - resultData.Rendered = m.renderTemplate(failureTmpl, struct { + resultData.Rendered = m.renderTemplate(templates.Lookup("failure"), struct { Command string Failure string }{ @@ -155,29 +192,29 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, }) } else if result.PlanSuccess != nil { if m.shouldUseWrappedTmpl(vcsHost, result.PlanSuccess.TerraformOutput) { - resultData.Rendered = m.renderTemplate(planSuccessWrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanSummary: result.PlanSuccess.Summary(), PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply, DisableRepoLocking: common.DisableRepoLocking, EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat}) + resultData.Rendered = m.renderTemplate(templates.Lookup("planSuccessWrapped"), planSuccessData{PlanSuccess: *result.PlanSuccess, PlanSummary: result.PlanSuccess.Summary(), PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply, DisableRepoLocking: common.DisableRepoLocking, EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat}) } else { - resultData.Rendered = m.renderTemplate(planSuccessUnwrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply, DisableRepoLocking: common.DisableRepoLocking, EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat}) + resultData.Rendered = m.renderTemplate(templates.Lookup("planSuccessUnwrapped"), planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply, DisableRepoLocking: common.DisableRepoLocking, EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat}) } numPlanSuccesses++ } else if result.PolicyCheckSuccess != nil { if m.shouldUseWrappedTmpl(vcsHost, result.PolicyCheckSuccess.PolicyCheckOutput) { - resultData.Rendered = m.renderTemplate(policyCheckSuccessWrappedTmpl, policyCheckSuccessData{PolicyCheckSuccess: *result.PolicyCheckSuccess}) + resultData.Rendered = m.renderTemplate(templates.Lookup("policyCheckSuccessWrapped"), policyCheckSuccessData{PolicyCheckSuccess: *result.PolicyCheckSuccess}) } else { - resultData.Rendered = m.renderTemplate(policyCheckSuccessUnwrappedTmpl, policyCheckSuccessData{PolicyCheckSuccess: *result.PolicyCheckSuccess}) + resultData.Rendered = m.renderTemplate(templates.Lookup("policyCheckSuccessUnwrapped"), policyCheckSuccessData{PolicyCheckSuccess: *result.PolicyCheckSuccess}) } numPolicyCheckSuccesses++ } else if result.ApplySuccess != "" { if m.shouldUseWrappedTmpl(vcsHost, result.ApplySuccess) { - resultData.Rendered = m.renderTemplate(applyWrappedSuccessTmpl, struct{ Output string }{result.ApplySuccess}) + resultData.Rendered = m.renderTemplate(templates.Lookup("applyWrappedSuccess"), struct{ Output string }{result.ApplySuccess}) } else { - resultData.Rendered = m.renderTemplate(applyUnwrappedSuccessTmpl, struct{ Output string }{result.ApplySuccess}) + resultData.Rendered = m.renderTemplate(templates.Lookup("applyUnwrappedSuccess"), struct{ Output string }{result.ApplySuccess}) } } else if result.VersionSuccess != "" { if m.shouldUseWrappedTmpl(vcsHost, result.VersionSuccess) { - resultData.Rendered = m.renderTemplate(versionWrappedSuccessTmpl, struct{ Output string }{result.VersionSuccess}) + resultData.Rendered = m.renderTemplate(templates.Lookup("versionWrappedSuccess"), struct{ Output string }{result.VersionSuccess}) } else { - resultData.Rendered = m.renderTemplate(versionUnwrappedSuccessTmpl, struct{ Output string }{result.VersionSuccess}) + resultData.Rendered = m.renderTemplate(templates.Lookup("versionUnwrappedSuccess"), struct{ Output string }{result.VersionSuccess}) } numVersionSuccesses++ } else { @@ -189,28 +226,28 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, var tmpl *template.Template switch { case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses > 0: - tmpl = singleProjectPlanSuccessTmpl + tmpl = templates.Lookup("singleProjectPlanSuccess") case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses == 0: - tmpl = singleProjectPlanUnsuccessfulTmpl + tmpl = templates.Lookup("singleProjectPlanUnsuccessful") case len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses > 0: - tmpl = singleProjectPlanSuccessTmpl + tmpl = templates.Lookup("singleProjectPlanSuccess") case len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses == 0: - tmpl = singleProjectPlanUnsuccessfulTmpl + tmpl = templates.Lookup("singleProjectPlanUnsuccessful") case len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses > 0: - tmpl = singleProjectVersionSuccessTmpl + tmpl = templates.Lookup("singleProjectVersionSuccess") case len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses == 0: - tmpl = singleProjectVersionUnsuccessfulTmpl + tmpl = templates.Lookup("singleProjectVersionUnsuccessful") case len(resultsTmplData) == 1 && common.Command == applyCommandTitle: - tmpl = singleProjectApplyTmpl + tmpl = templates.Lookup("singleProjectApply") case common.Command == planCommandTitle, common.Command == policyCheckCommandTitle: - tmpl = multiProjectPlanTmpl + tmpl = templates.Lookup("multiProjectPlan") case common.Command == approvePoliciesCommandTitle: - tmpl = approveAllProjectsTmpl + tmpl = templates.Lookup("approveAllProjects") case common.Command == applyCommandTitle: - tmpl = multiProjectApplyTmpl + tmpl = templates.Lookup("multiProjectApply") case common.Command == versionCommandTitle: - tmpl = multiProjectVersionTmpl + tmpl = templates.Lookup("multiProjectVersion") default: return "no template matched–this is a bug" } @@ -245,146 +282,3 @@ func (m *MarkdownRenderer) renderTemplate(tmpl *template.Template, data interfac } return buf.String() } - -// todo: refactor to remove duplication #refactor -var singleProjectApplyTmpl = template.Must(template.New("").Parse( - "{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n{{$result.Rendered}}\n" + logTmpl)) -var singleProjectPlanSuccessTmpl = template.Must(template.New("").Parse( - "{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n{{$result.Rendered}}\n" + - "\n" + - "{{ if ne .DisableApplyAll true }}---\n" + - "* :fast_forward: To **apply** all unapplied plans from this pull request, comment:\n" + - " * `atlantis apply`\n" + - "* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:\n" + - " * `atlantis unlock`{{ end }}" + logTmpl)) -var singleProjectPlanUnsuccessfulTmpl = template.Must(template.New("").Parse( - "{{$result := index .Results 0}}Ran {{.Command}} for dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n" + - "{{$result.Rendered}}\n" + logTmpl)) -var singleProjectVersionSuccessTmpl = template.Must(template.New("").Parse( - "{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n{{$result.Rendered}}\n" + logTmpl)) -var singleProjectVersionUnsuccessfulTmpl = template.Must(template.New("").Parse( - "{{$result := index .Results 0}}Ran {{.Command}} for dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n{{$result.Rendered}}\n" + logTmpl)) -var approveAllProjectsTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse( - "Approved Policies for {{ len .Results }} projects:\n\n" + - "{{ range $result := .Results }}" + - "1. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + - "{{end}}\n" + logTmpl)) -var multiProjectPlanTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse( - "Ran {{.Command}} for {{ len .Results }} projects:\n\n" + - "{{ range $result := .Results }}" + - "1. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + - "{{end}}\n" + - "{{ $disableApplyAll := .DisableApplyAll }}{{ range $i, $result := .Results }}" + - "### {{add $i 1}}. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + - "{{$result.Rendered}}\n\n" + - "{{ if ne $disableApplyAll true }}---\n{{end}}{{end}}{{ if ne .DisableApplyAll true }}{{ if and (gt (len .Results) 0) (not .PlansDeleted) }}* :fast_forward: To **apply** all unapplied plans from this pull request, comment:\n" + - " * `atlantis apply`\n" + - "* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:\n" + - " * `atlantis unlock`" + - "{{end}}{{end}}" + - logTmpl)) -var multiProjectApplyTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse( - "Ran {{.Command}} for {{ len .Results }} projects:\n\n" + - "{{ range $result := .Results }}" + - "1. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + - "{{end}}\n" + - "{{ range $i, $result := .Results }}" + - "### {{add $i 1}}. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + - "{{$result.Rendered}}\n\n" + - "---\n{{end}}" + - logTmpl)) -var multiProjectVersionTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse( - "Ran {{.Command}} for {{ len .Results }} projects:\n\n" + - "{{ range $result := .Results }}" + - "1. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + - "{{end}}\n" + - "{{ range $i, $result := .Results }}" + - "### {{add $i 1}}. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + - "{{$result.Rendered}}\n\n" + - "---\n{{end}}" + - logTmpl)) -var planSuccessUnwrappedTmpl = template.Must(template.New("").Parse( - "```diff\n" + - "{{ if .EnableDiffMarkdownFormat }}{{.DiffMarkdownFormattedTerraformOutput}}{{else}}{{.TerraformOutput}}{{end}}\n" + - "```\n\n" + planNextSteps + - "{{ if .HasDiverged }}\n\n:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.{{end}}")) - -var planSuccessWrappedTmpl = template.Must(template.New("").Parse( - "
Show Output\n\n" + - "```diff\n" + - "{{ if .EnableDiffMarkdownFormat }}{{.DiffMarkdownFormattedTerraformOutput}}{{else}}{{.TerraformOutput}}{{end}}\n" + - "```\n\n" + - planNextSteps + "\n" + - "
" + "\n" + - "{{.PlanSummary}}" + - "{{ if .HasDiverged }}\n\n:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.{{end}}")) - -var policyCheckSuccessUnwrappedTmpl = template.Must(template.New("").Parse( - "```diff\n" + - "{{.PolicyCheckOutput}}\n" + - "```\n\n" + policyCheckNextSteps + - "{{ if .HasDiverged }}\n\n:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.{{end}}")) - -var policyCheckSuccessWrappedTmpl = template.Must(template.New("").Parse( - "
Show Output\n\n" + - "```diff\n" + - "{{.PolicyCheckOutput}}\n" + - "```\n\n" + - policyCheckNextSteps + "\n" + - "
" + - "{{ if .HasDiverged }}\n\n:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.{{end}}")) - -// policyCheckNextSteps are instructions appended after successful plans as to what -// to do next. -var policyCheckNextSteps = "* :arrow_forward: To **apply** this plan, comment:\n" + - " * `{{.ApplyCmd}}`\n" + - "* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n" + - "* :repeat: To re-run policies **plan** this project again by commenting:\n" + - " * `{{.RePlanCmd}}`" - -// planNextSteps are instructions appended after successful plans as to what -// to do next. -var planNextSteps = "{{ if .PlanWasDeleted }}This plan was not saved because one or more projects failed and automerge requires all plans pass.{{ else }}" + - "{{ if not .DisableApply }}* :arrow_forward: To **apply** this plan, comment:\n" + - " * `{{.ApplyCmd}}`\n{{end}}" + - "{{ if not .DisableRepoLocking }}* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n{{end}}" + - "* :repeat: To **plan** this project again, comment:\n" + - " * `{{.RePlanCmd}}`{{end}}" -var applyUnwrappedSuccessTmpl = template.Must(template.New("").Parse( - "```diff\n" + - "{{.Output}}\n" + - "```")) -var applyWrappedSuccessTmpl = template.Must(template.New("").Parse( - "
Show Output\n\n" + - "```diff\n" + - "{{.Output}}\n" + - "```\n" + - "
")) -var versionUnwrappedSuccessTmpl = template.Must(template.New("").Parse("```\n{{.Output}}```")) -var versionWrappedSuccessTmpl = template.Must(template.New("").Parse( - "
Show Output\n\n" + - "```\n" + - "{{.Output}}" + - "```\n" + - "
")) -var unwrappedErrTmplText = "**{{.Command}} Error**\n" + - "```\n" + - "{{.Error}}\n" + - "```" + - "{{ if eq .Command \"Policy Check\" }}" + - "\n* :heavy_check_mark: To **approve** failing policies an authorized approver can comment:\n" + - " * `atlantis approve_policies`\n" + - "* :repeat: Or, address the policy failure by modifying the codebase and re-planning.\n" + - "{{ end }}" -var wrappedErrTmplText = "**{{.Command}} Error**\n" + - "
Show Output\n\n" + - "```\n" + - "{{.Error}}\n" + - "```\n
" -var unwrappedErrTmpl = template.Must(template.New("").Parse(unwrappedErrTmplText)) -var unwrappedErrWithLogTmpl = template.Must(template.New("").Parse(unwrappedErrTmplText + logTmpl)) -var wrappedErrTmpl = template.Must(template.New("").Parse(wrappedErrTmplText)) -var failureTmplText = "**{{.Command}} Failed**: {{.Failure}}" -var failureTmpl = template.Must(template.New("").Parse(failureTmplText)) -var failureWithLogTmpl = template.Must(template.New("").Parse(failureTmplText + logTmpl)) -var logTmpl = "{{if .Verbose}}\n
Log\n

\n\n```\n{{.Log}}```\n

{{end}}\n" diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index 779bc546d4..f7b4a41b8b 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -16,6 +16,7 @@ package events_test import ( "errors" "fmt" + "os" "strings" "testing" @@ -56,7 +57,7 @@ func TestRenderErr(t *testing.T) { }, } - r := events.MarkdownRenderer{} + r := events.GetMarkdownRenderer(false, false, false, false, false, false, "") for _, c := range cases { res := command.Result{ Error: c.Error, @@ -101,7 +102,7 @@ func TestRenderFailure(t *testing.T) { }, } - r := events.MarkdownRenderer{} + r := events.GetMarkdownRenderer(false, false, false, false, false, false, "") for _, c := range cases { res := command.Result{ Failure: c.Failure, @@ -120,7 +121,7 @@ func TestRenderFailure(t *testing.T) { } func TestRenderErrAndFailure(t *testing.T) { - r := events.MarkdownRenderer{} + r := events.GetMarkdownRenderer(false, false, false, false, false, false, "") res := command.Result{ Error: errors.New("error"), Failure: "failure", @@ -750,7 +751,7 @@ $$$ }, } - r := events.MarkdownRenderer{} + r := events.GetMarkdownRenderer(false, false, false, false, false, false, "") for _, c := range cases { t.Run(c.Description, func(t *testing.T) { res := command.Result{ @@ -901,9 +902,15 @@ $$$ `, }, } - r := events.MarkdownRenderer{ - DisableApplyAll: true, - } + r := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + true, // DisableApplyAll + false, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) for _, c := range cases { t.Run(c.Description, func(t *testing.T) { res := command.Result{ @@ -1046,10 +1053,16 @@ $$$ `, }, } - r := events.MarkdownRenderer{ - DisableApplyAll: true, - DisableApply: true, - } + + r := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + true, // DisableApplyAll + true, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) for _, c := range cases { t.Run(c.Description, func(t *testing.T) { res := command.Result{ @@ -1070,11 +1083,55 @@ $$$ } } +// Run policy check with a custom template to validate custom template rendering. +func TestRenderCustomPolicyCheckTemplate_DisableApplyAll(t *testing.T) { + tmpDir, cleanup := TempDir(t) + filePath := fmt.Sprintf("%s/templates.tmpl", tmpDir) + _, err := os.Create(filePath) + Ok(t, err) + err = os.WriteFile(filePath, []byte("{{ define \"policyCheckSuccessUnwrapped\" -}}somecustometext{{- end}}\n"), 0600) + Ok(t, err) + defer cleanup() + r := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + true, // DisableApplyAll + true, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + tmpDir, // MarkdownTemplateOverridesDir + ) + + rendered := r.Render(command.Result{ + ProjectResults: []command.ProjectResult{ + { + Workspace: "workspace", + RepoRelDir: "path", + PolicyCheckSuccess: &models.PolicyCheckSuccess{ + PolicyCheckOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + LockURL: "lock-url", + ApplyCmd: "atlantis apply -d path -w workspace", + RePlanCmd: "atlantis plan -d path -w workspace", + }, + }, + }, + }, command.PolicyCheck, "log", false, models.Github) + fmt.Println(rendered) + Equals(t, rendered, "Ran Policy Check for dir: `path` workspace: `workspace`\n\nsomecustometext\n\n\n") + +} + // Test that if folding is disabled that it's not used. func TestRenderProjectResults_DisableFolding(t *testing.T) { - mr := events.MarkdownRenderer{ - DisableMarkdownFolding: true, - } + mr := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + false, // DisableApplyAll + false, // DisableApply + true, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) rendered := mr.Render(command.Result{ ProjectResults: []command.ProjectResult{ @@ -1156,9 +1213,15 @@ func TestRenderProjectResults_WrappedErr(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 := events.GetMarkdownRenderer( + c.GitlabCommonMarkSupport, // GitlabSupportsCommonMark + false, // DisableApplyAll + false, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) rendered := mr.Render(command.Result{ ProjectResults: []command.ProjectResult{ @@ -1268,9 +1331,15 @@ 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 := events.GetMarkdownRenderer( + c.GitlabCommonMarkSupport, // GitlabSupportsCommonMark + false, // DisableApplyAll + false, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) var pr command.ProjectResult switch cmd { case command.Plan: @@ -1373,7 +1442,15 @@ $$$ } func TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) { - mr := events.MarkdownRenderer{} + mr := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + false, // DisableApplyAll + false, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) tfOut := strings.Repeat("line\n", 13) rendered := mr.Render(command.Result{ ProjectResults: []command.ProjectResult{ @@ -1419,7 +1496,15 @@ $$$ } func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) { - mr := events.MarkdownRenderer{} + mr := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + false, // DisableApplyAll + false, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) tfOut := strings.Repeat("line\n", 13) + "Plan: 1 to add, 0 to change, 0 to destroy." rendered := mr.Render(command.Result{ ProjectResults: []command.ProjectResult{ @@ -1592,7 +1677,15 @@ This plan was not saved because one or more projects failed and automerge requir for name, c := range cases { t.Run(name, func(t *testing.T) { - mr := events.MarkdownRenderer{} + mr := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + false, // DisableApplyAll + false, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) rendered := mr.Render(c.cr, command.Plan, "log", false, models.Github) expWithBackticks := strings.Replace(c.exp, "$", "`", -1) Equals(t, expWithBackticks, rendered) @@ -2052,7 +2145,15 @@ $$$ }, } - r := events.MarkdownRenderer{} + r := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + false, // DisableApplyAll + false, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + false, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) r.DisableRepoLocking = true for _, c := range cases { t.Run(c.Description, func(t *testing.T) { @@ -2481,11 +2582,16 @@ Plan: 1 to add, 2 to change, 1 to destroy. `, }, } - r := events.MarkdownRenderer{ - DisableApplyAll: true, - DisableApply: true, - EnableDiffMarkdownFormat: true, - } + r := events.GetMarkdownRenderer( + false, // GitlabSupportsCommonMark + true, // DisableApplyAll + true, // DisableApply + false, // DisableMarkdownFolding + false, // DisableRepoLocking + true, // EnableDiffMarkdownFormat + "", // MarkdownTemplateOverridesDir + ) + for _, c := range cases { t.Run(c.Description, func(t *testing.T) { res := command.Result{ diff --git a/server/events/templates/apply_unwrapped_success.tmpl b/server/events/templates/apply_unwrapped_success.tmpl new file mode 100644 index 0000000000..30a894b231 --- /dev/null +++ b/server/events/templates/apply_unwrapped_success.tmpl @@ -0,0 +1,5 @@ +{{ define "applyUnwrappedSuccess" -}} +```diff +{{.Output}} +``` +{{- end }} diff --git a/server/events/templates/apply_wrapped_success.tmpl b/server/events/templates/apply_wrapped_success.tmpl new file mode 100644 index 0000000000..c5d16a3cd5 --- /dev/null +++ b/server/events/templates/apply_wrapped_success.tmpl @@ -0,0 +1,6 @@ +{{ define "applyWrappedSuccess" -}} +
Show Output + +{{ template "applyUnwrappedSuccess" . }} +
+{{- end }} diff --git a/server/events/templates/approve_all_projects.tmpl b/server/events/templates/approve_all_projects.tmpl new file mode 100644 index 0000000000..8ce1d2d953 --- /dev/null +++ b/server/events/templates/approve_all_projects.tmpl @@ -0,0 +1,7 @@ +{{ define "approveAllProjects" -}} +Approved Policies for {{ len .Results }} projects: + +{{ range $result := .Results }}1. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` +{{end}} +{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/diverged.tmpl b/server/events/templates/diverged.tmpl new file mode 100644 index 0000000000..092f5d8a60 --- /dev/null +++ b/server/events/templates/diverged.tmpl @@ -0,0 +1,5 @@ +{{ define "diverged" -}} +{{ if .HasDiverged }} + +:warning: The branch we're merging into is ahead, it is recommended to pull new commits first.{{end}} +{{- end }} diff --git a/server/events/templates/failure.tmpl b/server/events/templates/failure.tmpl new file mode 100644 index 0000000000..ea7078066f --- /dev/null +++ b/server/events/templates/failure.tmpl @@ -0,0 +1,3 @@ +{{ define "failure" -}} +**{{.Command}} Failed**: {{.Failure}} +{{- end }} diff --git a/server/events/templates/failure_with_log.tmpl b/server/events/templates/failure_with_log.tmpl new file mode 100644 index 0000000000..371be01a12 --- /dev/null +++ b/server/events/templates/failure_with_log.tmpl @@ -0,0 +1,3 @@ +{{ define "failureWithLog" -}} +{{ template "failure" . }}{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/log.tmpl b/server/events/templates/log.tmpl new file mode 100644 index 0000000000..4dbb97c7a8 --- /dev/null +++ b/server/events/templates/log.tmpl @@ -0,0 +1,9 @@ +{{ define "log" -}} +{{if .Verbose}} +
Log +

+ +``` +{{.Log}}``` +

{{end}} +{{- end }} diff --git a/server/events/templates/multi_project_apply.tmpl b/server/events/templates/multi_project_apply.tmpl new file mode 100644 index 0000000000..ff17c53df8 --- /dev/null +++ b/server/events/templates/multi_project_apply.tmpl @@ -0,0 +1,8 @@ +{{ define "multiProjectApply" -}} +{{ template "multiProjectHeader" . }} +{{ range $i, $result := .Results }}### {{add $i 1}}. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` +{{$result.Rendered}} + +--- +{{end}}{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/multi_project_header.tmpl b/server/events/templates/multi_project_header.tmpl new file mode 100644 index 0000000000..66d0f39745 --- /dev/null +++ b/server/events/templates/multi_project_header.tmpl @@ -0,0 +1,6 @@ +{{ define "multiProjectHeader" -}} +Ran {{.Command}} for {{ len .Results }} projects: + +{{ range $result := .Results }}1. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` +{{end}} +{{- end }} diff --git a/server/events/templates/multi_project_plan.tmpl b/server/events/templates/multi_project_plan.tmpl new file mode 100644 index 0000000000..9ee33c590f --- /dev/null +++ b/server/events/templates/multi_project_plan.tmpl @@ -0,0 +1,11 @@ +{{ define "multiProjectPlan" -}} +{{ template "multiProjectHeader" . }} +{{ $disableApplyAll := .DisableApplyAll }}{{ range $i, $result := .Results }}### {{add $i 1}}. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` +{{$result.Rendered}} + +{{ if ne $disableApplyAll true }}--- +{{end}}{{end}}{{ if ne .DisableApplyAll true }}{{ if and (gt (len .Results) 0) (not .PlansDeleted) }}* :fast_forward: To **apply** all unapplied plans from this pull request, comment: + * `atlantis apply` +* :put_litter_in_its_place: To delete all plans and locks for the PR, comment: + * `atlantis unlock`{{end}}{{end}}{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/multi_project_version.tmpl b/server/events/templates/multi_project_version.tmpl new file mode 100644 index 0000000000..f8557b5244 --- /dev/null +++ b/server/events/templates/multi_project_version.tmpl @@ -0,0 +1,3 @@ +{{ define "multiProjectVersion" -}} +{{ template "multiProjectApply" . }} +{{- end }} diff --git a/server/events/templates/plan_success_unwrapped.tmpl b/server/events/templates/plan_success_unwrapped.tmpl new file mode 100644 index 0000000000..2514334053 --- /dev/null +++ b/server/events/templates/plan_success_unwrapped.tmpl @@ -0,0 +1,11 @@ +{{ define "planSuccessUnwrapped" -}} +```diff +{{ if .EnableDiffMarkdownFormat }}{{.DiffMarkdownFormattedTerraformOutput}}{{else}}{{.TerraformOutput}}{{end}} +``` + +{{ if .PlanWasDeleted }}This plan was not saved because one or more projects failed and automerge requires all plans pass.{{ else }}{{ if not .DisableApply }}* :arrow_forward: To **apply** this plan, comment: + * `{{.ApplyCmd}}` +{{end}}{{ if not .DisableRepoLocking }}* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}}) +{{end}}* :repeat: To **plan** this project again, comment: + * `{{.RePlanCmd}}`{{end}}{{ template "diverged" . }} +{{- end }} diff --git a/server/events/templates/plan_success_wrapped.tmpl b/server/events/templates/plan_success_wrapped.tmpl new file mode 100644 index 0000000000..23df190f5e --- /dev/null +++ b/server/events/templates/plan_success_wrapped.tmpl @@ -0,0 +1,15 @@ +{{ define "planSuccessWrapped" -}} +
Show Output + +```diff +{{ if .EnableDiffMarkdownFormat }}{{.DiffMarkdownFormattedTerraformOutput}}{{else}}{{.TerraformOutput}}{{end}} +``` + +{{ if .PlanWasDeleted }}This plan was not saved because one or more projects failed and automerge requires all plans pass.{{ else }}{{ if not .DisableApply }}* :arrow_forward: To **apply** this plan, comment: + * `{{.ApplyCmd}}` +{{end}}{{ if not .DisableRepoLocking }}* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}}) +{{end}}* :repeat: To **plan** this project again, comment: + * `{{.RePlanCmd}}`{{end}} +
+{{.PlanSummary}}{{ template "diverged" . }} +{{- end }} diff --git a/server/events/templates/policy_check_success_unwrapped.tmpl b/server/events/templates/policy_check_success_unwrapped.tmpl new file mode 100644 index 0000000000..34fa9757ac --- /dev/null +++ b/server/events/templates/policy_check_success_unwrapped.tmpl @@ -0,0 +1,11 @@ +{{ define "policyCheckSuccessUnwrapped" -}} +```diff +{{.PolicyCheckOutput}} +``` + +* :arrow_forward: To **apply** this plan, comment: + * `{{.ApplyCmd}}` +* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}}) +* :repeat: To re-run policies **plan** this project again by commenting: + * `{{.RePlanCmd}}`{{ template "diverged" . }} +{{- end }} diff --git a/server/events/templates/policy_check_success_wrapped.tmpl b/server/events/templates/policy_check_success_wrapped.tmpl new file mode 100644 index 0000000000..0066a385c0 --- /dev/null +++ b/server/events/templates/policy_check_success_wrapped.tmpl @@ -0,0 +1,14 @@ +{{ define "policyCheckSuccessWrapped" -}} +
Show Output + +```diff +{{.PolicyCheckOutput}} +``` + +* :arrow_forward: To **apply** this plan, comment: + * `{{.ApplyCmd}}` +* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}}) +* :repeat: To re-run policies **plan** this project again by commenting: + * `{{.RePlanCmd}}` +
{{ template "diverged" . }} +{{- end }} diff --git a/server/events/templates/single_project_apply.tmpl b/server/events/templates/single_project_apply.tmpl new file mode 100644 index 0000000000..da5174a0b5 --- /dev/null +++ b/server/events/templates/single_project_apply.tmpl @@ -0,0 +1,6 @@ +{{ define "singleProjectApply" -}} +{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` + +{{$result.Rendered}} +{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/single_project_plan_success.tmpl b/server/events/templates/single_project_plan_success.tmpl new file mode 100644 index 0000000000..08c1624037 --- /dev/null +++ b/server/events/templates/single_project_plan_success.tmpl @@ -0,0 +1,11 @@ +{{ define "singleProjectPlanSuccess" -}} +{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` + +{{$result.Rendered}} + +{{ if ne .DisableApplyAll true }}--- +* :fast_forward: To **apply** all unapplied plans from this pull request, comment: + * `atlantis apply` +* :put_litter_in_its_place: To delete all plans and locks for the PR, comment: + * `atlantis unlock`{{ end }}{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/single_project_plan_unsuccessful.tmpl b/server/events/templates/single_project_plan_unsuccessful.tmpl new file mode 100644 index 0000000000..96157d9507 --- /dev/null +++ b/server/events/templates/single_project_plan_unsuccessful.tmpl @@ -0,0 +1,6 @@ +{{ define "singleProjectPlanUnsuccessful" -}} +{{$result := index .Results 0}}Ran {{.Command}} for dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` + +{{$result.Rendered}} +{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/single_project_version_success.tmpl b/server/events/templates/single_project_version_success.tmpl new file mode 100644 index 0000000000..201cde63b5 --- /dev/null +++ b/server/events/templates/single_project_version_success.tmpl @@ -0,0 +1,6 @@ +{{ define "singleProjectVersionSuccess" -}} +{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` + +{{$result.Rendered}} +{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/single_project_version_unsuccessful.tmpl b/server/events/templates/single_project_version_unsuccessful.tmpl new file mode 100644 index 0000000000..69985b3316 --- /dev/null +++ b/server/events/templates/single_project_version_unsuccessful.tmpl @@ -0,0 +1,3 @@ +{{ define "singleProjectVersionUnsuccessful" -}} +{{ template "singleProjectPlanUnsuccessful" . }} +{{- end }} diff --git a/server/events/templates/unwrapped_err.tmpl b/server/events/templates/unwrapped_err.tmpl new file mode 100644 index 0000000000..3d732e4ae6 --- /dev/null +++ b/server/events/templates/unwrapped_err.tmpl @@ -0,0 +1,10 @@ +{{ define "unwrappedErr" -}} +**{{.Command}} Error** +``` +{{.Error}} +```{{ if eq .Command "Policy Check" }} +* :heavy_check_mark: To **approve** failing policies an authorized approver can comment: + * `atlantis approve_policies` +* :repeat: Or, address the policy failure by modifying the codebase and re-planning. +{{ end }} +{{- end }} diff --git a/server/events/templates/unwrapped_err_with_log.tmpl b/server/events/templates/unwrapped_err_with_log.tmpl new file mode 100644 index 0000000000..b27533a540 --- /dev/null +++ b/server/events/templates/unwrapped_err_with_log.tmpl @@ -0,0 +1,3 @@ +{{ define "unwrappedErrWithLog" -}} +{{ template "unwrappedErr" . }}{{ template "log" . }} +{{ end }} diff --git a/server/events/templates/version_unwrapped_success.tmpl b/server/events/templates/version_unwrapped_success.tmpl new file mode 100644 index 0000000000..efcb6e5009 --- /dev/null +++ b/server/events/templates/version_unwrapped_success.tmpl @@ -0,0 +1,5 @@ +{{ define "versionUnwrappedSuccess" -}} +``` +{{.Output}} +``` +{{ end }} diff --git a/server/events/templates/version_wrapped_success.tmpl b/server/events/templates/version_wrapped_success.tmpl new file mode 100644 index 0000000000..84371ce1c2 --- /dev/null +++ b/server/events/templates/version_wrapped_success.tmpl @@ -0,0 +1,6 @@ +{{ define "versionWrappedSuccess" -}} +
Show Output + +{{ template "versionUnwrappedSuccess" . }} +
+{{- end }} diff --git a/server/events/templates/wrapped_err.tmpl b/server/events/templates/wrapped_err.tmpl new file mode 100644 index 0000000000..39050b92dd --- /dev/null +++ b/server/events/templates/wrapped_err.tmpl @@ -0,0 +1,9 @@ +{{ define "wrappedErr" -}} +**{{.Command}} Error** +
Show Output + +``` +{{.Error}} +``` +
+{{- end }} diff --git a/server/server.go b/server/server.go index c41612d1d2..4b0faec4a4 100644 --- a/server/server.go +++ b/server/server.go @@ -389,14 +389,15 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if err != nil && flag.Lookup("test.v") == nil { return nil, errors.Wrap(err, "initializing terraform") } - markdownRenderer := &events.MarkdownRenderer{ - GitlabSupportsCommonMark: gitlabClient.SupportsCommonMark(), - DisableApplyAll: userConfig.DisableApplyAll, - DisableMarkdownFolding: userConfig.DisableMarkdownFolding, - DisableApply: userConfig.DisableApply, - DisableRepoLocking: userConfig.DisableRepoLocking, - EnableDiffMarkdownFormat: userConfig.EnableDiffMarkdownFormat, - } + markdownRenderer := events.GetMarkdownRenderer( + gitlabClient.SupportsCommonMark(), + userConfig.DisableApplyAll, + userConfig.DisableMarkdownFolding, + userConfig.DisableApply, + userConfig.DisableRepoLocking, + userConfig.EnableDiffMarkdownFormat, + userConfig.MarkdownTemplateOverridesDir, + ) var lockingClient locking.Locker var applyLockingClient locking.ApplyLocker diff --git a/server/user_config.go b/server/user_config.go index a68ef23037..3abd34d86c 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -51,6 +51,7 @@ type UserConfig struct { HidePrevPlanComments bool `mapstructure:"hide-prev-plan-comments"` LockingDBType string `mapstructure:"locking-db-type"` LogLevel string `mapstructure:"log-level"` + MarkdownTemplateOverridesDir string `mapstructure:"markdown-template-overrides-dir"` ParallelPoolSize int `mapstructure:"parallel-pool-size"` StatsNamespace string `mapstructure:"stats-namespace"` PlanDrafts bool `mapstructure:"allow-draft-prs"`