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  }