github.com/kevinklinger/open_terraform@v1.3.6/noninternal/backend/testing.go (about) 1 package backend 2 3 import ( 4 "reflect" 5 "sort" 6 "testing" 7 8 uuid "github.com/hashicorp/go-uuid" 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/hcldec" 11 12 "github.com/kevinklinger/open_terraform/noninternal/addrs" 13 "github.com/kevinklinger/open_terraform/noninternal/configs" 14 "github.com/kevinklinger/open_terraform/noninternal/configs/hcl2shim" 15 "github.com/kevinklinger/open_terraform/noninternal/states" 16 "github.com/kevinklinger/open_terraform/noninternal/states/statemgr" 17 "github.com/kevinklinger/open_terraform/noninternal/tfdiags" 18 ) 19 20 // TestBackendConfig validates and configures the backend with the 21 // given configuration. 22 func TestBackendConfig(t *testing.T, b Backend, c hcl.Body) Backend { 23 t.Helper() 24 25 t.Logf("TestBackendConfig on %T with %#v", b, c) 26 27 var diags tfdiags.Diagnostics 28 29 // To make things easier for test authors, we'll allow a nil body here 30 // (even though that's not normally valid) and just treat it as an empty 31 // body. 32 if c == nil { 33 c = hcl.EmptyBody() 34 } 35 36 schema := b.ConfigSchema() 37 spec := schema.DecoderSpec() 38 obj, decDiags := hcldec.Decode(c, spec, nil) 39 diags = diags.Append(decDiags) 40 41 newObj, valDiags := b.PrepareConfig(obj) 42 diags = diags.Append(valDiags.InConfigBody(c, "")) 43 44 // it's valid for a Backend to have warnings (e.g. a Deprecation) as such we should only raise on errors 45 if diags.HasErrors() { 46 t.Fatal(diags.ErrWithWarnings()) 47 } 48 49 obj = newObj 50 51 confDiags := b.Configure(obj) 52 if len(confDiags) != 0 { 53 confDiags = confDiags.InConfigBody(c, "") 54 t.Fatal(confDiags.ErrWithWarnings()) 55 } 56 57 return b 58 } 59 60 // TestWrapConfig takes a raw data structure and converts it into a 61 // synthetic hcl.Body to use for testing. 62 // 63 // The given structure should only include values that can be accepted by 64 // hcl2shim.HCL2ValueFromConfigValue. If incompatible values are given, 65 // this function will panic. 66 func TestWrapConfig(raw map[string]interface{}) hcl.Body { 67 obj := hcl2shim.HCL2ValueFromConfigValue(raw) 68 return configs.SynthBody("<TestWrapConfig>", obj.AsValueMap()) 69 } 70 71 // TestBackend will test the functionality of a Backend. The backend is 72 // assumed to already be configured. This will test state functionality. 73 // If the backend reports it doesn't support multi-state by returning the 74 // error ErrWorkspacesNotSupported, then it will not test that. 75 func TestBackendStates(t *testing.T, b Backend) { 76 t.Helper() 77 78 noDefault := false 79 if _, err := b.StateMgr(DefaultStateName); err != nil { 80 if err == ErrDefaultWorkspaceNotSupported { 81 noDefault = true 82 } else { 83 t.Fatalf("error: %v", err) 84 } 85 } 86 87 workspaces, err := b.Workspaces() 88 if err != nil { 89 if err == ErrWorkspacesNotSupported { 90 t.Logf("TestBackend: workspaces not supported in %T, skipping", b) 91 return 92 } 93 t.Fatalf("error: %v", err) 94 } 95 96 // Test it starts with only the default 97 if !noDefault && (len(workspaces) != 1 || workspaces[0] != DefaultStateName) { 98 t.Fatalf("should only have the default workspace to start: %#v", workspaces) 99 } 100 101 // Create a couple states 102 foo, err := b.StateMgr("foo") 103 if err != nil { 104 t.Fatalf("error: %s", err) 105 } 106 if err := foo.RefreshState(); err != nil { 107 t.Fatalf("bad: %s", err) 108 } 109 if v := foo.State(); v.HasManagedResourceInstanceObjects() { 110 t.Fatalf("should be empty: %s", v) 111 } 112 113 bar, err := b.StateMgr("bar") 114 if err != nil { 115 t.Fatalf("error: %s", err) 116 } 117 if err := bar.RefreshState(); err != nil { 118 t.Fatalf("bad: %s", err) 119 } 120 if v := bar.State(); v.HasManagedResourceInstanceObjects() { 121 t.Fatalf("should be empty: %s", v) 122 } 123 124 // Verify they are distinct states that can be read back from storage 125 { 126 // We'll use two distinct states here and verify that changing one 127 // does not also change the other. 128 fooState := states.NewState() 129 barState := states.NewState() 130 131 // write a known state to foo 132 if err := foo.WriteState(fooState); err != nil { 133 t.Fatal("error writing foo state:", err) 134 } 135 if err := foo.PersistState(nil); err != nil { 136 t.Fatal("error persisting foo state:", err) 137 } 138 139 // We'll make "bar" different by adding a fake resource state to it. 140 barState.SyncWrapper().SetResourceInstanceCurrent( 141 addrs.ResourceInstance{ 142 Resource: addrs.Resource{ 143 Mode: addrs.ManagedResourceMode, 144 Type: "test_thing", 145 Name: "foo", 146 }, 147 }.Absolute(addrs.RootModuleInstance), 148 &states.ResourceInstanceObjectSrc{ 149 AttrsJSON: []byte("{}"), 150 Status: states.ObjectReady, 151 SchemaVersion: 0, 152 }, 153 addrs.AbsProviderConfig{ 154 Provider: addrs.NewDefaultProvider("test"), 155 Module: addrs.RootModule, 156 }, 157 ) 158 159 // write a distinct known state to bar 160 if err := bar.WriteState(barState); err != nil { 161 t.Fatalf("bad: %s", err) 162 } 163 if err := bar.PersistState(nil); err != nil { 164 t.Fatalf("bad: %s", err) 165 } 166 167 // verify that foo is unchanged with the existing state manager 168 if err := foo.RefreshState(); err != nil { 169 t.Fatal("error refreshing foo:", err) 170 } 171 fooState = foo.State() 172 if fooState.HasManagedResourceInstanceObjects() { 173 t.Fatal("after writing a resource to bar, foo now has resources too") 174 } 175 176 // fetch foo again from the backend 177 foo, err = b.StateMgr("foo") 178 if err != nil { 179 t.Fatal("error re-fetching state:", err) 180 } 181 if err := foo.RefreshState(); err != nil { 182 t.Fatal("error refreshing foo:", err) 183 } 184 fooState = foo.State() 185 if fooState.HasManagedResourceInstanceObjects() { 186 t.Fatal("after writing a resource to bar and re-reading foo, foo now has resources too") 187 } 188 189 // fetch the bar again from the backend 190 bar, err = b.StateMgr("bar") 191 if err != nil { 192 t.Fatal("error re-fetching state:", err) 193 } 194 if err := bar.RefreshState(); err != nil { 195 t.Fatal("error refreshing bar:", err) 196 } 197 barState = bar.State() 198 if !barState.HasManagedResourceInstanceObjects() { 199 t.Fatal("after writing a resource instance object to bar and re-reading it, the object has vanished") 200 } 201 } 202 203 // Verify we can now list them 204 { 205 // we determined that named stated are supported earlier 206 workspaces, err := b.Workspaces() 207 if err != nil { 208 t.Fatalf("err: %s", err) 209 } 210 211 sort.Strings(workspaces) 212 expected := []string{"bar", "default", "foo"} 213 if noDefault { 214 expected = []string{"bar", "foo"} 215 } 216 if !reflect.DeepEqual(workspaces, expected) { 217 t.Fatalf("wrong workspaces list\ngot: %#v\nwant: %#v", workspaces, expected) 218 } 219 } 220 221 // Delete some workspaces 222 if err := b.DeleteWorkspace("foo"); err != nil { 223 t.Fatalf("err: %s", err) 224 } 225 226 // Verify the default state can't be deleted 227 if err := b.DeleteWorkspace(DefaultStateName); err == nil { 228 t.Fatal("expected error") 229 } 230 231 // Create and delete the foo workspace again. 232 // Make sure that there are no leftover artifacts from a deleted state 233 // preventing re-creation. 234 foo, err = b.StateMgr("foo") 235 if err != nil { 236 t.Fatalf("error: %s", err) 237 } 238 if err := foo.RefreshState(); err != nil { 239 t.Fatalf("bad: %s", err) 240 } 241 if v := foo.State(); v.HasManagedResourceInstanceObjects() { 242 t.Fatalf("should be empty: %s", v) 243 } 244 // and delete it again 245 if err := b.DeleteWorkspace("foo"); err != nil { 246 t.Fatalf("err: %s", err) 247 } 248 249 // Verify deletion 250 { 251 workspaces, err := b.Workspaces() 252 if err != nil { 253 t.Fatalf("err: %s", err) 254 } 255 256 sort.Strings(workspaces) 257 expected := []string{"bar", "default"} 258 if noDefault { 259 expected = []string{"bar"} 260 } 261 if !reflect.DeepEqual(workspaces, expected) { 262 t.Fatalf("wrong workspaces list\ngot: %#v\nwant: %#v", workspaces, expected) 263 } 264 } 265 } 266 267 // TestBackendStateLocks will test the locking functionality of the remote 268 // state backend. 269 func TestBackendStateLocks(t *testing.T, b1, b2 Backend) { 270 t.Helper() 271 testLocks(t, b1, b2, false) 272 } 273 274 // TestBackendStateForceUnlock verifies that the lock error is the expected 275 // type, and the lock can be unlocked using the ID reported in the error. 276 // Remote state backends that support -force-unlock should call this in at 277 // least one of the acceptance tests. 278 func TestBackendStateForceUnlock(t *testing.T, b1, b2 Backend) { 279 t.Helper() 280 testLocks(t, b1, b2, true) 281 } 282 283 // TestBackendStateLocksInWS will test the locking functionality of the remote 284 // state backend. 285 func TestBackendStateLocksInWS(t *testing.T, b1, b2 Backend, ws string) { 286 t.Helper() 287 testLocksInWorkspace(t, b1, b2, false, ws) 288 } 289 290 // TestBackendStateForceUnlockInWS verifies that the lock error is the expected 291 // type, and the lock can be unlocked using the ID reported in the error. 292 // Remote state backends that support -force-unlock should call this in at 293 // least one of the acceptance tests. 294 func TestBackendStateForceUnlockInWS(t *testing.T, b1, b2 Backend, ws string) { 295 t.Helper() 296 testLocksInWorkspace(t, b1, b2, true, ws) 297 } 298 299 func testLocks(t *testing.T, b1, b2 Backend, testForceUnlock bool) { 300 testLocksInWorkspace(t, b1, b2, testForceUnlock, DefaultStateName) 301 } 302 303 func testLocksInWorkspace(t *testing.T, b1, b2 Backend, testForceUnlock bool, workspace string) { 304 t.Helper() 305 306 // Get the default state for each 307 b1StateMgr, err := b1.StateMgr(DefaultStateName) 308 if err != nil { 309 t.Fatalf("error: %s", err) 310 } 311 if err := b1StateMgr.RefreshState(); err != nil { 312 t.Fatalf("bad: %s", err) 313 } 314 315 // Fast exit if this doesn't support locking at all 316 if _, ok := b1StateMgr.(statemgr.Locker); !ok { 317 t.Logf("TestBackend: backend %T doesn't support state locking, not testing", b1) 318 return 319 } 320 321 t.Logf("TestBackend: testing state locking for %T", b1) 322 323 b2StateMgr, err := b2.StateMgr(DefaultStateName) 324 if err != nil { 325 t.Fatalf("error: %s", err) 326 } 327 if err := b2StateMgr.RefreshState(); err != nil { 328 t.Fatalf("bad: %s", err) 329 } 330 331 // Reassign so its obvious whats happening 332 lockerA := b1StateMgr.(statemgr.Locker) 333 lockerB := b2StateMgr.(statemgr.Locker) 334 335 infoA := statemgr.NewLockInfo() 336 infoA.Operation = "test" 337 infoA.Who = "clientA" 338 339 infoB := statemgr.NewLockInfo() 340 infoB.Operation = "test" 341 infoB.Who = "clientB" 342 343 lockIDA, err := lockerA.Lock(infoA) 344 if err != nil { 345 t.Fatal("unable to get initial lock:", err) 346 } 347 348 // Make sure we can still get the statemgr.Full from another instance even 349 // when locked. This should only happen when a state is loaded via the 350 // backend, and as a remote state. 351 _, err = b2.StateMgr(DefaultStateName) 352 if err != nil { 353 t.Errorf("failed to read locked state from another backend instance: %s", err) 354 } 355 356 // If the lock ID is blank, assume locking is disabled 357 if lockIDA == "" { 358 t.Logf("TestBackend: %T: empty string returned for lock, assuming disabled", b1) 359 return 360 } 361 362 _, err = lockerB.Lock(infoB) 363 if err == nil { 364 lockerA.Unlock(lockIDA) 365 t.Fatal("client B obtained lock while held by client A") 366 } 367 368 if err := lockerA.Unlock(lockIDA); err != nil { 369 t.Fatal("error unlocking client A", err) 370 } 371 372 lockIDB, err := lockerB.Lock(infoB) 373 if err != nil { 374 t.Fatal("unable to obtain lock from client B") 375 } 376 377 if lockIDB == lockIDA { 378 t.Errorf("duplicate lock IDs: %q", lockIDB) 379 } 380 381 if err = lockerB.Unlock(lockIDB); err != nil { 382 t.Fatal("error unlocking client B:", err) 383 } 384 385 // test the equivalent of -force-unlock, by using the id from the error 386 // output. 387 if !testForceUnlock { 388 return 389 } 390 391 // get a new ID 392 infoA.ID, err = uuid.GenerateUUID() 393 if err != nil { 394 panic(err) 395 } 396 397 lockIDA, err = lockerA.Lock(infoA) 398 if err != nil { 399 t.Fatal("unable to get re lock A:", err) 400 } 401 unlock := func() { 402 err := lockerA.Unlock(lockIDA) 403 if err != nil { 404 t.Fatal(err) 405 } 406 } 407 408 _, err = lockerB.Lock(infoB) 409 if err == nil { 410 unlock() 411 t.Fatal("client B obtained lock while held by client A") 412 } 413 414 infoErr, ok := err.(*statemgr.LockError) 415 if !ok { 416 unlock() 417 t.Fatalf("expected type *statemgr.LockError, got : %#v", err) 418 } 419 420 // try to unlock with the second unlocker, using the ID from the error 421 if err := lockerB.Unlock(infoErr.Info.ID); err != nil { 422 unlock() 423 t.Fatalf("could not unlock with the reported ID %q: %s", infoErr.Info.ID, err) 424 } 425 }