From 9658d3ac12774390293ba184db7261a187b2d1aa Mon Sep 17 00:00:00 2001 From: Le Van Nghia Date: Wed, 7 Jul 2021 11:43:52 +0900 Subject: [PATCH] Make terraform cloud provider able to parse log including ansi codes (#2189) **What this PR does / why we need it**: **Which issue(s) this PR fixes**: Fixes #674 **Does this PR introduce a user-facing change?**: ```release-note NONE ``` This PR was merged by Kapetanios. --- .../cloudprovider/terraform/terraform.go | 93 +++++++++++++------ pkg/app/piped/executor/terraform/deploy.go | 21 ++++- pkg/app/piped/executor/terraform/rollback.go | 7 +- pkg/app/piped/planpreview/terraformdiff.go | 8 +- 4 files changed, 97 insertions(+), 32 deletions(-) diff --git a/pkg/app/piped/cloudprovider/terraform/terraform.go b/pkg/app/piped/cloudprovider/terraform/terraform.go index 18b8a05a38..16190bfaec 100644 --- a/pkg/app/piped/cloudprovider/terraform/terraform.go +++ b/pkg/app/piped/cloudprovider/terraform/terraform.go @@ -25,19 +25,49 @@ import ( "strings" ) +type options struct { + noColor bool + vars []string + varFiles []string +} + +type Option func(*options) + +func WithoutColor() Option { + return func(opts *options) { + opts.noColor = true + } +} + +func WithVars(vars []string) Option { + return func(opts *options) { + opts.vars = vars + } +} + +func WithVarFiles(files []string) Option { + return func(opts *options) { + opts.varFiles = files + } +} + type Terraform struct { execPath string dir string - vars []string - varFiles []string + + options options } -func NewTerraform(execPath, dir string, vars, varFiles []string) *Terraform { +func NewTerraform(execPath, dir string, opts ...Option) *Terraform { + opt := options{} + for _, o := range opts { + o(&opt) + } + return &Terraform{ execPath: execPath, dir: dir, - vars: vars, - varFiles: varFiles, + options: opt, } } @@ -58,12 +88,7 @@ func (t *Terraform) Init(ctx context.Context, w io.Writer) error { args := []string{ "init", } - for _, v := range t.vars { - args = append(args, fmt.Sprintf("-var=%s", v)) - } - for _, f := range t.varFiles { - args = append(args, fmt.Sprintf("-var-file=%s", f)) - } + args = append(args, t.makeCommonCommandArgs()...) cmd := exec.CommandContext(ctx, t.execPath, args...) cmd.Dir = t.dir @@ -114,17 +139,10 @@ func GetExitCode(err error) int { func (t *Terraform) Plan(ctx context.Context, w io.Writer) (PlanResult, error) { args := []string{ "plan", - // TODO: Remove this -no-color flag after parsePlanResult supports parsing the message containing color codes. - "-no-color", "-lock=false", "-detailed-exitcode", } - for _, v := range t.vars { - args = append(args, fmt.Sprintf("-var=%s", v)) - } - for _, f := range t.varFiles { - args = append(args, fmt.Sprintf("-var-file=%s", f)) - } + args = append(args, t.makeCommonCommandArgs()...) var buf bytes.Buffer stdout := io.MultiWriter(w, &buf) @@ -140,18 +158,40 @@ func (t *Terraform) Plan(ctx context.Context, w io.Writer) (PlanResult, error) { case 0: return PlanResult{}, nil case 2: - return parsePlanResult(buf.String()) + return parsePlanResult(buf.String(), !t.options.noColor) default: return PlanResult{}, err } } +func (t *Terraform) makeCommonCommandArgs() (args []string) { + if t.options.noColor { + args = append(args, "-no-color") + } + for _, v := range t.options.vars { + args = append(args, fmt.Sprintf("-var=%s", v)) + } + for _, f := range t.options.varFiles { + args = append(args, fmt.Sprintf("-var-file=%s", f)) + } + return +} + var ( planHasChangeRegex = regexp.MustCompile(`(?m)^Plan: (\d+) to add, (\d+) to change, (\d+) to destroy.$`) planNoChangesRegex = regexp.MustCompile(`(?m)^No changes. Infrastructure is up-to-date.$`) ) -func parsePlanResult(out string) (PlanResult, error) { +// Borrowed from https://github.com/acarl005/stripansi +const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + +var ansiRegex = regexp.MustCompile(ansi) + +func stripAnsiCodes(str string) string { + return ansiRegex.ReplaceAllString(str, "") +} + +func parsePlanResult(out string, ansiIncluded bool) (PlanResult, error) { parseNums := func(add, change, destroy string) (adds int, changes int, destroys int, err error) { adds, err = strconv.Atoi(add) if err != nil { @@ -168,6 +208,10 @@ func parsePlanResult(out string) (PlanResult, error) { return } + if ansiIncluded { + out = stripAnsiCodes(out) + } + if s := planHasChangeRegex.FindStringSubmatch(out); len(s) == 4 { adds, changes, destroys, err := parseNums(s[1], s[2], s[3]) if err == nil { @@ -192,12 +236,7 @@ func (t *Terraform) Apply(ctx context.Context, w io.Writer) error { "-auto-approve", "-input=false", } - for _, v := range t.vars { - args = append(args, fmt.Sprintf("-var=%s", v)) - } - for _, f := range t.varFiles { - args = append(args, fmt.Sprintf("-var-file=%s", f)) - } + args = append(args, t.makeCommonCommandArgs()...) cmd := exec.CommandContext(ctx, t.execPath, args...) cmd.Dir = t.dir diff --git a/pkg/app/piped/executor/terraform/deploy.go b/pkg/app/piped/executor/terraform/deploy.go index b16c0c2edc..29f9539031 100644 --- a/pkg/app/piped/executor/terraform/deploy.go +++ b/pkg/app/piped/executor/terraform/deploy.go @@ -89,7 +89,12 @@ func (e *deployExecutor) Execute(sig executor.StopSignal) model.StageStatus { } func (e *deployExecutor) ensureSync(ctx context.Context) model.StageStatus { - cmd := provider.NewTerraform(e.terraformPath, e.appDir, e.vars, e.deployCfg.Input.VarFiles) + cmd := provider.NewTerraform( + e.terraformPath, + e.appDir, + provider.WithVars(e.vars), + provider.WithVarFiles(e.deployCfg.Input.VarFiles), + ) if ok := showUsingVersion(ctx, cmd, e.LogPersister); !ok { return model.StageStatus_STAGE_FAILURE @@ -127,7 +132,12 @@ func (e *deployExecutor) ensureSync(ctx context.Context) model.StageStatus { } func (e *deployExecutor) ensurePlan(ctx context.Context) model.StageStatus { - cmd := provider.NewTerraform(e.terraformPath, e.appDir, e.vars, e.deployCfg.Input.VarFiles) + cmd := provider.NewTerraform( + e.terraformPath, + e.appDir, + provider.WithVars(e.vars), + provider.WithVarFiles(e.deployCfg.Input.VarFiles), + ) if ok := showUsingVersion(ctx, cmd, e.LogPersister); !ok { return model.StageStatus_STAGE_FAILURE @@ -158,7 +168,12 @@ func (e *deployExecutor) ensurePlan(ctx context.Context) model.StageStatus { } func (e *deployExecutor) ensureApply(ctx context.Context) model.StageStatus { - cmd := provider.NewTerraform(e.terraformPath, e.appDir, e.vars, e.deployCfg.Input.VarFiles) + cmd := provider.NewTerraform( + e.terraformPath, + e.appDir, + provider.WithVars(e.vars), + provider.WithVarFiles(e.deployCfg.Input.VarFiles), + ) if ok := showUsingVersion(ctx, cmd, e.LogPersister); !ok { return model.StageStatus_STAGE_FAILURE diff --git a/pkg/app/piped/executor/terraform/rollback.go b/pkg/app/piped/executor/terraform/rollback.go index e5c9344a33..0078279726 100644 --- a/pkg/app/piped/executor/terraform/rollback.go +++ b/pkg/app/piped/executor/terraform/rollback.go @@ -79,7 +79,12 @@ func (e *rollbackExecutor) ensureRollback(ctx context.Context) model.StageStatus vars = append(vars, deployCfg.Input.Vars...) e.LogPersister.Infof("Start rolling back to the state defined at commit %s", e.Deployment.RunningCommitHash) - cmd := provider.NewTerraform(terraformPath, ds.AppDir, vars, deployCfg.Input.VarFiles) + cmd := provider.NewTerraform( + terraformPath, + ds.AppDir, + provider.WithVars(vars), + provider.WithVarFiles(deployCfg.Input.VarFiles), + ) if ok := showUsingVersion(ctx, cmd, e.LogPersister); !ok { return model.StageStatus_STAGE_FAILURE diff --git a/pkg/app/piped/planpreview/terraformdiff.go b/pkg/app/piped/planpreview/terraformdiff.go index 570d3de57c..db91f6a054 100644 --- a/pkg/app/piped/planpreview/terraformdiff.go +++ b/pkg/app/piped/planpreview/terraformdiff.go @@ -85,7 +85,13 @@ func (b *builder) terraformDiff( vars = append(vars, cpCfg.Vars...) vars = append(vars, deployCfg.Input.Vars...) - executor := terraformprovider.NewTerraform(terraformPath, ds.AppDir, vars, deployCfg.Input.VarFiles) + executor := terraformprovider.NewTerraform( + terraformPath, + ds.AppDir, + terraformprovider.WithoutColor(), + terraformprovider.WithVars(vars), + terraformprovider.WithVarFiles(deployCfg.Input.VarFiles), + ) if err := executor.Init(ctx, buf); err != nil { fmt.Fprintf(buf, "failed while executing terraform init (%v)\n", err)