mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-30 02:57:57 +00:00
443 lines
17 KiB
Go
443 lines
17 KiB
Go
// Copyright 2017 HootSuite Media Inc.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the License);
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an AS IS BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
// Modified hereafter by contributors to runatlantis/atlantis.
|
||
|
||
package events
|
||
|
||
import (
|
||
"bytes"
|
||
"embed"
|
||
"fmt"
|
||
"strings"
|
||
"text/template"
|
||
|
||
"github.com/Masterminds/sprig/v3"
|
||
"github.com/runatlantis/atlantis/server/events/command"
|
||
"github.com/runatlantis/atlantis/server/events/models"
|
||
"golang.org/x/text/cases"
|
||
"golang.org/x/text/language"
|
||
)
|
||
|
||
var (
|
||
planCommandTitle = command.Plan.TitleString()
|
||
applyCommandTitle = command.Apply.TitleString()
|
||
policyCheckCommandTitle = command.PolicyCheck.TitleString()
|
||
approvePoliciesCommandTitle = command.ApprovePolicies.TitleString()
|
||
versionCommandTitle = command.Version.TitleString()
|
||
importCommandTitle = command.Import.TitleString()
|
||
stateCommandTitle = command.State.TitleString()
|
||
// 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.
|
||
type MarkdownRenderer struct {
|
||
// gitlabSupportsCommonMark is true if the version of GitLab we're
|
||
// using supports the CommonMark markdown format.
|
||
// If we're not configured with a GitLab client, this will be false.
|
||
gitlabSupportsCommonMark bool
|
||
disableApplyAll bool
|
||
disableApply bool
|
||
disableMarkdownFolding bool
|
||
disableRepoLocking bool
|
||
enableDiffMarkdownFormat bool
|
||
markdownTemplates *template.Template
|
||
executableName string
|
||
hideUnchangedPlanComments bool
|
||
quietPolicyChecks bool
|
||
}
|
||
|
||
// commonData is data that all responses have.
|
||
type commonData struct {
|
||
Command string
|
||
SubCommand string
|
||
Verbose bool
|
||
Log string
|
||
PlansDeleted bool
|
||
DisableApplyAll bool
|
||
DisableApply bool
|
||
DisableRepoLocking bool
|
||
EnableDiffMarkdownFormat bool
|
||
ExecutableName string
|
||
HideUnchangedPlanComments bool
|
||
QuietPolicyChecks bool
|
||
VcsRequestType string
|
||
}
|
||
|
||
// errData is data about an error response.
|
||
type errData struct {
|
||
Error string
|
||
RenderedContext string
|
||
commonData
|
||
}
|
||
|
||
// failureData is data about a failure response.
|
||
type failureData struct {
|
||
Failure string
|
||
RenderedContext string
|
||
commonData
|
||
}
|
||
|
||
type resultData struct {
|
||
Results []projectResultTmplData
|
||
commonData
|
||
}
|
||
|
||
type planResultData struct {
|
||
Results []projectResultTmplData
|
||
commonData
|
||
NumPlansWithChanges int
|
||
NumPlansWithNoChanges int
|
||
NumPlanFailures int
|
||
}
|
||
|
||
type applyResultData struct {
|
||
Results []projectResultTmplData
|
||
commonData
|
||
NumApplySuccesses int
|
||
NumApplyFailures int
|
||
NumApplyErrors int
|
||
}
|
||
|
||
type planSuccessData struct {
|
||
models.PlanSuccess
|
||
PlanSummary string
|
||
PlanWasDeleted bool
|
||
DisableApply bool
|
||
DisableRepoLocking bool
|
||
EnableDiffMarkdownFormat bool
|
||
PlanStats models.PlanSuccessStats
|
||
}
|
||
|
||
type policyCheckResultsData struct {
|
||
models.PolicyCheckResults
|
||
PreConftestOutput string
|
||
PostConftestOutput string
|
||
PolicyCheckSummary string
|
||
PolicyApprovalSummary string
|
||
PolicyCleared bool
|
||
commonData
|
||
}
|
||
|
||
type projectResultTmplData struct {
|
||
Workspace string
|
||
RepoRelDir string
|
||
ProjectName string
|
||
Rendered string
|
||
NoChanges bool
|
||
IsSuccessful bool
|
||
}
|
||
|
||
// Initialize templates
|
||
func NewMarkdownRenderer(
|
||
gitlabSupportsCommonMark bool,
|
||
disableApplyAll bool,
|
||
disableApply bool,
|
||
disableMarkdownFolding bool,
|
||
disableRepoLocking bool,
|
||
enableDiffMarkdownFormat bool,
|
||
markdownTemplateOverridesDir string,
|
||
executableName string,
|
||
hideUnchangedPlanComments bool,
|
||
quietPolicyChecks bool,
|
||
) *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,
|
||
executableName: executableName,
|
||
hideUnchangedPlanComments: hideUnchangedPlanComments,
|
||
quietPolicyChecks: quietPolicyChecks,
|
||
}
|
||
}
|
||
|
||
// Render formats the data into a markdown string.
|
||
// nolint: interfacer
|
||
func (m *MarkdownRenderer) Render(ctx *command.Context, res command.Result, cmd PullCommand) string {
|
||
commandStr := cases.Title(language.English).String(strings.Replace(cmd.CommandName().String(), "_", " ", -1))
|
||
var vcsRequestType string
|
||
if ctx.Pull.BaseRepo.VCSHost.Type == models.Gitlab {
|
||
vcsRequestType = "Merge Request"
|
||
} else {
|
||
vcsRequestType = "Pull Request"
|
||
}
|
||
|
||
common := commonData{
|
||
Command: commandStr,
|
||
SubCommand: cmd.SubCommandName(),
|
||
Verbose: cmd.IsVerbose(),
|
||
Log: ctx.Log.GetHistory(),
|
||
PlansDeleted: res.PlansDeleted,
|
||
DisableApplyAll: m.disableApplyAll || m.disableApply,
|
||
DisableApply: m.disableApply,
|
||
DisableRepoLocking: m.disableRepoLocking,
|
||
EnableDiffMarkdownFormat: m.enableDiffMarkdownFormat,
|
||
ExecutableName: m.executableName,
|
||
HideUnchangedPlanComments: m.hideUnchangedPlanComments,
|
||
QuietPolicyChecks: m.quietPolicyChecks,
|
||
VcsRequestType: vcsRequestType,
|
||
}
|
||
|
||
templates := m.markdownTemplates
|
||
|
||
if res.Error != nil {
|
||
return m.renderTemplateTrimSpace(templates.Lookup("unwrappedErrWithLog"), errData{res.Error.Error(), "", common})
|
||
}
|
||
if res.Failure != "" {
|
||
return m.renderTemplateTrimSpace(templates.Lookup("failureWithLog"), failureData{res.Failure, "", common})
|
||
}
|
||
return m.renderProjectResults(ctx, res.ProjectResults, common)
|
||
}
|
||
|
||
func (m *MarkdownRenderer) renderProjectResults(ctx *command.Context, results []command.ProjectResult, common commonData) string {
|
||
vcsHost := ctx.Pull.BaseRepo.VCSHost.Type
|
||
|
||
var resultsTmplData []projectResultTmplData
|
||
numPlanSuccesses := 0
|
||
numPolicyCheckSuccesses := 0
|
||
numPolicyApprovalSuccesses := 0
|
||
numVersionSuccesses := 0
|
||
numPlansWithChanges := 0
|
||
numPlansWithNoChanges := 0
|
||
numApplySuccesses := 0
|
||
numApplyFailures := 0
|
||
numApplyErrors := 0
|
||
|
||
templates := m.markdownTemplates
|
||
|
||
for _, result := range results {
|
||
resultData := projectResultTmplData{
|
||
Workspace: result.Workspace,
|
||
RepoRelDir: result.RepoRelDir,
|
||
ProjectName: result.ProjectName,
|
||
IsSuccessful: result.IsSuccessful(),
|
||
}
|
||
if result.PlanSuccess != nil {
|
||
result.PlanSuccess.TerraformOutput = strings.TrimSpace(result.PlanSuccess.TerraformOutput)
|
||
data := planSuccessData{
|
||
PlanSuccess: *result.PlanSuccess,
|
||
PlanWasDeleted: common.PlansDeleted,
|
||
DisableApply: common.DisableApply,
|
||
DisableRepoLocking: common.DisableRepoLocking,
|
||
EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat,
|
||
PlanStats: result.PlanSuccess.Stats(),
|
||
}
|
||
if m.shouldUseWrappedTmpl(vcsHost, result.PlanSuccess.TerraformOutput) {
|
||
data.PlanSummary = result.PlanSuccess.Summary()
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("planSuccessWrapped"), data)
|
||
} else {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("planSuccessUnwrapped"), data)
|
||
}
|
||
resultData.NoChanges = result.PlanSuccess.NoChanges()
|
||
if result.PlanSuccess.NoChanges() {
|
||
numPlansWithNoChanges++
|
||
} else {
|
||
numPlansWithChanges++
|
||
}
|
||
numPlanSuccesses++
|
||
} else if result.PolicyCheckResults != nil && common.Command == policyCheckCommandTitle {
|
||
policyCheckResults := policyCheckResultsData{
|
||
PreConftestOutput: result.PolicyCheckResults.PreConftestOutput,
|
||
PostConftestOutput: result.PolicyCheckResults.PostConftestOutput,
|
||
PolicyCheckResults: *result.PolicyCheckResults,
|
||
PolicyCheckSummary: result.PolicyCheckResults.Summary(),
|
||
PolicyApprovalSummary: result.PolicyCheckResults.PolicySummary(),
|
||
PolicyCleared: result.PolicyCheckResults.PolicyCleared(),
|
||
commonData: common,
|
||
}
|
||
if m.shouldUseWrappedTmpl(vcsHost, result.PolicyCheckResults.CombinedOutput()) {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("policyCheckResultsWrapped"), policyCheckResults)
|
||
} else {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("policyCheckResultsUnwrapped"), policyCheckResults)
|
||
}
|
||
if result.Error == nil && result.Failure == "" {
|
||
numPolicyCheckSuccesses++
|
||
}
|
||
} else if result.PolicyCheckResults != nil && common.Command == approvePoliciesCommandTitle {
|
||
policyCheckResults := policyCheckResultsData{
|
||
PolicyCheckResults: *result.PolicyCheckResults,
|
||
PolicyCheckSummary: result.PolicyCheckResults.Summary(),
|
||
PolicyApprovalSummary: result.PolicyCheckResults.PolicySummary(),
|
||
PolicyCleared: result.PolicyCheckResults.PolicyCleared(),
|
||
commonData: common,
|
||
}
|
||
if m.shouldUseWrappedTmpl(vcsHost, result.PolicyCheckResults.CombinedOutput()) {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("policyCheckResultsWrapped"), policyCheckResults)
|
||
} else {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("policyCheckResultsUnwrapped"), policyCheckResults)
|
||
}
|
||
if result.Error == nil && result.Failure == "" {
|
||
numPolicyApprovalSuccesses++
|
||
}
|
||
} else if result.ApplySuccess != "" {
|
||
output := strings.TrimSpace(result.ApplySuccess)
|
||
if m.shouldUseWrappedTmpl(vcsHost, result.ApplySuccess) {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("applyWrappedSuccess"), struct{ Output string }{output})
|
||
} else {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("applyUnwrappedSuccess"), struct{ Output string }{output})
|
||
}
|
||
numApplySuccesses++
|
||
} else if result.VersionSuccess != "" {
|
||
output := strings.TrimSpace(result.VersionSuccess)
|
||
if m.shouldUseWrappedTmpl(vcsHost, output) {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("versionWrappedSuccess"), struct{ Output string }{output})
|
||
} else {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("versionUnwrappedSuccess"), struct{ Output string }{output})
|
||
}
|
||
numVersionSuccesses++
|
||
} else if result.ImportSuccess != nil {
|
||
result.ImportSuccess.Output = strings.TrimSpace(result.ImportSuccess.Output)
|
||
if m.shouldUseWrappedTmpl(vcsHost, result.ImportSuccess.Output) {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("importSuccessWrapped"), result.ImportSuccess)
|
||
} else {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("importSuccessUnwrapped"), result.ImportSuccess)
|
||
}
|
||
} else if result.StateRmSuccess != nil {
|
||
result.StateRmSuccess.Output = strings.TrimSpace(result.StateRmSuccess.Output)
|
||
if m.shouldUseWrappedTmpl(vcsHost, result.StateRmSuccess.Output) {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("stateRmSuccessWrapped"), result.StateRmSuccess)
|
||
} else {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("stateRmSuccessUnwrapped"), result.StateRmSuccess)
|
||
}
|
||
// Error out if no template was found, only if there are no errors or failures.
|
||
// This is because some errors and failures rely on additional context rendered by templates, but not all errors or failures.
|
||
} else if !(result.Error != nil || result.Failure != "") {
|
||
resultData.Rendered = "Found no template. This is a bug!"
|
||
}
|
||
// Render error or failure templates. Done outside of previous block so that other context can be rendered for use here.
|
||
if result.Error != nil {
|
||
tmpl := templates.Lookup("unwrappedErr")
|
||
if m.shouldUseWrappedTmpl(vcsHost, result.Error.Error()) {
|
||
tmpl = templates.Lookup("wrappedErr")
|
||
}
|
||
resultData.Rendered = m.renderTemplateTrimSpace(tmpl, errData{result.Error.Error(), resultData.Rendered, common})
|
||
if common.Command == applyCommandTitle {
|
||
numApplyErrors++
|
||
}
|
||
} else if result.Failure != "" {
|
||
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("failure"), failureData{result.Failure, resultData.Rendered, common})
|
||
if common.Command == applyCommandTitle {
|
||
numApplyFailures++
|
||
}
|
||
}
|
||
resultsTmplData = append(resultsTmplData, resultData)
|
||
}
|
||
|
||
var tmpl *template.Template
|
||
switch {
|
||
case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses > 0:
|
||
tmpl = templates.Lookup("singleProjectPlanSuccess")
|
||
case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses == 0:
|
||
tmpl = templates.Lookup("singleProjectPlanUnsuccessful")
|
||
case len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses > 0:
|
||
tmpl = templates.Lookup("singleProjectPlanSuccess")
|
||
case len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses == 0:
|
||
tmpl = templates.Lookup("singleProjectPolicyUnsuccessful")
|
||
case len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses > 0:
|
||
tmpl = templates.Lookup("singleProjectVersionSuccess")
|
||
case len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses == 0:
|
||
tmpl = templates.Lookup("singleProjectVersionUnsuccessful")
|
||
case len(resultsTmplData) == 1 && common.Command == applyCommandTitle:
|
||
tmpl = templates.Lookup("singleProjectApply")
|
||
case len(resultsTmplData) == 1 && common.Command == importCommandTitle:
|
||
tmpl = templates.Lookup("singleProjectImport")
|
||
case len(resultsTmplData) == 1 && common.Command == stateCommandTitle:
|
||
switch common.SubCommand {
|
||
case "rm":
|
||
tmpl = templates.Lookup("singleProjectStateRm")
|
||
default:
|
||
return fmt.Sprintf("no template matched–this is a bug: command=%s, subcommand=%s", common.Command, common.SubCommand)
|
||
}
|
||
case common.Command == planCommandTitle:
|
||
tmpl = templates.Lookup("multiProjectPlan")
|
||
case common.Command == policyCheckCommandTitle:
|
||
if numPolicyCheckSuccesses == len(results) {
|
||
tmpl = templates.Lookup("multiProjectPolicy")
|
||
} else {
|
||
tmpl = templates.Lookup("multiProjectPolicyUnsuccessful")
|
||
}
|
||
case common.Command == approvePoliciesCommandTitle:
|
||
if numPolicyApprovalSuccesses == len(results) {
|
||
tmpl = templates.Lookup("approveAllProjects")
|
||
} else {
|
||
tmpl = templates.Lookup("multiProjectPolicyUnsuccessful")
|
||
}
|
||
case common.Command == applyCommandTitle:
|
||
tmpl = templates.Lookup("multiProjectApply")
|
||
case common.Command == versionCommandTitle:
|
||
tmpl = templates.Lookup("multiProjectVersion")
|
||
case common.Command == importCommandTitle:
|
||
tmpl = templates.Lookup("multiProjectImport")
|
||
case common.Command == stateCommandTitle:
|
||
switch common.SubCommand {
|
||
case "rm":
|
||
tmpl = templates.Lookup("multiProjectStateRm")
|
||
default:
|
||
return fmt.Sprintf("no template matched–this is a bug: command=%s, subcommand=%s", common.Command, common.SubCommand)
|
||
}
|
||
default:
|
||
return fmt.Sprintf("no template matched–this is a bug: command=%s", common.Command)
|
||
}
|
||
|
||
switch {
|
||
case common.Command == planCommandTitle:
|
||
numPlanFailures := len(results) - numPlanSuccesses
|
||
return m.renderTemplateTrimSpace(tmpl, planResultData{resultsTmplData, common, numPlansWithChanges, numPlansWithNoChanges, numPlanFailures})
|
||
case common.Command == applyCommandTitle:
|
||
return m.renderTemplateTrimSpace(tmpl, applyResultData{resultsTmplData, common, numApplySuccesses, numApplyFailures, numApplyErrors})
|
||
}
|
||
return m.renderTemplateTrimSpace(tmpl, resultData{resultsTmplData, common})
|
||
}
|
||
|
||
// shouldUseWrappedTmpl returns true if we should use the wrapped markdown
|
||
// templates that collapse the output to make the comment smaller on initial
|
||
// load. Some VCS providers or versions of VCS providers don't support this
|
||
// syntax.
|
||
func (m *MarkdownRenderer) shouldUseWrappedTmpl(vcsHost models.VCSHostType, output string) bool {
|
||
if m.disableMarkdownFolding {
|
||
return false
|
||
}
|
||
|
||
// Bitbucket Cloud and Server don't support the folding markdown syntax.
|
||
if vcsHost == models.BitbucketServer || vcsHost == models.BitbucketCloud {
|
||
return false
|
||
}
|
||
|
||
if vcsHost == models.Gitlab && !m.gitlabSupportsCommonMark {
|
||
return false
|
||
}
|
||
|
||
return strings.Count(output, "\n") > maxUnwrappedLines
|
||
}
|
||
|
||
func (m *MarkdownRenderer) renderTemplateTrimSpace(tmpl *template.Template, data interface{}) string {
|
||
buf := &bytes.Buffer{}
|
||
if err := tmpl.Execute(buf, data); err != nil {
|
||
return fmt.Sprintf("Failed to render template, this is a bug: %v", err)
|
||
}
|
||
return strings.TrimSpace(buf.String())
|
||
}
|