github.com/opentofu/opentofu@v1.7.1/internal/backend/testing.go (about)

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