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