github.com/kevinklinger/open_terraform@v1.3.6/noninternal/backend/remote-state/s3/backend_test.go (about)

     1  package s3
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"reflect"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/aws/aws-sdk-go/aws"
    12  	"github.com/aws/aws-sdk-go/service/dynamodb"
    13  	"github.com/aws/aws-sdk-go/service/s3"
    14  	awsbase "github.com/hashicorp/aws-sdk-go-base"
    15  	"github.com/kevinklinger/open_terraform/noninternal/backend"
    16  	"github.com/kevinklinger/open_terraform/noninternal/configs/hcl2shim"
    17  	"github.com/kevinklinger/open_terraform/noninternal/states"
    18  	"github.com/kevinklinger/open_terraform/noninternal/states/remote"
    19  )
    20  
    21  var (
    22  	mockStsGetCallerIdentityRequestBody = url.Values{
    23  		"Action":  []string{"GetCallerIdentity"},
    24  		"Version": []string{"2011-06-15"},
    25  	}.Encode()
    26  )
    27  
    28  // verify that we are doing ACC tests or the S3 tests specifically
    29  func testACC(t *testing.T) {
    30  	skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_S3_TEST") == ""
    31  	if skip {
    32  		t.Log("s3 backend tests require setting TF_ACC or TF_S3_TEST")
    33  		t.Skip()
    34  	}
    35  	if os.Getenv("AWS_DEFAULT_REGION") == "" {
    36  		os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
    37  	}
    38  }
    39  
    40  func TestBackend_impl(t *testing.T) {
    41  	var _ backend.Backend = new(Backend)
    42  }
    43  
    44  func TestBackendConfig(t *testing.T) {
    45  	testACC(t)
    46  	config := map[string]interface{}{
    47  		"region":         "us-west-1",
    48  		"bucket":         "tf-test",
    49  		"key":            "state",
    50  		"encrypt":        true,
    51  		"dynamodb_table": "dynamoTable",
    52  	}
    53  
    54  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
    55  
    56  	if *b.s3Client.Config.Region != "us-west-1" {
    57  		t.Fatalf("Incorrect region was populated")
    58  	}
    59  	if b.bucketName != "tf-test" {
    60  		t.Fatalf("Incorrect bucketName was populated")
    61  	}
    62  	if b.keyName != "state" {
    63  		t.Fatalf("Incorrect keyName was populated")
    64  	}
    65  
    66  	credentials, err := b.s3Client.Config.Credentials.Get()
    67  	if err != nil {
    68  		t.Fatalf("Error when requesting credentials")
    69  	}
    70  	if credentials.AccessKeyID == "" {
    71  		t.Fatalf("No Access Key Id was populated")
    72  	}
    73  	if credentials.SecretAccessKey == "" {
    74  		t.Fatalf("No Secret Access Key was populated")
    75  	}
    76  }
    77  
    78  func TestBackendConfig_AssumeRole(t *testing.T) {
    79  	testACC(t)
    80  
    81  	testCases := []struct {
    82  		Config           map[string]interface{}
    83  		Description      string
    84  		MockStsEndpoints []*awsbase.MockEndpoint
    85  	}{
    86  		{
    87  			Config: map[string]interface{}{
    88  				"bucket":       "tf-test",
    89  				"key":          "state",
    90  				"region":       "us-west-1",
    91  				"role_arn":     awsbase.MockStsAssumeRoleArn,
    92  				"session_name": awsbase.MockStsAssumeRoleSessionName,
    93  			},
    94  			Description: "role_arn",
    95  			MockStsEndpoints: []*awsbase.MockEndpoint{
    96  				{
    97  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
    98  						"Action":          []string{"AssumeRole"},
    99  						"DurationSeconds": []string{"900"},
   100  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   101  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   102  						"Version":         []string{"2011-06-15"},
   103  					}.Encode()},
   104  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   105  				},
   106  				{
   107  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   108  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   109  				},
   110  			},
   111  		},
   112  		{
   113  			Config: map[string]interface{}{
   114  				"assume_role_duration_seconds": 3600,
   115  				"bucket":                       "tf-test",
   116  				"key":                          "state",
   117  				"region":                       "us-west-1",
   118  				"role_arn":                     awsbase.MockStsAssumeRoleArn,
   119  				"session_name":                 awsbase.MockStsAssumeRoleSessionName,
   120  			},
   121  			Description: "assume_role_duration_seconds",
   122  			MockStsEndpoints: []*awsbase.MockEndpoint{
   123  				{
   124  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   125  						"Action":          []string{"AssumeRole"},
   126  						"DurationSeconds": []string{"3600"},
   127  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   128  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   129  						"Version":         []string{"2011-06-15"},
   130  					}.Encode()},
   131  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   132  				},
   133  				{
   134  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   135  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   136  				},
   137  			},
   138  		},
   139  		{
   140  			Config: map[string]interface{}{
   141  				"bucket":       "tf-test",
   142  				"external_id":  awsbase.MockStsAssumeRoleExternalId,
   143  				"key":          "state",
   144  				"region":       "us-west-1",
   145  				"role_arn":     awsbase.MockStsAssumeRoleArn,
   146  				"session_name": awsbase.MockStsAssumeRoleSessionName,
   147  			},
   148  			Description: "external_id",
   149  			MockStsEndpoints: []*awsbase.MockEndpoint{
   150  				{
   151  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   152  						"Action":          []string{"AssumeRole"},
   153  						"DurationSeconds": []string{"900"},
   154  						"ExternalId":      []string{awsbase.MockStsAssumeRoleExternalId},
   155  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   156  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   157  						"Version":         []string{"2011-06-15"},
   158  					}.Encode()},
   159  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   160  				},
   161  				{
   162  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   163  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   164  				},
   165  			},
   166  		},
   167  		{
   168  			Config: map[string]interface{}{
   169  				"assume_role_policy": awsbase.MockStsAssumeRolePolicy,
   170  				"bucket":             "tf-test",
   171  				"key":                "state",
   172  				"region":             "us-west-1",
   173  				"role_arn":           awsbase.MockStsAssumeRoleArn,
   174  				"session_name":       awsbase.MockStsAssumeRoleSessionName,
   175  			},
   176  			Description: "assume_role_policy",
   177  			MockStsEndpoints: []*awsbase.MockEndpoint{
   178  				{
   179  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   180  						"Action":          []string{"AssumeRole"},
   181  						"DurationSeconds": []string{"900"},
   182  						"Policy":          []string{awsbase.MockStsAssumeRolePolicy},
   183  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   184  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   185  						"Version":         []string{"2011-06-15"},
   186  					}.Encode()},
   187  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   188  				},
   189  				{
   190  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   191  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   192  				},
   193  			},
   194  		},
   195  		{
   196  			Config: map[string]interface{}{
   197  				"assume_role_policy_arns": []interface{}{awsbase.MockStsAssumeRolePolicyArn},
   198  				"bucket":                  "tf-test",
   199  				"key":                     "state",
   200  				"region":                  "us-west-1",
   201  				"role_arn":                awsbase.MockStsAssumeRoleArn,
   202  				"session_name":            awsbase.MockStsAssumeRoleSessionName,
   203  			},
   204  			Description: "assume_role_policy_arns",
   205  			MockStsEndpoints: []*awsbase.MockEndpoint{
   206  				{
   207  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   208  						"Action":                  []string{"AssumeRole"},
   209  						"DurationSeconds":         []string{"900"},
   210  						"PolicyArns.member.1.arn": []string{awsbase.MockStsAssumeRolePolicyArn},
   211  						"RoleArn":                 []string{awsbase.MockStsAssumeRoleArn},
   212  						"RoleSessionName":         []string{awsbase.MockStsAssumeRoleSessionName},
   213  						"Version":                 []string{"2011-06-15"},
   214  					}.Encode()},
   215  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   216  				},
   217  				{
   218  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   219  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   220  				},
   221  			},
   222  		},
   223  		{
   224  			Config: map[string]interface{}{
   225  				"assume_role_tags": map[string]interface{}{
   226  					awsbase.MockStsAssumeRoleTagKey: awsbase.MockStsAssumeRoleTagValue,
   227  				},
   228  				"bucket":       "tf-test",
   229  				"key":          "state",
   230  				"region":       "us-west-1",
   231  				"role_arn":     awsbase.MockStsAssumeRoleArn,
   232  				"session_name": awsbase.MockStsAssumeRoleSessionName,
   233  			},
   234  			Description: "assume_role_tags",
   235  			MockStsEndpoints: []*awsbase.MockEndpoint{
   236  				{
   237  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   238  						"Action":              []string{"AssumeRole"},
   239  						"DurationSeconds":     []string{"900"},
   240  						"RoleArn":             []string{awsbase.MockStsAssumeRoleArn},
   241  						"RoleSessionName":     []string{awsbase.MockStsAssumeRoleSessionName},
   242  						"Tags.member.1.Key":   []string{awsbase.MockStsAssumeRoleTagKey},
   243  						"Tags.member.1.Value": []string{awsbase.MockStsAssumeRoleTagValue},
   244  						"Version":             []string{"2011-06-15"},
   245  					}.Encode()},
   246  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   247  				},
   248  				{
   249  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   250  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   251  				},
   252  			},
   253  		},
   254  		{
   255  			Config: map[string]interface{}{
   256  				"assume_role_tags": map[string]interface{}{
   257  					awsbase.MockStsAssumeRoleTagKey: awsbase.MockStsAssumeRoleTagValue,
   258  				},
   259  				"assume_role_transitive_tag_keys": []interface{}{awsbase.MockStsAssumeRoleTagKey},
   260  				"bucket":                          "tf-test",
   261  				"key":                             "state",
   262  				"region":                          "us-west-1",
   263  				"role_arn":                        awsbase.MockStsAssumeRoleArn,
   264  				"session_name":                    awsbase.MockStsAssumeRoleSessionName,
   265  			},
   266  			Description: "assume_role_transitive_tag_keys",
   267  			MockStsEndpoints: []*awsbase.MockEndpoint{
   268  				{
   269  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   270  						"Action":                     []string{"AssumeRole"},
   271  						"DurationSeconds":            []string{"900"},
   272  						"RoleArn":                    []string{awsbase.MockStsAssumeRoleArn},
   273  						"RoleSessionName":            []string{awsbase.MockStsAssumeRoleSessionName},
   274  						"Tags.member.1.Key":          []string{awsbase.MockStsAssumeRoleTagKey},
   275  						"Tags.member.1.Value":        []string{awsbase.MockStsAssumeRoleTagValue},
   276  						"TransitiveTagKeys.member.1": []string{awsbase.MockStsAssumeRoleTagKey},
   277  						"Version":                    []string{"2011-06-15"},
   278  					}.Encode()},
   279  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   280  				},
   281  				{
   282  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   283  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   284  				},
   285  			},
   286  		},
   287  	}
   288  
   289  	for _, testCase := range testCases {
   290  		testCase := testCase
   291  
   292  		t.Run(testCase.Description, func(t *testing.T) {
   293  			closeSts, mockStsSession, err := awsbase.GetMockedAwsApiSession("STS", testCase.MockStsEndpoints)
   294  			defer closeSts()
   295  
   296  			if err != nil {
   297  				t.Fatalf("unexpected error creating mock STS server: %s", err)
   298  			}
   299  
   300  			if mockStsSession != nil && mockStsSession.Config != nil {
   301  				testCase.Config["sts_endpoint"] = aws.StringValue(mockStsSession.Config.Endpoint)
   302  			}
   303  
   304  			diags := New().Configure(hcl2shim.HCL2ValueFromConfigValue(testCase.Config))
   305  
   306  			if diags.HasErrors() {
   307  				for _, diag := range diags {
   308  					t.Errorf("unexpected error: %s", diag.Description().Summary)
   309  				}
   310  			}
   311  		})
   312  	}
   313  }
   314  
   315  func TestBackendConfig_invalidKey(t *testing.T) {
   316  	testACC(t)
   317  	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{
   318  		"region":         "us-west-1",
   319  		"bucket":         "tf-test",
   320  		"key":            "/leading-slash",
   321  		"encrypt":        true,
   322  		"dynamodb_table": "dynamoTable",
   323  	})
   324  
   325  	_, diags := New().PrepareConfig(cfg)
   326  	if !diags.HasErrors() {
   327  		t.Fatal("expected config validation error")
   328  	}
   329  }
   330  
   331  func TestBackendConfig_invalidSSECustomerKeyLength(t *testing.T) {
   332  	testACC(t)
   333  	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{
   334  		"region":           "us-west-1",
   335  		"bucket":           "tf-test",
   336  		"encrypt":          true,
   337  		"key":              "state",
   338  		"dynamodb_table":   "dynamoTable",
   339  		"sse_customer_key": "key",
   340  	})
   341  
   342  	_, diags := New().PrepareConfig(cfg)
   343  	if !diags.HasErrors() {
   344  		t.Fatal("expected error for invalid sse_customer_key length")
   345  	}
   346  }
   347  
   348  func TestBackendConfig_invalidSSECustomerKeyEncoding(t *testing.T) {
   349  	testACC(t)
   350  	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{
   351  		"region":           "us-west-1",
   352  		"bucket":           "tf-test",
   353  		"encrypt":          true,
   354  		"key":              "state",
   355  		"dynamodb_table":   "dynamoTable",
   356  		"sse_customer_key": "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka",
   357  	})
   358  
   359  	diags := New().Configure(cfg)
   360  	if !diags.HasErrors() {
   361  		t.Fatal("expected error for failing to decode sse_customer_key")
   362  	}
   363  }
   364  
   365  func TestBackendConfig_conflictingEncryptionSchema(t *testing.T) {
   366  	testACC(t)
   367  	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{
   368  		"region":           "us-west-1",
   369  		"bucket":           "tf-test",
   370  		"key":              "state",
   371  		"encrypt":          true,
   372  		"dynamodb_table":   "dynamoTable",
   373  		"sse_customer_key": "1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o=",
   374  		"kms_key_id":       "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
   375  	})
   376  
   377  	diags := New().Configure(cfg)
   378  	if !diags.HasErrors() {
   379  		t.Fatal("expected error for simultaneous usage of kms_key_id and sse_customer_key")
   380  	}
   381  }
   382  
   383  func TestBackend(t *testing.T) {
   384  	testACC(t)
   385  
   386  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   387  	keyName := "testState"
   388  
   389  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   390  		"bucket":  bucketName,
   391  		"key":     keyName,
   392  		"encrypt": true,
   393  	})).(*Backend)
   394  
   395  	createS3Bucket(t, b.s3Client, bucketName)
   396  	defer deleteS3Bucket(t, b.s3Client, bucketName)
   397  
   398  	backend.TestBackendStates(t, b)
   399  }
   400  
   401  func TestBackendLocked(t *testing.T) {
   402  	testACC(t)
   403  
   404  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   405  	keyName := "test/state"
   406  
   407  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   408  		"bucket":         bucketName,
   409  		"key":            keyName,
   410  		"encrypt":        true,
   411  		"dynamodb_table": bucketName,
   412  	})).(*Backend)
   413  
   414  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   415  		"bucket":         bucketName,
   416  		"key":            keyName,
   417  		"encrypt":        true,
   418  		"dynamodb_table": bucketName,
   419  	})).(*Backend)
   420  
   421  	createS3Bucket(t, b1.s3Client, bucketName)
   422  	defer deleteS3Bucket(t, b1.s3Client, bucketName)
   423  	createDynamoDBTable(t, b1.dynClient, bucketName)
   424  	defer deleteDynamoDBTable(t, b1.dynClient, bucketName)
   425  
   426  	backend.TestBackendStateLocks(t, b1, b2)
   427  	backend.TestBackendStateForceUnlock(t, b1, b2)
   428  }
   429  
   430  func TestBackendSSECustomerKey(t *testing.T) {
   431  	testACC(t)
   432  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   433  
   434  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   435  		"bucket":           bucketName,
   436  		"encrypt":          true,
   437  		"key":              "test-SSE-C",
   438  		"sse_customer_key": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
   439  	})).(*Backend)
   440  
   441  	createS3Bucket(t, b.s3Client, bucketName)
   442  	defer deleteS3Bucket(t, b.s3Client, bucketName)
   443  
   444  	backend.TestBackendStates(t, b)
   445  }
   446  
   447  // add some extra junk in S3 to try and confuse the env listing.
   448  func TestBackendExtraPaths(t *testing.T) {
   449  	testACC(t)
   450  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   451  	keyName := "test/state/tfstate"
   452  
   453  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   454  		"bucket":  bucketName,
   455  		"key":     keyName,
   456  		"encrypt": true,
   457  	})).(*Backend)
   458  
   459  	createS3Bucket(t, b.s3Client, bucketName)
   460  	defer deleteS3Bucket(t, b.s3Client, bucketName)
   461  
   462  	// put multiple states in old env paths.
   463  	s1 := states.NewState()
   464  	s2 := states.NewState()
   465  
   466  	// RemoteClient to Put things in various paths
   467  	client := &RemoteClient{
   468  		s3Client:             b.s3Client,
   469  		dynClient:            b.dynClient,
   470  		bucketName:           b.bucketName,
   471  		path:                 b.path("s1"),
   472  		serverSideEncryption: b.serverSideEncryption,
   473  		acl:                  b.acl,
   474  		kmsKeyID:             b.kmsKeyID,
   475  		ddbTable:             b.ddbTable,
   476  	}
   477  
   478  	// Write the first state
   479  	stateMgr := &remote.State{Client: client}
   480  	stateMgr.WriteState(s1)
   481  	if err := stateMgr.PersistState(nil); err != nil {
   482  		t.Fatal(err)
   483  	}
   484  
   485  	// Write the second state
   486  	// Note a new state manager - otherwise, because these
   487  	// states are equal, the state will not Put to the remote
   488  	client.path = b.path("s2")
   489  	stateMgr2 := &remote.State{Client: client}
   490  	stateMgr2.WriteState(s2)
   491  	if err := stateMgr2.PersistState(nil); err != nil {
   492  		t.Fatal(err)
   493  	}
   494  
   495  	s2Lineage := stateMgr2.StateSnapshotMeta().Lineage
   496  
   497  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
   498  		t.Fatal(err)
   499  	}
   500  
   501  	// put a state in an env directory name
   502  	client.path = b.workspaceKeyPrefix + "/error"
   503  	stateMgr.WriteState(states.NewState())
   504  	if err := stateMgr.PersistState(nil); err != nil {
   505  		t.Fatal(err)
   506  	}
   507  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
   508  		t.Fatal(err)
   509  	}
   510  
   511  	// add state with the wrong key for an existing env
   512  	client.path = b.workspaceKeyPrefix + "/s2/notTestState"
   513  	stateMgr.WriteState(states.NewState())
   514  	if err := stateMgr.PersistState(nil); err != nil {
   515  		t.Fatal(err)
   516  	}
   517  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
   518  		t.Fatal(err)
   519  	}
   520  
   521  	// remove the state with extra subkey
   522  	if err := client.Delete(); err != nil {
   523  		t.Fatal(err)
   524  	}
   525  
   526  	// delete the real workspace
   527  	if err := b.DeleteWorkspace("s2"); err != nil {
   528  		t.Fatal(err)
   529  	}
   530  
   531  	if err := checkStateList(b, []string{"default", "s1"}); err != nil {
   532  		t.Fatal(err)
   533  	}
   534  
   535  	// fetch that state again, which should produce a new lineage
   536  	s2Mgr, err := b.StateMgr("s2")
   537  	if err != nil {
   538  		t.Fatal(err)
   539  	}
   540  	if err := s2Mgr.RefreshState(); err != nil {
   541  		t.Fatal(err)
   542  	}
   543  
   544  	if s2Mgr.(*remote.State).StateSnapshotMeta().Lineage == s2Lineage {
   545  		t.Fatal("state s2 was not deleted")
   546  	}
   547  	s2 = s2Mgr.State()
   548  	s2Lineage = stateMgr.StateSnapshotMeta().Lineage
   549  
   550  	// add a state with a key that matches an existing environment dir name
   551  	client.path = b.workspaceKeyPrefix + "/s2/"
   552  	stateMgr.WriteState(states.NewState())
   553  	if err := stateMgr.PersistState(nil); err != nil {
   554  		t.Fatal(err)
   555  	}
   556  
   557  	// make sure s2 is OK
   558  	s2Mgr, err = b.StateMgr("s2")
   559  	if err != nil {
   560  		t.Fatal(err)
   561  	}
   562  	if err := s2Mgr.RefreshState(); err != nil {
   563  		t.Fatal(err)
   564  	}
   565  
   566  	if stateMgr.StateSnapshotMeta().Lineage != s2Lineage {
   567  		t.Fatal("we got the wrong state for s2")
   568  	}
   569  
   570  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
   571  		t.Fatal(err)
   572  	}
   573  }
   574  
   575  // ensure we can separate the workspace prefix when it also matches the prefix
   576  // of the workspace name itself.
   577  func TestBackendPrefixInWorkspace(t *testing.T) {
   578  	testACC(t)
   579  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   580  
   581  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   582  		"bucket":               bucketName,
   583  		"key":                  "test-env.tfstate",
   584  		"workspace_key_prefix": "env",
   585  	})).(*Backend)
   586  
   587  	createS3Bucket(t, b.s3Client, bucketName)
   588  	defer deleteS3Bucket(t, b.s3Client, bucketName)
   589  
   590  	// get a state that contains the prefix as a substring
   591  	sMgr, err := b.StateMgr("env-1")
   592  	if err != nil {
   593  		t.Fatal(err)
   594  	}
   595  	if err := sMgr.RefreshState(); err != nil {
   596  		t.Fatal(err)
   597  	}
   598  
   599  	if err := checkStateList(b, []string{"default", "env-1"}); err != nil {
   600  		t.Fatal(err)
   601  	}
   602  }
   603  
   604  func TestKeyEnv(t *testing.T) {
   605  	testACC(t)
   606  	keyName := "some/paths/tfstate"
   607  
   608  	bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix())
   609  	b0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   610  		"bucket":               bucket0Name,
   611  		"key":                  keyName,
   612  		"encrypt":              true,
   613  		"workspace_key_prefix": "",
   614  	})).(*Backend)
   615  
   616  	createS3Bucket(t, b0.s3Client, bucket0Name)
   617  	defer deleteS3Bucket(t, b0.s3Client, bucket0Name)
   618  
   619  	bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix())
   620  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   621  		"bucket":               bucket1Name,
   622  		"key":                  keyName,
   623  		"encrypt":              true,
   624  		"workspace_key_prefix": "project/env:",
   625  	})).(*Backend)
   626  
   627  	createS3Bucket(t, b1.s3Client, bucket1Name)
   628  	defer deleteS3Bucket(t, b1.s3Client, bucket1Name)
   629  
   630  	bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix())
   631  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   632  		"bucket":  bucket2Name,
   633  		"key":     keyName,
   634  		"encrypt": true,
   635  	})).(*Backend)
   636  
   637  	createS3Bucket(t, b2.s3Client, bucket2Name)
   638  	defer deleteS3Bucket(t, b2.s3Client, bucket2Name)
   639  
   640  	if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil {
   641  		t.Fatal(err)
   642  	}
   643  
   644  	if err := testGetWorkspaceForKey(b0, "ws1/some/paths/tfstate", "ws1"); err != nil {
   645  		t.Fatal(err)
   646  	}
   647  
   648  	if err := testGetWorkspaceForKey(b1, "project/env:/ws1/some/paths/tfstate", "ws1"); err != nil {
   649  		t.Fatal(err)
   650  	}
   651  
   652  	if err := testGetWorkspaceForKey(b1, "project/env:/ws2/some/paths/tfstate", "ws2"); err != nil {
   653  		t.Fatal(err)
   654  	}
   655  
   656  	if err := testGetWorkspaceForKey(b2, "env:/ws3/some/paths/tfstate", "ws3"); err != nil {
   657  		t.Fatal(err)
   658  	}
   659  
   660  	backend.TestBackendStates(t, b0)
   661  	backend.TestBackendStates(t, b1)
   662  	backend.TestBackendStates(t, b2)
   663  }
   664  
   665  func testGetWorkspaceForKey(b *Backend, key string, expected string) error {
   666  	if actual := b.keyEnv(key); actual != expected {
   667  		return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual)
   668  	}
   669  	return nil
   670  }
   671  
   672  func checkStateList(b backend.Backend, expected []string) error {
   673  	states, err := b.Workspaces()
   674  	if err != nil {
   675  		return err
   676  	}
   677  
   678  	if !reflect.DeepEqual(states, expected) {
   679  		return fmt.Errorf("incorrect states listed: %q", states)
   680  	}
   681  	return nil
   682  }
   683  
   684  func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
   685  	createBucketReq := &s3.CreateBucketInput{
   686  		Bucket: &bucketName,
   687  	}
   688  
   689  	// Be clear about what we're doing in case the user needs to clean
   690  	// this up later.
   691  	t.Logf("creating S3 bucket %s in %s", bucketName, *s3Client.Config.Region)
   692  	_, err := s3Client.CreateBucket(createBucketReq)
   693  	if err != nil {
   694  		t.Fatal("failed to create test S3 bucket:", err)
   695  	}
   696  }
   697  
   698  func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
   699  	warning := "WARNING: Failed to delete the test S3 bucket. It may have been left in your AWS account and may incur storage charges. (error was %s)"
   700  
   701  	// first we have to get rid of the env objects, or we can't delete the bucket
   702  	resp, err := s3Client.ListObjects(&s3.ListObjectsInput{Bucket: &bucketName})
   703  	if err != nil {
   704  		t.Logf(warning, err)
   705  		return
   706  	}
   707  	for _, obj := range resp.Contents {
   708  		if _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{Bucket: &bucketName, Key: obj.Key}); err != nil {
   709  			// this will need cleanup no matter what, so just warn and exit
   710  			t.Logf(warning, err)
   711  			return
   712  		}
   713  	}
   714  
   715  	if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil {
   716  		t.Logf(warning, err)
   717  	}
   718  }
   719  
   720  // create the dynamoDB table, and wait until we can query it.
   721  func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
   722  	createInput := &dynamodb.CreateTableInput{
   723  		AttributeDefinitions: []*dynamodb.AttributeDefinition{
   724  			{
   725  				AttributeName: aws.String("LockID"),
   726  				AttributeType: aws.String("S"),
   727  			},
   728  		},
   729  		KeySchema: []*dynamodb.KeySchemaElement{
   730  			{
   731  				AttributeName: aws.String("LockID"),
   732  				KeyType:       aws.String("HASH"),
   733  			},
   734  		},
   735  		ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
   736  			ReadCapacityUnits:  aws.Int64(5),
   737  			WriteCapacityUnits: aws.Int64(5),
   738  		},
   739  		TableName: aws.String(tableName),
   740  	}
   741  
   742  	_, err := dynClient.CreateTable(createInput)
   743  	if err != nil {
   744  		t.Fatal(err)
   745  	}
   746  
   747  	// now wait until it's ACTIVE
   748  	start := time.Now()
   749  	time.Sleep(time.Second)
   750  
   751  	describeInput := &dynamodb.DescribeTableInput{
   752  		TableName: aws.String(tableName),
   753  	}
   754  
   755  	for {
   756  		resp, err := dynClient.DescribeTable(describeInput)
   757  		if err != nil {
   758  			t.Fatal(err)
   759  		}
   760  
   761  		if *resp.Table.TableStatus == "ACTIVE" {
   762  			return
   763  		}
   764  
   765  		if time.Since(start) > time.Minute {
   766  			t.Fatalf("timed out creating DynamoDB table %s", tableName)
   767  		}
   768  
   769  		time.Sleep(3 * time.Second)
   770  	}
   771  
   772  }
   773  
   774  func deleteDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
   775  	params := &dynamodb.DeleteTableInput{
   776  		TableName: aws.String(tableName),
   777  	}
   778  	_, err := dynClient.DeleteTable(params)
   779  	if err != nil {
   780  		t.Logf("WARNING: Failed to delete the test DynamoDB table %q. It has been left in your AWS account and may incur charges. (error was %s)", tableName, err)
   781  	}
   782  }