Skip to content

Commit 9263e7e

Browse files
committed
emit apply change summary on failure
1 parent e3d2b7d commit 9263e7e

File tree

5 files changed

+161
-17
lines changed

5 files changed

+161
-17
lines changed

internal/command/apply.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,19 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
126126
return 1
127127
}
128128

129-
if op.Result != backendrun.OperationSuccess {
130-
return op.Result.ExitStatus()
131-
}
132-
133129
// Render the resource count and outputs, unless those counts are being
134130
// rendered already in a remote Terraform process.
135131
if rb, isRemoteBackend := be.(BackendWithRemoteTerraformVersion); !isRemoteBackend || rb.IsLocalOperations() {
136-
view.ResourceCount(args.State.StateOutPath)
137-
if !c.Destroy && op.State != nil {
132+
view.ResourceCount(args.State.StateOutPath, op.Result != backendrun.OperationSuccess)
133+
if op.Result == backendrun.OperationSuccess && !c.Destroy && op.State != nil {
138134
view.Outputs(op.State.RootOutputValues)
139135
}
140136
}
141137

138+
if op.Result != backendrun.OperationSuccess {
139+
return op.Result.ExitStatus()
140+
}
141+
142142
view.Diagnostics(diags)
143143

144144
if diags.HasErrors() {

internal/command/apply_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,78 @@ func TestApply_error(t *testing.T) {
528528
}
529529
}
530530

531+
func TestApply_jsonErrorIncludesChangeSummary(t *testing.T) {
532+
td := t.TempDir()
533+
testCopyDir(t, testFixturePath("apply-error"), td)
534+
t.Chdir(td)
535+
536+
statePath := testTempFile(t)
537+
538+
p := testProvider()
539+
view, done := testView(t)
540+
c := &ApplyCommand{
541+
Meta: Meta{
542+
testingOverrides: metaOverridesForProvider(p),
543+
View: view,
544+
},
545+
}
546+
547+
var lock sync.Mutex
548+
errored := false
549+
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
550+
lock.Lock()
551+
defer lock.Unlock()
552+
553+
if !errored {
554+
errored = true
555+
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error"))
556+
}
557+
558+
s := req.PlannedState.AsValueMap()
559+
s["id"] = cty.StringVal("foo")
560+
561+
resp.NewState = cty.ObjectVal(s)
562+
return
563+
}
564+
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
565+
s := req.ProposedNewState.AsValueMap()
566+
s["id"] = cty.UnknownVal(cty.String)
567+
resp.PlannedState = cty.ObjectVal(s)
568+
return
569+
}
570+
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
571+
ResourceTypes: map[string]providers.Schema{
572+
"test_instance": {
573+
Body: &configschema.Block{
574+
Attributes: map[string]*configschema.Attribute{
575+
"id": {Type: cty.String, Optional: true, Computed: true},
576+
"ami": {Type: cty.String, Optional: true},
577+
"error": {Type: cty.Bool, Optional: true},
578+
},
579+
},
580+
},
581+
},
582+
}
583+
584+
args := []string{
585+
"-json",
586+
"-state", statePath,
587+
"-auto-approve",
588+
}
589+
code := c.Run(args)
590+
output := done(t)
591+
if code != 1 {
592+
t.Fatalf("wrong exit code %d; want 1\n%s", code, output.Stdout())
593+
}
594+
595+
if got, want := output.Stdout(), `"type":"change_summary"`; !strings.Contains(got, want) {
596+
t.Fatalf("missing change summary in JSON output:\n%s", got)
597+
}
598+
if got, want := output.Stdout(), `"@message":"Apply incomplete with errors! Resources: 1 added, 0 changed, 0 destroyed."`; !strings.Contains(got, want) {
599+
t.Fatalf("missing partial apply summary in JSON output:\n%s", got)
600+
}
601+
}
602+
531603
func TestApply_input(t *testing.T) {
532604
// Create a temporary working directory that is empty
533605
td := t.TempDir()

internal/command/views/apply.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616

1717
// The Apply view is used for the apply command.
1818
type Apply interface {
19-
ResourceCount(stateOutPath string)
19+
ResourceCount(stateOutPath string, errored bool)
2020
Outputs(outputValues map[string]*states.OutputValue)
2121

2222
Operation() Operation
@@ -60,25 +60,33 @@ type ApplyHuman struct {
6060

6161
var _ Apply = (*ApplyHuman)(nil)
6262

63-
func (v *ApplyHuman) ResourceCount(stateOutPath string) {
63+
func (v *ApplyHuman) ResourceCount(stateOutPath string, errored bool) {
6464
var summary string
65+
summaryColor := "[reset][bold][green]"
66+
completionString := "complete"
67+
if errored {
68+
summaryColor = "[reset][bold][red]"
69+
completionString = "incomplete with errors"
70+
}
6571
if v.destroy {
66-
summary = fmt.Sprintf("Destroy complete! Resources: %d destroyed.", v.countHook.Removed)
72+
summary = fmt.Sprintf("Destroy %s! Resources: %d destroyed.", completionString, v.countHook.Removed)
6773
} else if v.countHook.Imported > 0 {
68-
summary = fmt.Sprintf("Apply complete! Resources: %d imported, %d added, %d changed, %d destroyed.",
74+
summary = fmt.Sprintf("Apply %s! Resources: %d imported, %d added, %d changed, %d destroyed.",
75+
completionString,
6976
v.countHook.Imported,
7077
v.countHook.Added,
7178
v.countHook.Changed,
7279
v.countHook.Removed)
7380
} else {
74-
summary = fmt.Sprintf("Apply complete! Resources: %d added, %d changed, %d destroyed.",
81+
summary = fmt.Sprintf("Apply %s! Resources: %d added, %d changed, %d destroyed.",
82+
completionString,
7583
v.countHook.Added,
7684
v.countHook.Changed,
7785
v.countHook.Removed)
7886
}
79-
v.view.streams.Print(v.view.colorize.Color("[reset][bold][green]\n" + summary))
87+
v.view.streams.Print(v.view.colorize.Color(summaryColor + "\n" + summary))
8088
if v.countHook.ActionInvocation > 0 {
81-
v.view.streams.Print(v.view.colorize.Color(fmt.Sprintf("[reset][bold][green] Actions: %d invoked.", v.countHook.ActionInvocation)))
89+
v.view.streams.Print(v.view.colorize.Color(fmt.Sprintf("%s Actions: %d invoked.", summaryColor, v.countHook.ActionInvocation)))
8290
}
8391
v.view.streams.Print("\n")
8492
if (v.countHook.Added > 0 || v.countHook.Changed > 0) && stateOutPath != "" {
@@ -131,7 +139,7 @@ type ApplyJSON struct {
131139

132140
var _ Apply = (*ApplyJSON)(nil)
133141

134-
func (v *ApplyJSON) ResourceCount(stateOutPath string) {
142+
func (v *ApplyJSON) ResourceCount(stateOutPath string, errored bool) {
135143
operation := json.OperationApplied
136144
if v.destroy {
137145
operation = json.OperationDestroyed
@@ -143,6 +151,7 @@ func (v *ApplyJSON) ResourceCount(stateOutPath string) {
143151
Import: v.countHook.Imported,
144152
ActionInvocation: v.countHook.ActionInvocation,
145153
Operation: operation,
154+
Errored: errored,
146155
})
147156
}
148157

internal/command/views/apply_test.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func TestApply_resourceCount(t *testing.T) {
153153
count.Imported = 1
154154
}
155155

156-
v.ResourceCount("")
156+
v.ResourceCount("", false)
157157

158158
got := done(t).Stdout()
159159
if !strings.Contains(got, tc.want) {
@@ -222,7 +222,7 @@ func TestApplyHuman_resourceCountStatePath(t *testing.T) {
222222
count.Changed = tc.changed
223223
count.Removed = tc.removed
224224

225-
v.ResourceCount(tc.statePath)
225+
v.ResourceCount(tc.statePath, false)
226226

227227
got := done(t).Stdout()
228228
want := "State path: " + tc.statePath
@@ -268,3 +268,61 @@ func TestApplyJSON_outputs(t *testing.T) {
268268
}
269269
testJSONViewOutputEquals(t, done(t).Stdout(), want)
270270
}
271+
272+
func TestApplyHuman_resourceCountErrored(t *testing.T) {
273+
streams, done := terminal.StreamsForTesting(t)
274+
view := NewView(streams)
275+
view.Configure(&arguments.View{NoColor: false})
276+
v := NewApply(arguments.ViewHuman, false, view)
277+
hooks := v.Hooks()
278+
279+
var count *countHook
280+
for _, hook := range hooks {
281+
if ch, ok := hook.(*countHook); ok {
282+
count = ch
283+
}
284+
}
285+
if count == nil {
286+
t.Fatalf("expected Hooks to include a countHook: %#v", hooks)
287+
}
288+
289+
count.Added = 1
290+
count.Changed = 2
291+
count.Removed = 3
292+
293+
v.ResourceCount("", true)
294+
295+
got := done(t).Stdout()
296+
want := "\x1b[0m\x1b[1m\x1b[31m\nApply incomplete with errors! Resources: 1 added, 2 changed, 3 destroyed."
297+
if !strings.Contains(got, want) {
298+
t.Fatalf("wrong result\ngot: %#q\nwant to contain: %#q", got, want)
299+
}
300+
}
301+
302+
func TestApplyJSON_resourceCountErrored(t *testing.T) {
303+
streams, done := terminal.StreamsForTesting(t)
304+
v := NewApply(arguments.ViewJSON, false, NewView(streams))
305+
hooks := v.Hooks()
306+
307+
var count *countHook
308+
for _, hook := range hooks {
309+
if ch, ok := hook.(*countHook); ok {
310+
count = ch
311+
}
312+
}
313+
if count == nil {
314+
t.Fatalf("expected Hooks to include a countHook: %#v", hooks)
315+
}
316+
317+
count.Added = 1
318+
319+
v.ResourceCount("", true)
320+
321+
got := done(t).Stdout()
322+
if !strings.Contains(got, `"@message":"Apply incomplete with errors! Resources: 1 added, 0 changed, 0 destroyed."`) {
323+
t.Fatalf("wrong result\ngot: %q", got)
324+
}
325+
if !strings.Contains(got, `"errored":true`) {
326+
t.Fatalf("expected errored field in json output\ngot: %q", got)
327+
}
328+
}

internal/command/views/json/change_summary.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type ChangeSummary struct {
2323
Remove int `json:"remove"`
2424
ActionInvocation int `json:"action_invocation"`
2525
Operation Operation `json:"operation"`
26+
Errored bool `json:"errored,omitempty"`
2627
}
2728

2829
// The summary strings for apply and plan are accidentally a public interface
@@ -32,7 +33,11 @@ func (cs *ChangeSummary) String() string {
3233
var buf strings.Builder
3334
switch cs.Operation {
3435
case OperationApplied:
35-
buf.WriteString("Apply complete! Resources: ")
36+
if cs.Errored {
37+
buf.WriteString("Apply incomplete with errors! Resources: ")
38+
} else {
39+
buf.WriteString("Apply complete! Resources: ")
40+
}
3641
if cs.Import > 0 {
3742
buf.WriteString(fmt.Sprintf("%d imported, ", cs.Import))
3843
}

0 commit comments

Comments
 (0)