1package views
2
3import (
4	"fmt"
5	"testing"
6	"time"
7
8	"github.com/hashicorp/terraform/internal/addrs"
9	"github.com/hashicorp/terraform/internal/plans"
10	"github.com/hashicorp/terraform/internal/states"
11	"github.com/hashicorp/terraform/internal/terminal"
12	"github.com/hashicorp/terraform/internal/terraform"
13	"github.com/zclconf/go-cty/cty"
14)
15
16// Test a sequence of hooks associated with creating a resource
17func TestJSONHook_create(t *testing.T) {
18	streams, done := terminal.StreamsForTesting(t)
19	hook := newJSONHook(NewJSONView(NewView(streams)))
20
21	now := time.Now()
22	hook.timeNow = func() time.Time { return now }
23	after := make(chan time.Time, 1)
24	hook.timeAfter = func(time.Duration) <-chan time.Time { return after }
25
26	addr := addrs.Resource{
27		Mode: addrs.ManagedResourceMode,
28		Type: "test_instance",
29		Name: "boop",
30	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
31	priorState := cty.NullVal(cty.Object(map[string]cty.Type{
32		"id":  cty.String,
33		"bar": cty.List(cty.String),
34	}))
35	plannedNewState := cty.ObjectVal(map[string]cty.Value{
36		"id": cty.StringVal("test"),
37		"bar": cty.ListVal([]cty.Value{
38			cty.StringVal("baz"),
39		}),
40	})
41
42	action, err := hook.PreApply(addr, states.CurrentGen, plans.Create, priorState, plannedNewState)
43	testHookReturnValues(t, action, err)
44
45	action, err = hook.PreProvisionInstanceStep(addr, "local-exec")
46	testHookReturnValues(t, action, err)
47
48	hook.ProvisionOutput(addr, "local-exec", `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`)
49
50	action, err = hook.PostProvisionInstanceStep(addr, "local-exec", nil)
51	testHookReturnValues(t, action, err)
52
53	// Travel 10s into the future, notify the progress goroutine, and sleep
54	// briefly to allow it to execute
55	now = now.Add(10 * time.Second)
56	after <- now
57	time.Sleep(1 * time.Millisecond)
58
59	// Travel 10s into the future, notify the progress goroutine, and sleep
60	// briefly to allow it to execute
61	now = now.Add(10 * time.Second)
62	after <- now
63	time.Sleep(1 * time.Millisecond)
64
65	// Travel 2s into the future. We have arrived!
66	now = now.Add(2 * time.Second)
67
68	action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, nil)
69	testHookReturnValues(t, action, err)
70
71	// Shut down the progress goroutine if still active
72	hook.applyingLock.Lock()
73	for key, progress := range hook.applying {
74		close(progress.done)
75		<-progress.heartbeatDone
76		delete(hook.applying, key)
77	}
78	hook.applyingLock.Unlock()
79
80	wantResource := map[string]interface{}{
81		"addr":             string("test_instance.boop"),
82		"implied_provider": string("test"),
83		"module":           string(""),
84		"resource":         string("test_instance.boop"),
85		"resource_key":     nil,
86		"resource_name":    string("boop"),
87		"resource_type":    string("test_instance"),
88	}
89	want := []map[string]interface{}{
90		{
91			"@level":   "info",
92			"@message": "test_instance.boop: Creating...",
93			"@module":  "terraform.ui",
94			"type":     "apply_start",
95			"hook": map[string]interface{}{
96				"action":   string("create"),
97				"resource": wantResource,
98			},
99		},
100		{
101			"@level":   "info",
102			"@message": "test_instance.boop: Provisioning with 'local-exec'...",
103			"@module":  "terraform.ui",
104			"type":     "provision_start",
105			"hook": map[string]interface{}{
106				"provisioner": "local-exec",
107				"resource":    wantResource,
108			},
109		},
110		{
111			"@level":   "info",
112			"@message": `test_instance.boop: (local-exec): Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
113			"@module":  "terraform.ui",
114			"type":     "provision_progress",
115			"hook": map[string]interface{}{
116				"output":      `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
117				"provisioner": "local-exec",
118				"resource":    wantResource,
119			},
120		},
121		{
122			"@level":   "info",
123			"@message": "test_instance.boop: (local-exec) Provisioning complete",
124			"@module":  "terraform.ui",
125			"type":     "provision_complete",
126			"hook": map[string]interface{}{
127				"provisioner": "local-exec",
128				"resource":    wantResource,
129			},
130		},
131		{
132			"@level":   "info",
133			"@message": "test_instance.boop: Still creating... [10s elapsed]",
134			"@module":  "terraform.ui",
135			"type":     "apply_progress",
136			"hook": map[string]interface{}{
137				"action":          string("create"),
138				"elapsed_seconds": float64(10),
139				"resource":        wantResource,
140			},
141		},
142		{
143			"@level":   "info",
144			"@message": "test_instance.boop: Still creating... [20s elapsed]",
145			"@module":  "terraform.ui",
146			"type":     "apply_progress",
147			"hook": map[string]interface{}{
148				"action":          string("create"),
149				"elapsed_seconds": float64(20),
150				"resource":        wantResource,
151			},
152		},
153		{
154			"@level":   "info",
155			"@message": "test_instance.boop: Creation complete after 22s [id=test]",
156			"@module":  "terraform.ui",
157			"type":     "apply_complete",
158			"hook": map[string]interface{}{
159				"action":          string("create"),
160				"elapsed_seconds": float64(22),
161				"id_key":          "id",
162				"id_value":        "test",
163				"resource":        wantResource,
164			},
165		},
166	}
167
168	testJSONViewOutputEquals(t, done(t).Stdout(), want)
169}
170
171func TestJSONHook_errors(t *testing.T) {
172	streams, done := terminal.StreamsForTesting(t)
173	hook := newJSONHook(NewJSONView(NewView(streams)))
174
175	addr := addrs.Resource{
176		Mode: addrs.ManagedResourceMode,
177		Type: "test_instance",
178		Name: "boop",
179	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
180	priorState := cty.NullVal(cty.Object(map[string]cty.Type{
181		"id":  cty.String,
182		"bar": cty.List(cty.String),
183	}))
184	plannedNewState := cty.ObjectVal(map[string]cty.Value{
185		"id": cty.StringVal("test"),
186		"bar": cty.ListVal([]cty.Value{
187			cty.StringVal("baz"),
188		}),
189	})
190
191	action, err := hook.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState)
192	testHookReturnValues(t, action, err)
193
194	provisionError := fmt.Errorf("provisioner didn't want to")
195	action, err = hook.PostProvisionInstanceStep(addr, "local-exec", provisionError)
196	testHookReturnValues(t, action, err)
197
198	applyError := fmt.Errorf("provider was sad")
199	action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, applyError)
200	testHookReturnValues(t, action, err)
201
202	// Shut down the progress goroutine
203	hook.applyingLock.Lock()
204	for key, progress := range hook.applying {
205		close(progress.done)
206		<-progress.heartbeatDone
207		delete(hook.applying, key)
208	}
209	hook.applyingLock.Unlock()
210
211	wantResource := map[string]interface{}{
212		"addr":             string("test_instance.boop"),
213		"implied_provider": string("test"),
214		"module":           string(""),
215		"resource":         string("test_instance.boop"),
216		"resource_key":     nil,
217		"resource_name":    string("boop"),
218		"resource_type":    string("test_instance"),
219	}
220	want := []map[string]interface{}{
221		{
222			"@level":   "info",
223			"@message": "test_instance.boop: Destroying...",
224			"@module":  "terraform.ui",
225			"type":     "apply_start",
226			"hook": map[string]interface{}{
227				"action":   string("delete"),
228				"resource": wantResource,
229			},
230		},
231		{
232			"@level":   "info",
233			"@message": "test_instance.boop: (local-exec) Provisioning errored",
234			"@module":  "terraform.ui",
235			"type":     "provision_errored",
236			"hook": map[string]interface{}{
237				"provisioner": "local-exec",
238				"resource":    wantResource,
239			},
240		},
241		{
242			"@level":   "info",
243			"@message": "test_instance.boop: Destruction errored after 0s",
244			"@module":  "terraform.ui",
245			"type":     "apply_errored",
246			"hook": map[string]interface{}{
247				"action":          string("delete"),
248				"elapsed_seconds": float64(0),
249				"resource":        wantResource,
250			},
251		},
252	}
253
254	testJSONViewOutputEquals(t, done(t).Stdout(), want)
255}
256
257func TestJSONHook_refresh(t *testing.T) {
258	streams, done := terminal.StreamsForTesting(t)
259	hook := newJSONHook(NewJSONView(NewView(streams)))
260
261	addr := addrs.Resource{
262		Mode: addrs.DataResourceMode,
263		Type: "test_data_source",
264		Name: "beep",
265	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
266	state := cty.ObjectVal(map[string]cty.Value{
267		"id": cty.StringVal("honk"),
268		"bar": cty.ListVal([]cty.Value{
269			cty.StringVal("baz"),
270		}),
271	})
272
273	action, err := hook.PreRefresh(addr, states.CurrentGen, state)
274	testHookReturnValues(t, action, err)
275
276	action, err = hook.PostRefresh(addr, states.CurrentGen, state, state)
277	testHookReturnValues(t, action, err)
278
279	wantResource := map[string]interface{}{
280		"addr":             string("data.test_data_source.beep"),
281		"implied_provider": string("test"),
282		"module":           string(""),
283		"resource":         string("data.test_data_source.beep"),
284		"resource_key":     nil,
285		"resource_name":    string("beep"),
286		"resource_type":    string("test_data_source"),
287	}
288	want := []map[string]interface{}{
289		{
290			"@level":   "info",
291			"@message": "data.test_data_source.beep: Refreshing state... [id=honk]",
292			"@module":  "terraform.ui",
293			"type":     "refresh_start",
294			"hook": map[string]interface{}{
295				"resource": wantResource,
296				"id_key":   "id",
297				"id_value": "honk",
298			},
299		},
300		{
301			"@level":   "info",
302			"@message": "data.test_data_source.beep: Refresh complete [id=honk]",
303			"@module":  "terraform.ui",
304			"type":     "refresh_complete",
305			"hook": map[string]interface{}{
306				"resource": wantResource,
307				"id_key":   "id",
308				"id_value": "honk",
309			},
310		},
311	}
312
313	testJSONViewOutputEquals(t, done(t).Stdout(), want)
314}
315
316func testHookReturnValues(t *testing.T, action terraform.HookAction, err error) {
317	t.Helper()
318
319	if err != nil {
320		t.Fatal(err)
321	}
322	if action != terraform.HookActionContinue {
323		t.Fatalf("Expected hook to continue, given: %#v", action)
324	}
325}
326