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