github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote-state/s3/client_test.go (about)

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