github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/oss/client_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package oss
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	"bytes"
    13  	"crypto/md5"
    14  
    15  	"github.com/terramate-io/tf/backend"
    16  	"github.com/terramate-io/tf/states/remote"
    17  	"github.com/terramate-io/tf/states/statefile"
    18  	"github.com/terramate-io/tf/states/statemgr"
    19  )
    20  
    21  // NOTE: Before running this testcase, please create a OTS instance called 'tf-oss-remote'
    22  var RemoteTestUsedOTSEndpoint = "https://tf-oss-remote.cn-hangzhou.ots.aliyuncs.com"
    23  
    24  func TestRemoteClient_impl(t *testing.T) {
    25  	var _ remote.Client = new(RemoteClient)
    26  	var _ remote.ClientLocker = new(RemoteClient)
    27  }
    28  
    29  func TestRemoteClient(t *testing.T) {
    30  	testACC(t)
    31  	bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix())
    32  	path := "testState"
    33  
    34  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    35  		"bucket":  bucketName,
    36  		"prefix":  path,
    37  		"encrypt": true,
    38  	})).(*Backend)
    39  
    40  	createOSSBucket(t, b.ossClient, bucketName)
    41  	defer deleteOSSBucket(t, b.ossClient, bucketName)
    42  
    43  	state, err := b.StateMgr(backend.DefaultStateName)
    44  	if err != nil {
    45  		t.Fatal(err)
    46  	}
    47  
    48  	remote.TestClient(t, state.(*remote.State).Client)
    49  }
    50  
    51  func TestRemoteClientLocks(t *testing.T) {
    52  	testACC(t)
    53  	bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix())
    54  	tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix())
    55  	path := "testState"
    56  
    57  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    58  		"bucket":              bucketName,
    59  		"prefix":              path,
    60  		"encrypt":             true,
    61  		"tablestore_table":    tableName,
    62  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
    63  	})).(*Backend)
    64  
    65  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    66  		"bucket":              bucketName,
    67  		"prefix":              path,
    68  		"encrypt":             true,
    69  		"tablestore_table":    tableName,
    70  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
    71  	})).(*Backend)
    72  
    73  	createOSSBucket(t, b1.ossClient, bucketName)
    74  	defer deleteOSSBucket(t, b1.ossClient, bucketName)
    75  	createTablestoreTable(t, b1.otsClient, tableName)
    76  	defer deleteTablestoreTable(t, b1.otsClient, tableName)
    77  
    78  	s1, err := b1.StateMgr(backend.DefaultStateName)
    79  	if err != nil {
    80  		t.Fatal(err)
    81  	}
    82  
    83  	s2, err := b2.StateMgr(backend.DefaultStateName)
    84  	if err != nil {
    85  		t.Fatal(err)
    86  	}
    87  
    88  	remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
    89  }
    90  
    91  // verify that the backend can handle more than one state in the same table
    92  func TestRemoteClientLocks_multipleStates(t *testing.T) {
    93  	testACC(t)
    94  	bucketName := fmt.Sprintf("tf-remote-oss-test-force-%x", time.Now().Unix())
    95  	tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix())
    96  	path := "testState"
    97  
    98  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    99  		"bucket":              bucketName,
   100  		"prefix":              path,
   101  		"encrypt":             true,
   102  		"tablestore_table":    tableName,
   103  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
   104  	})).(*Backend)
   105  
   106  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   107  		"bucket":              bucketName,
   108  		"prefix":              path,
   109  		"encrypt":             true,
   110  		"tablestore_table":    tableName,
   111  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
   112  	})).(*Backend)
   113  
   114  	createOSSBucket(t, b1.ossClient, bucketName)
   115  	defer deleteOSSBucket(t, b1.ossClient, bucketName)
   116  	createTablestoreTable(t, b1.otsClient, tableName)
   117  	defer deleteTablestoreTable(t, b1.otsClient, tableName)
   118  
   119  	s1, err := b1.StateMgr("s1")
   120  	if err != nil {
   121  		t.Fatal(err)
   122  	}
   123  	if _, err := s1.Lock(statemgr.NewLockInfo()); err != nil {
   124  		t.Fatal("failed to get lock for s1:", err)
   125  	}
   126  
   127  	// s1 is now locked, s2 should not be locked as it's a different state file
   128  	s2, err := b2.StateMgr("s2")
   129  	if err != nil {
   130  		t.Fatal(err)
   131  	}
   132  	if _, err := s2.Lock(statemgr.NewLockInfo()); err != nil {
   133  		t.Fatal("failed to get lock for s2:", err)
   134  	}
   135  }
   136  
   137  // verify that we can unlock a state with an existing lock
   138  func TestRemoteForceUnlock(t *testing.T) {
   139  	testACC(t)
   140  	bucketName := fmt.Sprintf("tf-remote-oss-test-force-%x", time.Now().Unix())
   141  	tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix())
   142  	path := "testState"
   143  
   144  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   145  		"bucket":              bucketName,
   146  		"prefix":              path,
   147  		"encrypt":             true,
   148  		"tablestore_table":    tableName,
   149  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
   150  	})).(*Backend)
   151  
   152  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   153  		"bucket":              bucketName,
   154  		"prefix":              path,
   155  		"encrypt":             true,
   156  		"tablestore_table":    tableName,
   157  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
   158  	})).(*Backend)
   159  
   160  	createOSSBucket(t, b1.ossClient, bucketName)
   161  	defer deleteOSSBucket(t, b1.ossClient, bucketName)
   162  	createTablestoreTable(t, b1.otsClient, tableName)
   163  	defer deleteTablestoreTable(t, b1.otsClient, tableName)
   164  
   165  	// first test with default
   166  	s1, err := b1.StateMgr(backend.DefaultStateName)
   167  	if err != nil {
   168  		t.Fatal(err)
   169  	}
   170  
   171  	info := statemgr.NewLockInfo()
   172  	info.Operation = "test"
   173  	info.Who = "clientA"
   174  
   175  	lockID, err := s1.Lock(info)
   176  	if err != nil {
   177  		t.Fatal("unable to get initial lock:", err)
   178  	}
   179  
   180  	// s1 is now locked, get the same state through s2 and unlock it
   181  	s2, err := b2.StateMgr(backend.DefaultStateName)
   182  	if err != nil {
   183  		t.Fatal("failed to get default state to force unlock:", err)
   184  	}
   185  
   186  	if err := s2.Unlock(lockID); err != nil {
   187  		t.Fatal("failed to force-unlock default state")
   188  	}
   189  
   190  	// now try the same thing with a named state
   191  	// first test with default
   192  	s1, err = b1.StateMgr("test")
   193  	if err != nil {
   194  		t.Fatal(err)
   195  	}
   196  
   197  	info = statemgr.NewLockInfo()
   198  	info.Operation = "test"
   199  	info.Who = "clientA"
   200  
   201  	lockID, err = s1.Lock(info)
   202  	if err != nil {
   203  		t.Fatal("unable to get initial lock:", err)
   204  	}
   205  
   206  	// s1 is now locked, get the same state through s2 and unlock it
   207  	s2, err = b2.StateMgr("test")
   208  	if err != nil {
   209  		t.Fatal("failed to get named state to force unlock:", err)
   210  	}
   211  
   212  	if err = s2.Unlock(lockID); err != nil {
   213  		t.Fatal("failed to force-unlock named state")
   214  	}
   215  }
   216  
   217  func TestRemoteClient_clientMD5(t *testing.T) {
   218  	testACC(t)
   219  
   220  	bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix())
   221  	tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix())
   222  	path := "testState"
   223  
   224  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   225  		"bucket":              bucketName,
   226  		"prefix":              path,
   227  		"tablestore_table":    tableName,
   228  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
   229  	})).(*Backend)
   230  
   231  	createOSSBucket(t, b.ossClient, bucketName)
   232  	defer deleteOSSBucket(t, b.ossClient, bucketName)
   233  	createTablestoreTable(t, b.otsClient, tableName)
   234  	defer deleteTablestoreTable(t, b.otsClient, tableName)
   235  
   236  	s, err := b.StateMgr(backend.DefaultStateName)
   237  	if err != nil {
   238  		t.Fatal(err)
   239  	}
   240  	client := s.(*remote.State).Client.(*RemoteClient)
   241  
   242  	sum := md5.Sum([]byte("test"))
   243  
   244  	if err := client.putMD5(sum[:]); err != nil {
   245  		t.Fatal(err)
   246  	}
   247  
   248  	getSum, err := client.getMD5()
   249  	if err != nil {
   250  		t.Fatal(err)
   251  	}
   252  
   253  	if !bytes.Equal(getSum, sum[:]) {
   254  		t.Fatalf("getMD5 returned the wrong checksum: expected %x, got %x", sum[:], getSum)
   255  	}
   256  
   257  	if err := client.deleteMD5(); err != nil {
   258  		t.Fatal(err)
   259  	}
   260  
   261  	if getSum, err := client.getMD5(); err == nil {
   262  		t.Fatalf("expected getMD5 error, got none. checksum: %x", getSum)
   263  	}
   264  }
   265  
   266  // verify that a client won't return a state with an incorrect checksum.
   267  func TestRemoteClient_stateChecksum(t *testing.T) {
   268  	testACC(t)
   269  
   270  	bucketName := fmt.Sprintf("tf-remote-oss-test-%x", time.Now().Unix())
   271  	tableName := fmt.Sprintf("tfRemoteTestForce%x", time.Now().Unix())
   272  	path := "testState"
   273  
   274  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   275  		"bucket":              bucketName,
   276  		"prefix":              path,
   277  		"tablestore_table":    tableName,
   278  		"tablestore_endpoint": RemoteTestUsedOTSEndpoint,
   279  	})).(*Backend)
   280  
   281  	createOSSBucket(t, b1.ossClient, bucketName)
   282  	defer deleteOSSBucket(t, b1.ossClient, bucketName)
   283  	createTablestoreTable(t, b1.otsClient, tableName)
   284  	defer deleteTablestoreTable(t, b1.otsClient, tableName)
   285  
   286  	s1, err := b1.StateMgr(backend.DefaultStateName)
   287  	if err != nil {
   288  		t.Fatal(err)
   289  	}
   290  	client1 := s1.(*remote.State).Client
   291  
   292  	// create an old and new state version to persist
   293  	s := statemgr.TestFullInitialState()
   294  	sf := &statefile.File{State: s}
   295  	var oldState bytes.Buffer
   296  	if err := statefile.Write(sf, &oldState); err != nil {
   297  		t.Fatal(err)
   298  	}
   299  	sf.Serial++
   300  	var newState bytes.Buffer
   301  	if err := statefile.Write(sf, &newState); err != nil {
   302  		t.Fatal(err)
   303  	}
   304  
   305  	// Use b2 without a tablestore_table to bypass the lock table to write the state directly.
   306  	// client2 will write the "incorrect" state, simulating oss eventually consistency delays
   307  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   308  		"bucket": bucketName,
   309  		"prefix": path,
   310  	})).(*Backend)
   311  	s2, err := b2.StateMgr(backend.DefaultStateName)
   312  	if err != nil {
   313  		t.Fatal(err)
   314  	}
   315  	client2 := s2.(*remote.State).Client
   316  
   317  	// write the new state through client2 so that there is no checksum yet
   318  	if err := client2.Put(newState.Bytes()); err != nil {
   319  		t.Fatal(err)
   320  	}
   321  
   322  	// verify that we can pull a state without a checksum
   323  	if _, err := client1.Get(); err != nil {
   324  		t.Fatal(err)
   325  	}
   326  
   327  	// write the new state back with its checksum
   328  	if err := client1.Put(newState.Bytes()); err != nil {
   329  		t.Fatal(err)
   330  	}
   331  
   332  	// put an empty state in place to check for panics during get
   333  	if err := client2.Put([]byte{}); err != nil {
   334  		t.Fatal(err)
   335  	}
   336  
   337  	// remove the timeouts so we can fail immediately
   338  	origTimeout := consistencyRetryTimeout
   339  	origInterval := consistencyRetryPollInterval
   340  	defer func() {
   341  		consistencyRetryTimeout = origTimeout
   342  		consistencyRetryPollInterval = origInterval
   343  	}()
   344  	consistencyRetryTimeout = 0
   345  	consistencyRetryPollInterval = 0
   346  
   347  	// fetching an empty state through client1 should now error out due to a
   348  	// mismatched checksum.
   349  	if _, err := client1.Get(); !strings.HasPrefix(err.Error(), errBadChecksumFmt[:80]) {
   350  		t.Fatalf("expected state checksum error: got %s", err)
   351  	}
   352  
   353  	// put the old state in place of the new, without updating the checksum
   354  	if err := client2.Put(oldState.Bytes()); err != nil {
   355  		t.Fatal(err)
   356  	}
   357  
   358  	// fetching the wrong state through client1 should now error out due to a
   359  	// mismatched checksum.
   360  	if _, err := client1.Get(); !strings.HasPrefix(err.Error(), errBadChecksumFmt[:80]) {
   361  		t.Fatalf("expected state checksum error: got %s", err)
   362  	}
   363  
   364  	// update the state with the correct one after we Get again
   365  	testChecksumHook = func() {
   366  		if err := client2.Put(newState.Bytes()); err != nil {
   367  			t.Fatal(err)
   368  		}
   369  		testChecksumHook = nil
   370  	}
   371  
   372  	consistencyRetryTimeout = origTimeout
   373  
   374  	// this final Get will fail to fail the checksum verification, the above
   375  	// callback will update the state with the correct version, and Get should
   376  	// retry automatically.
   377  	if _, err := client1.Get(); err != nil {
   378  		t.Fatal(err)
   379  	}
   380  }