// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package backend import ( "reflect" "sort" "testing" uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/hcl2shim" "github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states/statemgr" "github.com/opentofu/opentofu/internal/tfdiags" ) // TestBackendConfig validates and configures the backend with the // given configuration. func TestBackendConfig(t *testing.T, b Backend, c hcl.Body) Backend { t.Helper() t.Logf("TestBackendConfig on %T with %#v", b, c) var diags tfdiags.Diagnostics // To make things easier for test authors, we'll allow a nil body here // (even though that's not normally valid) and just treat it as an empty // body. if c == nil { c = hcl.EmptyBody() } schema := b.ConfigSchema() spec := schema.DecoderSpec() obj, decDiags := hcldec.Decode(c, spec, nil) diags = diags.Append(decDiags) newObj, valDiags := b.PrepareConfig(obj) diags = diags.Append(valDiags.InConfigBody(c, "")) // it's valid for a Backend to have warnings (e.g. a Deprecation) as such we should only raise on errors if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } obj = newObj confDiags := b.Configure(obj) if len(confDiags) != 0 { confDiags = confDiags.InConfigBody(c, "") t.Fatal(confDiags.ErrWithWarnings()) } return b } // TestWrapConfig takes a raw data structure and converts it into a // synthetic hcl.Body to use for testing. // // The given structure should only include values that can be accepted by // hcl2shim.HCL2ValueFromConfigValue. If incompatible values are given, // this function will panic. func TestWrapConfig(raw map[string]interface{}) hcl.Body { obj := hcl2shim.HCL2ValueFromConfigValue(raw) return configs.SynthBody("", obj.AsValueMap()) } // TestBackend will test the functionality of a Backend. The backend is // assumed to already be configured. This will test state functionality. // If the backend reports it doesn't support multi-state by returning the // error ErrWorkspacesNotSupported, then it will not test that. func TestBackendStates(t *testing.T, b Backend) { t.Helper() noDefault := false if _, err := b.StateMgr(DefaultStateName); err != nil { if err == ErrDefaultWorkspaceNotSupported { noDefault = true } else { t.Fatalf("error: %v", err) } } workspaces, err := b.Workspaces() if err != nil { if err == ErrWorkspacesNotSupported { t.Logf("TestBackend: workspaces not supported in %T, skipping", b) return } t.Fatalf("error: %v", err) } // Test it starts with only the default if !noDefault && (len(workspaces) != 1 || workspaces[0] != DefaultStateName) { t.Fatalf("should only have the default workspace to start: %#v", workspaces) } // Create a couple states foo, err := b.StateMgr("foo") if err != nil { t.Fatalf("error: %s", err) } if err := foo.RefreshState(); err != nil { t.Fatalf("bad: %s", err) } if v := foo.State(); v.HasManagedResourceInstanceObjects() { t.Fatalf("should be empty: %s", v) } bar, err := b.StateMgr("bar") if err != nil { t.Fatalf("error: %s", err) } if err := bar.RefreshState(); err != nil { t.Fatalf("bad: %s", err) } if v := bar.State(); v.HasManagedResourceInstanceObjects() { t.Fatalf("should be empty: %s", v) } // Verify they are distinct states that can be read back from storage { // We'll use two distinct states here and verify that changing one // does not also change the other. fooState := states.NewState() barState := states.NewState() // write a known state to foo if err := foo.WriteState(fooState); err != nil { t.Fatal("error writing foo state:", err) } if err := foo.PersistState(nil); err != nil { t.Fatal("error persisting foo state:", err) } // We'll make "bar" different by adding a fake resource state to it. barState.SyncWrapper().SetResourceInstanceCurrent( addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }, }.Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte("{}"), Status: states.ObjectReady, SchemaVersion: 0, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) // write a distinct known state to bar if err := bar.WriteState(barState); err != nil { t.Fatalf("bad: %s", err) } if err := bar.PersistState(nil); err != nil { t.Fatalf("bad: %s", err) } // verify that foo is unchanged with the existing state manager if err := foo.RefreshState(); err != nil { t.Fatal("error refreshing foo:", err) } fooState = foo.State() if fooState.HasManagedResourceInstanceObjects() { t.Fatal("after writing a resource to bar, foo now has resources too") } // fetch foo again from the backend foo, err = b.StateMgr("foo") if err != nil { t.Fatal("error re-fetching state:", err) } if err := foo.RefreshState(); err != nil { t.Fatal("error refreshing foo:", err) } fooState = foo.State() if fooState.HasManagedResourceInstanceObjects() { t.Fatal("after writing a resource to bar and re-reading foo, foo now has resources too") } // fetch the bar again from the backend bar, err = b.StateMgr("bar") if err != nil { t.Fatal("error re-fetching state:", err) } if err := bar.RefreshState(); err != nil { t.Fatal("error refreshing bar:", err) } barState = bar.State() if !barState.HasManagedResourceInstanceObjects() { t.Fatal("after writing a resource instance object to bar and re-reading it, the object has vanished") } } // Verify we can now list them { // we determined that named stated are supported earlier workspaces, err := b.Workspaces() if err != nil { t.Fatalf("err: %s", err) } sort.Strings(workspaces) expected := []string{"bar", "default", "foo"} if noDefault { expected = []string{"bar", "foo"} } if !reflect.DeepEqual(workspaces, expected) { t.Fatalf("wrong workspaces list\ngot: %#v\nwant: %#v", workspaces, expected) } } // Delete some workspaces if err := b.DeleteWorkspace("foo", true); err != nil { t.Fatalf("err: %s", err) } // Verify the default state can't be deleted if err := b.DeleteWorkspace(DefaultStateName, true); err == nil { t.Fatal("expected error") } // Create and delete the foo workspace again. // Make sure that there are no leftover artifacts from a deleted state // preventing re-creation. foo, err = b.StateMgr("foo") if err != nil { t.Fatalf("error: %s", err) } if err := foo.RefreshState(); err != nil { t.Fatalf("bad: %s", err) } if v := foo.State(); v.HasManagedResourceInstanceObjects() { t.Fatalf("should be empty: %s", v) } // and delete it again if err := b.DeleteWorkspace("foo", true); err != nil { t.Fatalf("err: %s", err) } // Verify deletion { workspaces, err := b.Workspaces() if err != nil { t.Fatalf("err: %s", err) } sort.Strings(workspaces) expected := []string{"bar", "default"} if noDefault { expected = []string{"bar"} } if !reflect.DeepEqual(workspaces, expected) { t.Fatalf("wrong workspaces list\ngot: %#v\nwant: %#v", workspaces, expected) } } } // TestBackendStateLocks will test the locking functionality of the remote // state backend. func TestBackendStateLocks(t *testing.T, b1, b2 Backend) { t.Helper() testLocks(t, b1, b2, false) } // TestBackendStateForceUnlock verifies that the lock error is the expected // type, and the lock can be unlocked using the ID reported in the error. // Remote state backends that support -force-unlock should call this in at // least one of the acceptance tests. func TestBackendStateForceUnlock(t *testing.T, b1, b2 Backend) { t.Helper() testLocks(t, b1, b2, true) } // TestBackendStateLocksInWS will test the locking functionality of the remote // state backend. func TestBackendStateLocksInWS(t *testing.T, b1, b2 Backend, ws string) { t.Helper() testLocksInWorkspace(t, b1, b2, false, ws) } // TestBackendStateForceUnlockInWS verifies that the lock error is the expected // type, and the lock can be unlocked using the ID reported in the error. // Remote state backends that support -force-unlock should call this in at // least one of the acceptance tests. func TestBackendStateForceUnlockInWS(t *testing.T, b1, b2 Backend, ws string) { t.Helper() testLocksInWorkspace(t, b1, b2, true, ws) } func testLocks(t *testing.T, b1, b2 Backend, testForceUnlock bool) { testLocksInWorkspace(t, b1, b2, testForceUnlock, DefaultStateName) } func testLocksInWorkspace(t *testing.T, b1, b2 Backend, testForceUnlock bool, workspace string) { t.Helper() // Get the default state for each b1StateMgr, err := b1.StateMgr(DefaultStateName) if err != nil { t.Fatalf("error: %s", err) } if err := b1StateMgr.RefreshState(); err != nil { t.Fatalf("bad: %s", err) } // Fast exit if this doesn't support locking at all if _, ok := b1StateMgr.(statemgr.Locker); !ok { t.Logf("TestBackend: backend %T doesn't support state locking, not testing", b1) return } t.Logf("TestBackend: testing state locking for %T", b1) b2StateMgr, err := b2.StateMgr(DefaultStateName) if err != nil { t.Fatalf("error: %s", err) } if err := b2StateMgr.RefreshState(); err != nil { t.Fatalf("bad: %s", err) } // Reassign so its obvious whats happening lockerA := b1StateMgr.(statemgr.Locker) lockerB := b2StateMgr.(statemgr.Locker) infoA := statemgr.NewLockInfo() infoA.Operation = "test" infoA.Who = "clientA" infoB := statemgr.NewLockInfo() infoB.Operation = "test" infoB.Who = "clientB" lockIDA, err := lockerA.Lock(infoA) if err != nil { t.Fatal("unable to get initial lock:", err) } // Make sure we can still get the statemgr.Full from another instance even // when locked. This should only happen when a state is loaded via the // backend, and as a remote state. _, err = b2.StateMgr(DefaultStateName) if err != nil { t.Errorf("failed to read locked state from another backend instance: %s", err) } // If the lock ID is blank, assume locking is disabled if lockIDA == "" { t.Logf("TestBackend: %T: empty string returned for lock, assuming disabled", b1) return } _, err = lockerB.Lock(infoB) if err == nil { lockerA.Unlock(lockIDA) t.Fatal("client B obtained lock while held by client A") } if err := lockerA.Unlock(lockIDA); err != nil { t.Fatal("error unlocking client A", err) } lockIDB, err := lockerB.Lock(infoB) if err != nil { t.Fatal("unable to obtain lock from client B") } if lockIDB == lockIDA { t.Errorf("duplicate lock IDs: %q", lockIDB) } if err = lockerB.Unlock(lockIDB); err != nil { t.Fatal("error unlocking client B:", err) } // test the equivalent of -force-unlock, by using the id from the error // output. if !testForceUnlock { return } // get a new ID infoA.ID, err = uuid.GenerateUUID() if err != nil { panic(err) } lockIDA, err = lockerA.Lock(infoA) if err != nil { t.Fatal("unable to get re lock A:", err) } unlock := func() { err := lockerA.Unlock(lockIDA) if err != nil { t.Fatal(err) } } _, err = lockerB.Lock(infoB) if err == nil { unlock() t.Fatal("client B obtained lock while held by client A") } infoErr, ok := err.(*statemgr.LockError) if !ok { unlock() t.Fatalf("expected type *statemgr.LockError, got : %#v", err) } // try to unlock with the second unlocker, using the ID from the error if err := lockerB.Unlock(infoErr.Info.ID); err != nil { unlock() t.Fatalf("could not unlock with the reported ID %q: %s", infoErr.Info.ID, err) } }