diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index a209761454..eb1edd64f7 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -63,6 +63,7 @@ type plan struct { RelevantAttributes []ResourceAttr `json:"relevant_attributes,omitempty"` Checks json.RawMessage `json:"checks,omitempty"` Timestamp string `json:"timestamp,omitempty"` + Errored bool `json:"errored"` } func newPlan() *plan { @@ -221,6 +222,7 @@ func Marshal( output := newPlan() output.TerraformVersion = version.String() output.Timestamp = p.Timestamp.Format(time.RFC3339) + output.Errored = p.Errored err := output.marshalPlanVariables(p.VariableValues, config.Module.Variables) if err != nil { diff --git a/internal/command/show_test.go b/internal/command/show_test.go index 7ddeea0703..b3e3e6116c 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -421,7 +421,43 @@ func TestShow_planWithForceReplaceChange(t *testing.T) { if !strings.Contains(got, want) { t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) } +} + +func TestShow_planErrored(t *testing.T) { + _, snap := testModuleWithSnapshot(t, "show") + plan := testPlan(t) + plan.Errored = true + planFilePath := testPlanFile( + t, + snap, + states.NewState(), + plan, + ) + + view, done := testView(t) + c := &ShowCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(showFixtureProvider()), + View: view, + }, + } + args := []string{ + planFilePath, + "-no-color", + } + code := c.Run(args) + output := done(t) + + if code != 0 { + t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) + } + + got := output.Stdout() + want := `Planning failed. Terraform encountered an error while generating this plan.` + if !strings.Contains(got, want) { + t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) + } } func TestShow_plan_json(t *testing.T) { @@ -525,6 +561,20 @@ func TestShow_json_output(t *testing.T) { t.Fatalf("init failed\n%s", ui.ErrorWriter) } + // read expected output + wantFile, err := os.Open("output.json") + if err != nil { + t.Fatalf("unexpected err: %s", err) + } + defer wantFile.Close() + byteValue, err := ioutil.ReadAll(wantFile) + if err != nil { + t.Fatalf("unexpected err: %s", err) + } + + var want plan + json.Unmarshal([]byte(byteValue), &want) + // plan planView, planDone := testView(t) pc := &PlanCommand{ @@ -542,8 +592,15 @@ func TestShow_json_output(t *testing.T) { code := pc.Run(args) planOutput := planDone(t) - if code != 0 { - t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, planOutput.Stderr()) + var wantedCode int + if want.Errored { + wantedCode = 1 + } else { + wantedCode = 0 + } + + if code != wantedCode { + t.Fatalf("unexpected exit status %d; want %d\ngot: %s", code, wantedCode, planOutput.Stderr()) } // show @@ -569,22 +626,11 @@ func TestShow_json_output(t *testing.T) { } // compare view output to wanted output - var got, want plan + var got plan gotString := showOutput.Stdout() json.Unmarshal([]byte(gotString), &got) - wantFile, err := os.Open("output.json") - if err != nil { - t.Fatalf("unexpected err: %s", err) - } - defer wantFile.Close() - byteValue, err := ioutil.ReadAll(wantFile) - if err != nil { - t.Fatalf("unexpected err: %s", err) - } - json.Unmarshal([]byte(byteValue), &want) - // Disregard format version to reduce needless test fixture churn want.FormatVersion = got.FormatVersion @@ -1150,6 +1196,7 @@ type plan struct { OutputChanges map[string]interface{} `json:"output_changes,omitempty"` PriorState priorState `json:"prior_state,omitempty"` Config map[string]interface{} `json:"configuration,omitempty"` + Errored bool `json:"errored"` } type priorState struct { diff --git a/internal/command/testdata/show-json/plan-error/main.tf b/internal/command/testdata/show-json/plan-error/main.tf new file mode 100644 index 0000000000..c26c1a0aa4 --- /dev/null +++ b/internal/command/testdata/show-json/plan-error/main.tf @@ -0,0 +1,15 @@ +locals { + ami = "bar" +} + +resource "test_instance" "test" { + ami = local.ami + + lifecycle { + precondition { + // failing condition + condition = local.ami != "bar" + error_message = "ami is bar" + } + } +} \ No newline at end of file diff --git a/internal/command/testdata/show-json/plan-error/output.json b/internal/command/testdata/show-json/plan-error/output.json new file mode 100644 index 0000000000..9bd021972b --- /dev/null +++ b/internal/command/testdata/show-json/plan-error/output.json @@ -0,0 +1,35 @@ +{ + "format_version": "1.2", + "planned_values": { + "root_module": {} + }, + "prior_state": {}, + "configuration": { + "provider_config": { + "test": { + "full_name": "registry.terraform.io/hashicorp/test", + "name": "test" + } + }, + "root_module": { + "resources": [ + { + "address": "test_instance.test", + "expressions": { + "ami": { + "references": [ + "local.ami" + ] + } + }, + "mode": "managed", + "name": "test", + "provider_config_key": "test", + "schema_version": 0, + "type": "test_instance" + } + ] + } + }, + "errored": true +} \ No newline at end of file diff --git a/website/docs/internals/json-format.mdx b/website/docs/internals/json-format.mdx index 822a9b757d..d4c95041b5 100644 --- a/website/docs/internals/json-format.mdx +++ b/website/docs/internals/json-format.mdx @@ -229,7 +229,11 @@ For ease of consumption by callers, the plan representation includes a partial r // resources with postconditions, with as much information as Terraform can // recognize at plan time. Some objects will have status "unknown" to // indicate that their status will only be determined after applying the plan. - "checks" + "checks" , + + // "errored" indicates whether planning failed. An errored plan cannot be applied, + // but the actions planned before failure may help to understand the error. + "errored": false } ```