Skip to content

Commit 9114953

Browse files
authored
support PLANOUT (#2077)
1 parent 9140a7a commit 9114953

File tree

8 files changed

+129
-66
lines changed

8 files changed

+129
-66
lines changed

cli/pkg/digger/digger_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (m *MockTerraformExecutor) Destroy(params []string, envs map[string]string)
5757
return "", "", nil
5858
}
5959

60-
func (m *MockTerraformExecutor) Show(params []string, envs map[string]string, planJsonFilePath string) (string, string, error) {
60+
func (m *MockTerraformExecutor) Show(params []string, envs map[string]string, planJsonFilePath string, b bool) (string, string, error) {
6161
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"
6262
m.Commands = append(m.Commands, RunInfo{"Show", strings.Join(params, " "), time.Now()})
6363
return nonEmptyTerraformPlanJson, "", nil

docs/ce/howto/custom-commands.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,22 @@ If your custom command writes into a file path defined in the `$DIGGER_OUT` env
3737
![](/images/custom-command-output-infracost.png)
3838

3939
The value of `$DIGGER_OUT` defaults to `$RUNNER_TEMP/digger-out.log`; you can change that if needed by setting the env var explicitly.
40+
41+
## Overriding plan commands
42+
43+
You can add extra arguments to the plan command by setting the `extra_args` key in the `steps` section of the `plan` command.
44+
45+
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:
46+
47+
```
48+
49+
workflows:
50+
default:
51+
plan:
52+
steps:
53+
- init
54+
# exclude plan entierly and use custom command
55+
- run: terraform plan -input=false -refresh -no-color -out $DIGGER_PLANFILE
56+
```
57+
58+
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.

libs/execution/execution.go

Lines changed: 83 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func (d DiggerExecutor) RetrievePlanJson() (string, error) {
192192
}
193193

194194
showArgs := make([]string, 0)
195-
terraformPlanOutput, _, _ := executor.TerraformExecutor.Show(showArgs, executor.CommandEnvVars, *storedPlanPath)
195+
terraformPlanOutput, _, _ := executor.TerraformExecutor.Show(showArgs, executor.CommandEnvVars, *storedPlanPath, true)
196196
return terraformPlanOutput, nil
197197

198198
} else {
@@ -202,7 +202,7 @@ func (d DiggerExecutor) RetrievePlanJson() (string, error) {
202202

203203
func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, string, error) {
204204
plan := ""
205-
terraformPlanOutput := ""
205+
terraformPlanOutputJsonString := ""
206206
planSummary := &iac_utils.IacSummary{}
207207
isEmptyPlan := true
208208
var planSteps []scheduler.Step
@@ -219,6 +219,11 @@ func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, strin
219219
},
220220
}
221221
}
222+
223+
hasPlanStep := lo.ContainsBy(planSteps, func(step scheduler.Step) bool {
224+
return step.Action == "plan"
225+
})
226+
222227
for _, step := range planSteps {
223228
slog.Info("Running step", "action", step.Action)
224229
if step.Action == "init" {
@@ -234,46 +239,22 @@ func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, strin
234239
// TODO remove those only for pulumi project
235240
planArgs = append(planArgs, step.ExtraArgs...)
236241

237-
_, stdout, stderr, err := d.TerraformExecutor.Plan(planArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), d.PlanStage.FilterRegex)
238-
if err != nil {
239-
return nil, false, false, "", "", fmt.Errorf("error executing plan: %v", err)
240-
}
241-
showArgs := make([]string, 0)
242-
terraformPlanOutput, _, _ = d.TerraformExecutor.Show(showArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath())
243-
244-
isEmptyPlan, planSummary, err = d.IacUtils.GetSummaryFromPlanJson(terraformPlanOutput)
242+
var err error
243+
var stdout, stderr string
244+
isEmptyPlan, stdout, stderr, err = d.TerraformExecutor.Plan(planArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), d.PlanStage.FilterRegex)
245245
if err != nil {
246-
return nil, false, false, "", "", fmt.Errorf("error checking for empty plan: %v", err)
247-
}
248-
249-
if !isEmptyPlan {
250-
nonEmptyPlanFilepath := strings.Replace(d.PlanPathProvider.LocalPlanFilePath(), d.PlanPathProvider.StoredPlanFilePath(), "isNonEmptyPlan.txt", 1)
251-
file, err := os.Create(nonEmptyPlanFilepath)
252-
if err != nil {
253-
return nil, false, false, "", "", fmt.Errorf("unable to create file: %v", err)
254-
}
255-
defer file.Close()
256-
}
257-
258-
if d.PlanStorage != nil {
259-
260-
fileBytes, err := os.ReadFile(d.PlanPathProvider.LocalPlanFilePath())
261-
if err != nil {
262-
fmt.Println("Error reading file:", err)
263-
return nil, false, false, "", "", fmt.Errorf("error reading file bytes: %v", err)
264-
}
265-
266-
err = d.PlanStorage.StorePlanFile(fileBytes, d.PlanPathProvider.ArtifactName(), d.PlanPathProvider.StoredPlanFilePath())
267-
if err != nil {
268-
fmt.Println("Error storing artifact file:", err)
269-
return nil, false, false, "", "", fmt.Errorf("error storing artifact file: %v", err)
270-
}
246+
return nil, false, false, "", "", fmt.Errorf("error executing plan: %v, stdout: %v, stderr: %v", err, stdout, stderr)
271247
}
272248

273-
// TODO: move this function to iacUtils interface and implement for pulumi
274-
plan = cleanupTerraformPlan(!isEmptyPlan, err, stdout, stderr)
249+
plan, terraformPlanOutputJsonString, planSummary, isEmptyPlan, err = d.postProcessPlan(stdout)
275250
if err != nil {
276-
slog.Error("error publishing comment", "error", err)
251+
slog.Debug("error post processing plan",
252+
"error", err,
253+
"plan", plan,
254+
"planSummary", planSummary,
255+
"isEmptyPlan", isEmptyPlan,
256+
)
257+
return nil, false, false, "", "", fmt.Errorf("error post processing plan: %v", err) //nolint:wrapcheck // err
277258
}
278259
}
279260
if step.Action == "run" {
@@ -297,8 +278,67 @@ func (d DiggerExecutor) Plan() (*iac_utils.IacSummary, bool, bool, string, strin
297278
}
298279
}
299280
}
281+
282+
if !hasPlanStep {
283+
rawPlan, _, err := d.TerraformExecutor.Show(make([]string, 0), d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), false)
284+
if err != nil {
285+
return nil, false, false, "", "", fmt.Errorf("error running terraform show: %v", err)
286+
}
287+
plan, terraformPlanOutputJsonString, planSummary, isEmptyPlan, err = d.postProcessPlan(rawPlan)
288+
if err != nil {
289+
slog.Debug("error post processing plan",
290+
"error", err,
291+
"plan", plan,
292+
"planSummary", planSummary,
293+
"isEmptyPlan", isEmptyPlan,
294+
)
295+
return nil, false, false, "", "", fmt.Errorf("error post processing plan: %v", err) //nolint:wrapcheck // err
296+
}
297+
}
298+
300299
reportAdditionalOutput(d.Reporter, d.projectId())
301-
return planSummary, true, !isEmptyPlan, plan, terraformPlanOutput, nil
300+
return planSummary, true, !isEmptyPlan, plan, terraformPlanOutputJsonString, nil
301+
}
302+
303+
func (d DiggerExecutor) postProcessPlan(stdout string) (string, string, *iac_utils.IacSummary, bool, error) {
304+
showArgs := make([]string, 0)
305+
terraformPlanJsonOutputString, _, err := d.TerraformExecutor.Show(showArgs, d.CommandEnvVars, d.PlanPathProvider.LocalPlanFilePath(), true)
306+
if err != nil {
307+
return "", "", nil, false, fmt.Errorf("error running terraform show: %v", err)
308+
}
309+
310+
isEmptyPlan, planSummary, err := d.IacUtils.GetSummaryFromPlanJson(terraformPlanJsonOutputString)
311+
if err != nil {
312+
return "", "", nil, false, fmt.Errorf("error checking for empty plan: %v", err)
313+
}
314+
315+
if !isEmptyPlan {
316+
nonEmptyPlanFilepath := strings.Replace(d.PlanPathProvider.LocalPlanFilePath(), d.PlanPathProvider.StoredPlanFilePath(), "isNonEmptyPlan.txt", 1)
317+
file, err := os.Create(nonEmptyPlanFilepath)
318+
if err != nil {
319+
return "", "", nil, false, fmt.Errorf("unable to create file: %v", err)
320+
}
321+
defer file.Close()
322+
}
323+
324+
if d.PlanStorage != nil {
325+
fileBytes, err := os.ReadFile(d.PlanPathProvider.LocalPlanFilePath())
326+
if err != nil {
327+
fmt.Println("Error reading file:", err)
328+
return "", "", nil, false, fmt.Errorf("error reading file bytes: %v", err)
329+
}
330+
331+
err = d.PlanStorage.StorePlanFile(fileBytes, d.PlanPathProvider.ArtifactName(), d.PlanPathProvider.StoredPlanFilePath())
332+
if err != nil {
333+
fmt.Println("Error storing artifact file:", err)
334+
return "", "", nil, false, fmt.Errorf("error storing artifact file: %v", err)
335+
336+
}
337+
}
338+
339+
// TODO: move this function to iacUtils interface and implement for pulumi
340+
cleanedUpPlan := cleanupTerraformPlan(stdout)
341+
return cleanedUpPlan, terraformPlanJsonOutputString, planSummary, isEmptyPlan, nil
302342
}
303343

304344
func reportError(r reporting.Reporter, stderr string) {
@@ -483,25 +523,14 @@ func (d DiggerExecutor) Destroy() (bool, error) {
483523
return true, nil
484524
}
485525

486-
func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string, stderr string, regexStr *string) string {
487-
var errorStr string
488-
526+
func cleanupTerraformOutput(stdout string, regexStr *string) string {
489527
// removes output of terraform -version command that terraform-exec executes on every run
490528
i := strings.Index(stdout, "Initializing the backend...")
491529
if i != -1 {
492530
stdout = stdout[i:]
493531
}
494532
endPos := len(stdout)
495533

496-
if planError != nil {
497-
if stderr != "" {
498-
errorStr = stderr
499-
} else if stdout != "" {
500-
errorStr = stdout
501-
}
502-
return errorStr
503-
}
504-
505534
delimiters := []string{
506535
"Terraform will perform the following actions:",
507536
"OpenTofu will perform the following actions:",
@@ -535,12 +564,12 @@ func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string,
535564
}
536565

537566
func cleanupTerraformApply(nonEmptyPlan bool, planError error, stdout string, stderr string) string {
538-
return cleanupTerraformOutput(nonEmptyPlan, planError, stdout, stderr, nil)
567+
return cleanupTerraformOutput(stdout, nil)
539568
}
540569

541-
func cleanupTerraformPlan(nonEmptyPlan bool, planError error, stdout string, stderr string) string {
570+
func cleanupTerraformPlan(stdout string) string {
542571
regex := `───────────.+`
543-
return cleanupTerraformOutput(nonEmptyPlan, planError, stdout, stderr, &regex)
572+
return cleanupTerraformOutput(stdout, &regex)
544573
}
545574

546575
func (d DiggerExecutor) projectId() string {

libs/execution/execution_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Terraform will perform the following actions:
6565
6666
Plan: 2 to add, 0 to change, 0 to destroy.
6767
`
68-
res := cleanupTerraformPlan(true, nil, stdout, "")
68+
res := cleanupTerraformPlan(stdout)
6969
index := strings.Index(stdout, "Terraform will perform the following actions:")
7070
assert.Equal(t, stdout[index:], res)
7171
}
@@ -256,7 +256,7 @@ Plan: 9 to add, 0 to change, 0 to destroy.
256256
Changes to Outputs:
257257
+ api_url = (known after apply)
258258
`
259-
res := cleanupTerraformPlan(true, nil, stdout, "")
259+
res := cleanupTerraformPlan(stdout)
260260
index := strings.Index(stdout, "OpenTofu will perform the following actions:")
261261
assert.Equal(t, stdout[index:], res)
262262
}

libs/execution/opentofu.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,12 @@ func (tf OpenTofu) Plan(params []string, envs map[string]string, planArtefactFil
6363
return statusCode == 2, stdout, stderr, nil
6464
}
6565

66-
func (tf OpenTofu) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) {
67-
params = append(params, []string{"-no-color", "-json", planArtefactFilePath}...)
66+
func (tf OpenTofu) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) {
67+
params = append(params, "-no-color")
68+
if returnJson {
69+
params = append(params, "-json")
70+
}
71+
params = append(params, planArtefactFilePath)
6872
stdout, stderr, _, err := tf.runOpentofuCommand("show", false, envs, nil, params...)
6973
if err != nil {
7074
return "", "", err

libs/execution/pulumi.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ func (pl Pulumi) Plan(params []string, envs map[string]string, planArtefactFileP
4545
return statusCode == 2, stdout, stderr, nil
4646
}
4747

48-
func (pl Pulumi) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) {
48+
func (pl Pulumi) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) {
4949
pl.selectStack()
5050
// TODO figure out how to avoid running a second plan (preview) here
51-
params = append(params, []string{"--json"}...)
51+
if returnJson {
52+
params = append(params, []string{"--json"}...)
53+
}
54+
5255
stdout, stderr, statusCode, err := pl.runPululmiCommand("preview", false, envs, params...)
5356
if err != nil && statusCode != 2 {
5457
return "", "", err

libs/execution/terragrunt.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,12 @@ func (terragrunt Terragrunt) Plan(params []string, envs map[string]string, planA
6262
return true, stdout, stderr, err
6363
}
6464

65-
func (terragrunt Terragrunt) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) {
66-
params = append(params, []string{"-no-color", "-json", planArtefactFilePath}...)
65+
func (terragrunt Terragrunt) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) {
66+
params = append(params, "-no-color")
67+
if returnJson {
68+
params = append(params, "-json")
69+
}
70+
params = append(params, planArtefactFilePath)
6771
stdout, stderr, exitCode, err := terragrunt.runTerragruntCommand("show", false, envs, nil, params...)
6872
if exitCode != 0 {
6973
logCommandFail(exitCode, err)

libs/execution/tf.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type TerraformExecutor interface {
1616
Apply([]string, *string, map[string]string) (string, string, error)
1717
Destroy([]string, map[string]string) (string, string, error)
1818
Plan([]string, map[string]string, string, *string) (bool, string, string, error)
19-
Show([]string, map[string]string, string) (string, string, error)
19+
Show([]string, map[string]string, string, bool) (string, string, error)
2020
}
2121

2222
type Terraform struct {
@@ -167,8 +167,12 @@ func (tf Terraform) Plan(params []string, envs map[string]string, planArtefactFi
167167
return statusCode == 2, stdout, stderr, nil
168168
}
169169

170-
func (tf Terraform) Show(params []string, envs map[string]string, planArtefactFilePath string) (string, string, error) {
171-
params = append(params, []string{"-no-color", "-json", planArtefactFilePath}...)
170+
func (tf Terraform) Show(params []string, envs map[string]string, planArtefactFilePath string, returnJson bool) (string, string, error) {
171+
params = append(params, "-no-color")
172+
if returnJson {
173+
params = append(params, "-json")
174+
}
175+
params = append(params, planArtefactFilePath)
172176
stdout, stderr, _, err := tf.runTerraformCommand("show", false, envs, nil, params...)
173177
if err != nil {
174178
return "", "", err

0 commit comments

Comments
 (0)