diff --git a/cli/pkg/digger/digger_test.go b/cli/pkg/digger/digger_test.go index 7b89c2e62..93289f8c4 100644 --- a/cli/pkg/digger/digger_test.go +++ b/cli/pkg/digger/digger_test.go @@ -57,7 +57,7 @@ func (m *MockTerraformExecutor) Destroy(params []string, envs map[string]string) return "", "", nil } -func (m *MockTerraformExecutor) Show(params []string, envs map[string]string, planJsonFilePath string) (string, string, error) { +func (m *MockTerraformExecutor) Show(params []string, envs map[string]string, planJsonFilePath string, b bool) (string, string, error) { nonEmptyTerraformPlanJson := "{\"format_version\":\"1.1\",\"terraform_version\":\"1.4.6\",\"planned_values\":{\"root_module\":{\"resources\":[{\"address\":\"null_resource.test\",\"mode\":\"managed\",\"type\":\"null_resource\",\"name\":\"test\",\"provider_name\":\"registry.terraform.io/hashicorp/null\",\"schema_version\":0,\"values\":{\"id\":\"7587790946951100994\",\"triggers\":null},\"sensitive_values\":{}},{\"address\":\"null_resource.testx\",\"mode\":\"managed\",\"type\":\"null_resource\",\"name\":\"testx\",\"provider_name\":\"registry.terraform.io/hashicorp/null\",\"schema_version\":0,\"values\":{\"triggers\":null},\"sensitive_values\":{}}]}},\"resource_changes\":[{\"address\":\"null_resource.test\",\"mode\":\"managed\",\"type\":\"null_resource\",\"name\":\"test\",\"provider_name\":\"registry.terraform.io/hashicorp/null\",\"change\":{\"actions\":[\"no-op\"],\"before\":{\"id\":\"7587790946951100994\",\"triggers\":null},\"after\":{\"id\":\"7587790946951100994\",\"triggers\":null},\"after_unknown\":{},\"before_sensitive\":{},\"after_sensitive\":{}}},{\"address\":\"null_resource.testx\",\"mode\":\"managed\",\"type\":\"null_resource\",\"name\":\"testx\",\"provider_name\":\"registry.terraform.io/hashicorp/null\",\"change\":{\"actions\":[\"create\"],\"before\":null,\"after\":{\"triggers\":null},\"after_unknown\":{\"id\":true},\"before_sensitive\":false,\"after_sensitive\":{}}}],\"prior_state\":{\"format_version\":\"1.0\",\"terraform_version\":\"1.4.6\",\"values\":{\"root_module\":{\"resources\":[{\"address\":\"null_resource.test\",\"mode\":\"managed\",\"type\":\"null_resource\",\"name\":\"test\",\"provider_name\":\"registry.terraform.io/hashicorp/null\",\"schema_version\":0,\"values\":{\"id\":\"7587790946951100994\",\"triggers\":null},\"sensitive_values\":{}}]}}},\"configuration\":{\"provider_config\":{\"null\":{\"name\":\"null\",\"full_name\":\"registry.terraform.io/hashicorp/null\"}},\"root_module\":{\"resources\":[{\"address\":\"null_resource.test\",\"mode\":\"managed\",\"type\":\"null_resource\",\"name\":\"test\",\"provider_config_key\":\"null\",\"schema_version\":0},{\"address\":\"null_resource.testx\",\"mode\":\"managed\",\"type\":\"null_resource\",\"name\":\"testx\",\"provider_config_key\":\"null\",\"schema_version\":0}]}}}\n" m.Commands = append(m.Commands, RunInfo{"Show", strings.Join(params, " "), time.Now()}) return nonEmptyTerraformPlanJson, "", nil diff --git a/docs/ce/howto/custom-commands.mdx b/docs/ce/howto/custom-commands.mdx index 3cebf0c3e..a8bc0f56d 100644 --- a/docs/ce/howto/custom-commands.mdx +++ b/docs/ce/howto/custom-commands.mdx @@ -37,3 +37,22 @@ If your custom command writes into a file path defined in the `$DIGGER_OUT` env ![](/images/custom-command-output-infracost.png) The value of `$DIGGER_OUT` defaults to `$RUNNER_TEMP/digger-out.log`; you can change that if needed by setting the env var explicitly. + +## Overriding plan commands + +You can add extra arguments to the plan command by setting the `extra_args` key in the `steps` section of the `plan` command. + +However in some cases if you wish to override the plan command entirely you can do it by excluding the plan in the steps and having your command specified in the run like so: + +``` + +workflows: + default: + plan: + steps: + - init + # exclude plan entierly and use custom command + - run: terraform plan -input=false -refresh -no-color -out $DIGGER_PLANFILE +``` + +Note that you need to use the -out flag to write the output to the $DIGGER_PLANFILE env variable, since this will be used in postprocessing steps by digger. \ No newline at end of file diff --git a/libs/execution/execution.go b/libs/execution/execution.go index 4583a2153..9b3b713f7 100644 --- a/libs/execution/execution.go +++ b/libs/execution/execution.go @@ -192,7 +192,7 @@ func (d DiggerExecutor) RetrievePlanJson() (string, error) { } showArgs := make([]string, 0) - terraformPlanOutput, _, _ := executor.TerraformExecutor.Show(showArgs, executor.CommandEnvVars, *storedPlanPath) + terraformPlanOutput, _, _ := executor.TerraformExecutor.Show(showArgs, executor.CommandEnvVars, *storedPlanPath, true) return terraformPlanOutput, nil } else { @@ -202,7 +202,7 @@ func (d DiggerExecutor) RetrievePlanJson() (string, error) { func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, string, error) { plan := "" - terraformPlanOutput := "" + terraformPlanOutputJsonString := "" planSummary := &iac_utils.IacSummary{} isEmptyPlan := true var planSteps []scheduler.Step @@ -219,6 +219,11 @@ func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, strin }, } } + + hasPlanStep := lo.ContainsBy(planSteps, func(step scheduler.Step) bool { + return step.Action == "plan" + }) + for _, step := range planSteps { slog.Info("Running step", "action", step.Action) if step.Action == "init" { @@ -234,46 +239,22 @@ func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, strin // TODO remove those only for pulumi project planArgs = append(planArgs, step.ExtraArgs...) - _, stdout, stderr, err := d.TerraformExecutor.Plan(planArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), d.PlanStage.FilterRegex) - if err != nil { - return nil, false, false, "", "", fmt.Errorf("error executing plan: %v", err) - } - showArgs := make([]string, 0) - terraformPlanOutput, _, _ = d.TerraformExecutor.Show(showArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath()) - - isEmptyPlan, planSummary, err = d.IacUtils.GetSummaryFromPlanJson(terraformPlanOutput) + var err error + var stdout, stderr string + isEmptyPlan, stdout, stderr, err = d.TerraformExecutor.Plan(planArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), d.PlanStage.FilterRegex) if err != nil { - return nil, false, false, "", "", fmt.Errorf("error checking for empty plan: %v", err) - } - - if !isEmptyPlan { - nonEmptyPlanFilepath := strings.Replace(d.PlanPathProvider.LocalPlanFilePath(), d.PlanPathProvider.StoredPlanFilePath(), "isNonEmptyPlan.txt", 1) - file, err := os.Create(nonEmptyPlanFilepath) - if err != nil { - return nil, false, false, "", "", fmt.Errorf("unable to create file: %v", err) - } - defer file.Close() - } - - if d.PlanStorage != nil { - - fileBytes, err := os.ReadFile(d.PlanPathProvider.LocalPlanFilePath()) - if err != nil { - fmt.Println("Error reading file:", err) - return nil, false, false, "", "", fmt.Errorf("error reading file bytes: %v", err) - } - - err = d.PlanStorage.StorePlanFile(fileBytes, d.PlanPathProvider.ArtifactName(), d.PlanPathProvider.StoredPlanFilePath()) - if err != nil { - fmt.Println("Error storing artifact file:", err) - return nil, false, false, "", "", fmt.Errorf("error storing artifact file: %v", err) - } + return nil, false, false, "", "", fmt.Errorf("error executing plan: %v, stdout: %v, stderr: %v", err, stdout, stderr) } - // TODO: move this function to iacUtils interface and implement for pulumi - plan = cleanupTerraformPlan(!isEmptyPlan, err, stdout, stderr) + plan, terraformPlanOutputJsonString, planSummary, isEmptyPlan, err = d.postProcessPlan(stdout) if err != nil { - slog.Error("error publishing comment", "error", err) + slog.Debug("error post processing plan", + "error", err, + "plan", plan, + "planSummary", planSummary, + "isEmptyPlan", isEmptyPlan, + ) + return nil, false, false, "", "", fmt.Errorf("error post processing plan: %v", err) //nolint:wrapcheck // err } } if step.Action == "run" { @@ -297,8 +278,67 @@ func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, strin } } } + + if !hasPlanStep { + rawPlan, _, err := d.TerraformExecutor.Show(make([]string, 0), d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), false) + if err != nil { + return nil, false, false, "", "", fmt.Errorf("error running terraform show: %v", err) + } + plan, terraformPlanOutputJsonString, planSummary, isEmptyPlan, err = d.postProcessPlan(rawPlan) + if err != nil { + slog.Debug("error post processing plan", + "error", err, + "plan", plan, + "planSummary", planSummary, + "isEmptyPlan", isEmptyPlan, + ) + return nil, false, false, "", "", fmt.Errorf("error post processing plan: %v", err) //nolint:wrapcheck // err + } + } + reportAdditionalOutput(d.Reporter, d.projectId()) - return planSummary, true, !isEmptyPlan, plan, terraformPlanOutput, nil + return planSummary, true, !isEmptyPlan, plan, terraformPlanOutputJsonString, nil +} + +func (d DiggerExecutor) postProcessPlan(stdout string) (string, string, *iac_utils.IacSummary, bool, error) { + showArgs := make([]string, 0) + terraformPlanJsonOutputString, _, err := d.TerraformExecutor.Show(showArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), true) + if err != nil { + return "", "", nil, false, fmt.Errorf("error running terraform show: %v", err) + } + + isEmptyPlan, planSummary, err := d.IacUtils.GetSummaryFromPlanJson(terraformPlanJsonOutputString) + if err != nil { + return "", "", nil, false, fmt.Errorf("error checking for empty plan: %v", err) + } + + if !isEmptyPlan { + nonEmptyPlanFilepath := strings.Replace(d.PlanPathProvider.LocalPlanFilePath(), d.PlanPathProvider.StoredPlanFilePath(), "isNonEmptyPlan.txt", 1) + file, err := os.Create(nonEmptyPlanFilepath) + if err != nil { + return "", "", nil, false, fmt.Errorf("unable to create file: %v", err) + } + defer file.Close() + } + + if d.PlanStorage != nil { + fileBytes, err := os.ReadFile(d.PlanPathProvider.LocalPlanFilePath()) + if err != nil { + fmt.Println("Error reading file:", err) + return "", "", nil, false, fmt.Errorf("error reading file bytes: %v", err) + } + + err = d.PlanStorage.StorePlanFile(fileBytes, d.PlanPathProvider.ArtifactName(), d.PlanPathProvider.StoredPlanFilePath()) + if err != nil { + fmt.Println("Error storing artifact file:", err) + return "", "", nil, false, fmt.Errorf("error storing artifact file: %v", err) + + } + } + + // TODO: move this function to iacUtils interface and implement for pulumi + cleanedUpPlan := cleanupTerraformPlan(stdout) + return cleanedUpPlan, terraformPlanJsonOutputString, planSummary, isEmptyPlan, nil } func reportError(r reporting.Reporter, stderr string) { @@ -483,9 +523,7 @@ func (d DiggerExecutor) Destroy() (bool, error) { return true, nil } -func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string, stderr string, regexStr *string) string { - var errorStr string - +func cleanupTerraformOutput(stdout string, regexStr *string) string { // removes output of terraform -version command that terraform-exec executes on every run i := strings.Index(stdout, "Initializing the backend...") if i != -1 { @@ -493,15 +531,6 @@ func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string, } endPos := len(stdout) - if planError != nil { - if stderr != "" { - errorStr = stderr - } else if stdout != "" { - errorStr = stdout - } - return errorStr - } - delimiters := []string{ "Terraform will perform the following actions:", "OpenTofu will perform the following actions:", @@ -535,12 +564,12 @@ func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string, } func cleanupTerraformApply(nonEmptyPlan bool, planError error, stdout string, stderr string) string { - return cleanupTerraformOutput(nonEmptyPlan, planError, stdout, stderr, nil) + return cleanupTerraformOutput(stdout, nil) } -func cleanupTerraformPlan(nonEmptyPlan bool, planError error, stdout string, stderr string) string { +func cleanupTerraformPlan(stdout string) string { regex := `───────────.+` - return cleanupTerraformOutput(nonEmptyPlan, planError, stdout, stderr, ®ex) + return cleanupTerraformOutput(stdout, ®ex) } func (d DiggerExecutor) projectId() string { diff --git a/libs/execution/execution_test.go b/libs/execution/execution_test.go index 096f5c0ed..25cffd376 100644 --- a/libs/execution/execution_test.go +++ b/libs/execution/execution_test.go @@ -65,7 +65,7 @@ Terraform will perform the following actions: Plan: 2 to add, 0 to change, 0 to destroy. ` - res := cleanupTerraformPlan(true, nil, stdout, "") + res := cleanupTerraformPlan(stdout) index := strings.Index(stdout, "Terraform will perform the following actions:") assert.Equal(t, stdout[index:], res) } @@ -256,7 +256,7 @@ Plan: 9 to add, 0 to change, 0 to destroy. Changes to Outputs: + api_url = (known after apply) ` - res := cleanupTerraformPlan(true, nil, stdout, "") + res := cleanupTerraformPlan(stdout) index := strings.Index(stdout, "OpenTofu will perform the following actions:") assert.Equal(t, stdout[index:], res) } diff --git a/libs/execution/opentofu.go b/libs/execution/opentofu.go index d7189d4fb..253a41747 100644 --- a/libs/execution/opentofu.go +++ b/libs/execution/opentofu.go @@ -63,8 +63,12 @@ func (tf OpenTofu) Plan(params []string, envs map[string]string, planArtefactFil return statusCode == 2, stdout, stderr, nil } -func (tf OpenTofu) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) { - params = append(params, []string{"-no-color", "-json", planArtefactFilePath}...) +func (tf OpenTofu) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) { + params = append(params, "-no-color") + if returnJson { + params = append(params, "-json") + } + params = append(params, planArtefactFilePath) stdout, stderr, _, err := tf.runOpentofuCommand("show", false, envs, nil, params...) if err != nil { return "", "", err diff --git a/libs/execution/pulumi.go b/libs/execution/pulumi.go index 91def1f29..aedbd4c91 100644 --- a/libs/execution/pulumi.go +++ b/libs/execution/pulumi.go @@ -45,10 +45,13 @@ func (pl Pulumi) Plan(params []string, envs map[string]string, planArtefactFileP return statusCode == 2, stdout, stderr, nil } -func (pl Pulumi) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) { +func (pl Pulumi) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) { pl.selectStack() // TODO figure out how to avoid running a second plan (preview) here - params = append(params, []string{"--json"}...) + if returnJson { + params = append(params, []string{"--json"}...) + } + stdout, stderr, statusCode, err := pl.runPululmiCommand("preview", false, envs, params...) if err != nil && statusCode != 2 { return "", "", err diff --git a/libs/execution/terragrunt.go b/libs/execution/terragrunt.go index 582c5b4b5..8839126ab 100644 --- a/libs/execution/terragrunt.go +++ b/libs/execution/terragrunt.go @@ -62,8 +62,12 @@ func (terragrunt Terragrunt) Plan(params []string, envs map[string]string, planA return true, stdout, stderr, err } -func (terragrunt Terragrunt) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) { - params = append(params, []string{"-no-color", "-json", planArtefactFilePath}...) +func (terragrunt Terragrunt) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) { + params = append(params, "-no-color") + if returnJson { + params = append(params, "-json") + } + params = append(params, planArtefactFilePath) stdout, stderr, exitCode, err := terragrunt.runTerragruntCommand("show", false, envs, nil, params...) if exitCode != 0 { logCommandFail(exitCode, err) diff --git a/libs/execution/tf.go b/libs/execution/tf.go index 924dadb34..80fa7f01d 100644 --- a/libs/execution/tf.go +++ b/libs/execution/tf.go @@ -16,7 +16,7 @@ type TerraformExecutor interface { Apply([]string, *string, map[string]string) (string, string, error) Destroy([]string, map[string]string) (string, string, error) Plan([]string, map[string]string, string, *string) (bool, string, string, error) - Show([]string, map[string]string, string) (string, string, error) + Show([]string, map[string]string, string, bool) (string, string, error) } type Terraform struct { @@ -167,8 +167,12 @@ func (tf Terraform) Plan(params []string, envs map[string]string, planArtefactFi return statusCode == 2, stdout, stderr, nil } -func (tf Terraform) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) { - params = append(params, []string{"-no-color", "-json", planArtefactFilePath}...) +func (tf Terraform) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) { + params = append(params, "-no-color") + if returnJson { + params = append(params, "-json") + } + params = append(params, planArtefactFilePath) stdout, stderr, _, err := tf.runTerraformCommand("show", false, envs, nil, params...) if err != nil { return "", "", err