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