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