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}}\nLog
\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"`