Skip to content

Commit 48148ed

Browse files
authored
Merge pull request #34687 from hashicorp/alisdair/stackeval-hook-replace-action-counts
stackeval: Add support for replace actions to hook
2 parents 7df7998 + 06723d3 commit 48148ed

File tree

2 files changed

+285
-15
lines changed

2 files changed

+285
-15
lines changed

internal/stacks/stackruntime/internal/stackeval/terraform_hook.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,20 @@ type componentInstanceTerraformHook struct {
3131
hooks *Hooks
3232
addr stackaddrs.AbsComponentInstance
3333

34-
mu sync.Mutex
35-
resourceInstanceObjectApplyAction addrs.Map[addrs.AbsResourceInstanceObject, plans.Action]
34+
mu sync.Mutex
35+
36+
// We record the current action for a resource instance during the
37+
// pre-apply hook, so that we can refer to it in the post-apply hook, and
38+
// report on the apply action to our caller.
39+
resourceInstanceObjectApplyAction addrs.Map[addrs.AbsResourceInstanceObject, plans.Action]
40+
41+
// Only successfully applied resource instances should be included in the
42+
// change counts for the apply operation, so we record whether or not apply
43+
// failed here.
3644
resourceInstanceObjectApplySuccess addrs.Set[addrs.AbsResourceInstanceObject]
3745
}
3846

39-
func (h *componentInstanceTerraformHook) resourceInstanceAddr(addr addrs.AbsResourceInstance) stackaddrs.AbsResourceInstance {
40-
return stackaddrs.AbsResourceInstance{
41-
Component: h.addr,
42-
Item: addr,
43-
}
44-
}
47+
var _ terraform.Hook = (*componentInstanceTerraformHook)(nil)
4548

4649
func (h *componentInstanceTerraformHook) resourceInstanceObjectAddr(riAddr addrs.AbsResourceInstance, dk addrs.DeposedKey) stackaddrs.AbsResourceInstanceObject {
4750
return stackaddrs.AbsResourceInstanceObject{
@@ -81,13 +84,23 @@ func (h *componentInstanceTerraformHook) PreApply(addr addrs.AbsResourceInstance
8184
if h.resourceInstanceObjectApplyAction.Len() == 0 {
8285
h.resourceInstanceObjectApplyAction = addrs.MakeMap[addrs.AbsResourceInstanceObject, plans.Action]()
8386
}
84-
h.resourceInstanceObjectApplyAction.Put(
85-
addrs.AbsResourceInstanceObject{
86-
ResourceInstance: addr,
87-
DeposedKey: dk,
88-
},
89-
action,
90-
)
87+
localObjAddr := addrs.AbsResourceInstanceObject{
88+
ResourceInstance: addr,
89+
DeposedKey: dk,
90+
}
91+
92+
// We may have stored a previous action for this resource instance if it is
93+
// planned as create-then-destroy or destroy-then-create. For those two
94+
// cases we need to synthesize the compound action so that it is reported
95+
// correctly at the end of the apply process.
96+
if prevAction, ok := h.resourceInstanceObjectApplyAction.GetOk(localObjAddr); ok {
97+
if prevAction == plans.Delete && action == plans.Create {
98+
action = plans.DeleteThenCreate
99+
} else if prevAction == plans.Create && action == plans.Delete {
100+
action = plans.CreateThenDelete
101+
}
102+
}
103+
h.resourceInstanceObjectApplyAction.Put(localObjAddr, action)
91104
h.mu.Unlock()
92105

93106
return terraform.HookActionContinue, nil
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package stackeval
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"testing"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/hashicorp/terraform/internal/addrs"
14+
"github.com/hashicorp/terraform/internal/plans"
15+
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
16+
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
17+
"github.com/hashicorp/terraform/internal/terraform"
18+
"github.com/zclconf/go-cty/cty"
19+
)
20+
21+
func TestTerraformHook(t *testing.T) {
22+
var gotRihd *hooks.ResourceInstanceStatusHookData
23+
testHooks := &Hooks{
24+
ReportResourceInstanceStatus: func(ctx context.Context, span any, rihd *hooks.ResourceInstanceStatusHookData) any {
25+
gotRihd = rihd
26+
return span
27+
},
28+
}
29+
componentAddr := stackaddrs.AbsComponentInstance{
30+
Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")),
31+
Item: stackaddrs.ComponentInstance{
32+
Component: stackaddrs.Component{Name: "foo"},
33+
Key: addrs.StringKey("beep"),
34+
},
35+
}
36+
37+
makeHook := func() *componentInstanceTerraformHook {
38+
return &componentInstanceTerraformHook{
39+
ctx: context.Background(),
40+
seq: &hookSeq{
41+
tracking: "boop",
42+
},
43+
hooks: testHooks,
44+
addr: componentAddr,
45+
}
46+
}
47+
48+
resourceAddr := addrs.AbsResourceInstance{
49+
Module: addrs.RootModuleInstance,
50+
Resource: addrs.ResourceInstance{
51+
Resource: addrs.Resource{
52+
Mode: addrs.ManagedResourceMode,
53+
Type: "foo",
54+
Name: "bar",
55+
},
56+
Key: addrs.NoKey,
57+
},
58+
}
59+
stackAddr := stackaddrs.AbsResourceInstanceObject{
60+
Component: componentAddr,
61+
Item: resourceAddr.CurrentObject(),
62+
}
63+
64+
t.Run("PreDiff", func(t *testing.T) {
65+
hook := makeHook()
66+
action, err := hook.PreDiff(resourceAddr, addrs.NotDeposed, cty.NilVal, cty.NilVal)
67+
if err != nil {
68+
t.Errorf("unexpected error: %s", err)
69+
}
70+
if action != terraform.HookActionContinue {
71+
t.Errorf("wrong action: %#v", action)
72+
}
73+
if hook.seq.tracking != "boop" {
74+
t.Errorf("wrong tracking value: %#v", hook.seq.tracking)
75+
}
76+
77+
wantRihd := &hooks.ResourceInstanceStatusHookData{
78+
Addr: stackAddr,
79+
Status: hooks.ResourceInstancePlanning,
80+
}
81+
if diff := cmp.Diff(gotRihd, wantRihd); diff != "" {
82+
t.Errorf("wrong status hook data:\n%s", diff)
83+
}
84+
})
85+
86+
t.Run("PostDiff", func(t *testing.T) {
87+
hook := makeHook()
88+
action, err := hook.PostDiff(resourceAddr, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal)
89+
if err != nil {
90+
t.Errorf("unexpected error: %s", err)
91+
}
92+
if action != terraform.HookActionContinue {
93+
t.Errorf("wrong action: %#v", action)
94+
}
95+
if hook.seq.tracking != "boop" {
96+
t.Errorf("wrong tracking value: %#v", hook.seq.tracking)
97+
}
98+
99+
wantRihd := &hooks.ResourceInstanceStatusHookData{
100+
Addr: stackAddr,
101+
Status: hooks.ResourceInstancePlanned,
102+
}
103+
if diff := cmp.Diff(gotRihd, wantRihd); diff != "" {
104+
t.Errorf("wrong status hook data:\n%s", diff)
105+
}
106+
})
107+
108+
t.Run("PreApply", func(t *testing.T) {
109+
hook := makeHook()
110+
action, err := hook.PreApply(resourceAddr, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal)
111+
if err != nil {
112+
t.Errorf("unexpected error: %s", err)
113+
}
114+
if action != terraform.HookActionContinue {
115+
t.Errorf("wrong action: %#v", action)
116+
}
117+
if hook.seq.tracking != "boop" {
118+
t.Errorf("wrong tracking value: %#v", hook.seq.tracking)
119+
}
120+
121+
wantRihd := &hooks.ResourceInstanceStatusHookData{
122+
Addr: stackAddr,
123+
Status: hooks.ResourceInstanceApplying,
124+
}
125+
if diff := cmp.Diff(gotRihd, wantRihd); diff != "" {
126+
t.Errorf("wrong status hook data:\n%s", diff)
127+
}
128+
})
129+
130+
t.Run("PostApply", func(t *testing.T) {
131+
hook := makeHook()
132+
// It is invalid to call PostApply without first calling PreApply
133+
action, err := hook.PreApply(resourceAddr, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal)
134+
if err != nil {
135+
t.Errorf("unexpected error: %s", err)
136+
}
137+
if action != terraform.HookActionContinue {
138+
t.Errorf("wrong action: %#v", action)
139+
}
140+
141+
action, err = hook.PostApply(resourceAddr, addrs.NotDeposed, cty.NilVal, nil)
142+
if err != nil {
143+
t.Errorf("unexpected error: %s", err)
144+
}
145+
if action != terraform.HookActionContinue {
146+
t.Errorf("wrong action: %#v", action)
147+
}
148+
if hook.seq.tracking != "boop" {
149+
t.Errorf("wrong tracking value: %#v", hook.seq.tracking)
150+
}
151+
152+
wantRihd := &hooks.ResourceInstanceStatusHookData{
153+
Addr: stackAddr,
154+
Status: hooks.ResourceInstanceApplied,
155+
}
156+
if diff := cmp.Diff(gotRihd, wantRihd); diff != "" {
157+
t.Errorf("wrong status hook data:\n%s", diff)
158+
}
159+
})
160+
161+
t.Run("PostApply errored", func(t *testing.T) {
162+
hook := makeHook()
163+
// It is invalid to call PostApply without first calling PreApply
164+
action, err := hook.PreApply(resourceAddr, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal)
165+
if err != nil {
166+
t.Errorf("unexpected error: %s", err)
167+
}
168+
if action != terraform.HookActionContinue {
169+
t.Errorf("wrong action: %#v", action)
170+
}
171+
172+
action, err = hook.PostApply(resourceAddr, addrs.NotDeposed, cty.NilVal, errors.New("splines unreticulatable"))
173+
if err != nil {
174+
t.Errorf("unexpected error: %s", err)
175+
}
176+
if action != terraform.HookActionContinue {
177+
t.Errorf("wrong action: %#v", action)
178+
}
179+
if hook.seq.tracking != "boop" {
180+
t.Errorf("wrong tracking value: %#v", hook.seq.tracking)
181+
}
182+
183+
wantRihd := &hooks.ResourceInstanceStatusHookData{
184+
Addr: stackAddr,
185+
Status: hooks.ResourceInstanceErrored,
186+
}
187+
if diff := cmp.Diff(gotRihd, wantRihd); diff != "" {
188+
t.Errorf("wrong status hook data:\n%s", diff)
189+
}
190+
})
191+
192+
t.Run("ResourceInstanceObjectAppliedAction", func(t *testing.T) {
193+
testCases := []struct {
194+
actions []plans.Action
195+
want plans.Action
196+
}{
197+
{
198+
actions: []plans.Action{plans.NoOp},
199+
want: plans.NoOp,
200+
},
201+
{
202+
actions: []plans.Action{plans.Create},
203+
want: plans.Create,
204+
},
205+
{
206+
actions: []plans.Action{plans.Delete},
207+
want: plans.Delete,
208+
},
209+
{
210+
actions: []plans.Action{plans.Update},
211+
want: plans.Update,
212+
},
213+
{
214+
// We return a fallback of no-op if the object has no recorded
215+
// applied action.
216+
actions: []plans.Action{},
217+
want: plans.NoOp,
218+
},
219+
{
220+
// Create-then-delete plans result in two separate apply
221+
// operations, which we need to recombine into a single one in
222+
// order to correctly count the operations.
223+
actions: []plans.Action{plans.Create, plans.Delete},
224+
want: plans.CreateThenDelete,
225+
},
226+
{
227+
// See above: same for delete-then-create.
228+
actions: []plans.Action{plans.Delete, plans.Create},
229+
want: plans.DeleteThenCreate,
230+
},
231+
}
232+
233+
for _, tc := range testCases {
234+
t.Run(fmt.Sprintf("%v", tc.actions), func(t *testing.T) {
235+
hook := makeHook()
236+
237+
for _, action := range tc.actions {
238+
_, err := hook.PreApply(resourceAddr, addrs.NotDeposed, action, cty.NilVal, cty.NilVal)
239+
if err != nil {
240+
t.Fatalf("unexpected error in PreApply: %s", err)
241+
}
242+
243+
_, err = hook.PostApply(resourceAddr, addrs.NotDeposed, cty.NilVal, nil)
244+
if err != nil {
245+
t.Fatalf("unexpected error in PostApply: %s", err)
246+
}
247+
}
248+
249+
got := hook.ResourceInstanceObjectAppliedAction(resourceAddr.CurrentObject())
250+
251+
if got != tc.want {
252+
t.Errorf("wrong result: got %v, want %v", got, tc.want)
253+
}
254+
})
255+
}
256+
})
257+
}

0 commit comments

Comments
 (0)