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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package s3
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"net/url"
    10  	"os"
    11  	"reflect"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/aws/aws-sdk-go/aws"
    17  	"github.com/aws/aws-sdk-go/service/dynamodb"
    18  	"github.com/aws/aws-sdk-go/service/s3"
    19  	"github.com/google/go-cmp/cmp"
    20  	awsbase "github.com/hashicorp/aws-sdk-go-base"
    21  	"github.com/terramate-io/tf/backend"
    22  	"github.com/terramate-io/tf/configs/configschema"
    23  	"github.com/terramate-io/tf/configs/hcl2shim"
    24  	"github.com/terramate-io/tf/states"
    25  	"github.com/terramate-io/tf/states/remote"
    26  	"github.com/terramate-io/tf/tfdiags"
    27  	"github.com/zclconf/go-cty/cty"
    28  )
    29  
    30  var (
    31  	mockStsGetCallerIdentityRequestBody = url.Values{
    32  		"Action":  []string{"GetCallerIdentity"},
    33  		"Version": []string{"2011-06-15"},
    34  	}.Encode()
    35  )
    36  
    37  // verify that we are doing ACC tests or the S3 tests specifically
    38  func testACC(t *testing.T) {
    39  	skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_S3_TEST") == ""
    40  	if skip {
    41  		t.Log("s3 backend tests require setting TF_ACC or TF_S3_TEST")
    42  		t.Skip()
    43  	}
    44  	if os.Getenv("AWS_DEFAULT_REGION") == "" {
    45  		os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
    46  	}
    47  }
    48  
    49  func TestBackend_impl(t *testing.T) {
    50  	var _ backend.Backend = new(Backend)
    51  }
    52  
    53  func TestBackendConfig_original(t *testing.T) {
    54  	testACC(t)
    55  	config := map[string]interface{}{
    56  		"region":         "us-west-1",
    57  		"bucket":         "tf-test",
    58  		"key":            "state",
    59  		"encrypt":        true,
    60  		"dynamodb_table": "dynamoTable",
    61  	}
    62  
    63  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
    64  
    65  	if aws.StringValue(b.s3Client.Config.Region) != "us-west-1" {
    66  		t.Fatalf("Incorrect region was populated")
    67  	}
    68  	if aws.IntValue(b.s3Client.Config.MaxRetries) != 5 {
    69  		t.Fatalf("Default max_retries was not set")
    70  	}
    71  	if b.bucketName != "tf-test" {
    72  		t.Fatalf("Incorrect bucketName was populated")
    73  	}
    74  	if b.keyName != "state" {
    75  		t.Fatalf("Incorrect keyName was populated")
    76  	}
    77  
    78  	checkClientEndpoint(t, b.s3Client.Config, "")
    79  
    80  	checkClientEndpoint(t, b.dynClient.Config, "")
    81  
    82  	credentials, err := b.s3Client.Config.Credentials.Get()
    83  	if err != nil {
    84  		t.Fatalf("Error when requesting credentials")
    85  	}
    86  	if credentials.AccessKeyID == "" {
    87  		t.Fatalf("No Access Key Id was populated")
    88  	}
    89  	if credentials.SecretAccessKey == "" {
    90  		t.Fatalf("No Secret Access Key was populated")
    91  	}
    92  }
    93  
    94  func checkClientEndpoint(t *testing.T, config aws.Config, expected string) {
    95  	if a := aws.StringValue(config.Endpoint); a != expected {
    96  		t.Errorf("expected endpoint %q, got %q", expected, a)
    97  	}
    98  }
    99  
   100  func TestBackendConfig_InvalidRegion(t *testing.T) {
   101  	testACC(t)
   102  
   103  	cases := map[string]struct {
   104  		config        map[string]any
   105  		expectedDiags tfdiags.Diagnostics
   106  	}{
   107  		"with region validation": {
   108  			config: map[string]interface{}{
   109  				"region":                      "nonesuch",
   110  				"bucket":                      "tf-test",
   111  				"key":                         "state",
   112  				"skip_credentials_validation": true,
   113  			},
   114  			expectedDiags: tfdiags.Diagnostics{
   115  				tfdiags.AttributeValue(
   116  					tfdiags.Error,
   117  					"Invalid region value",
   118  					`Invalid AWS Region: nonesuch`,
   119  					cty.Path{cty.GetAttrStep{Name: "region"}},
   120  				),
   121  			},
   122  		},
   123  		"skip region validation": {
   124  			config: map[string]interface{}{
   125  				"region":                      "nonesuch",
   126  				"bucket":                      "tf-test",
   127  				"key":                         "state",
   128  				"skip_region_validation":      true,
   129  				"skip_credentials_validation": true,
   130  			},
   131  			expectedDiags: nil,
   132  		},
   133  	}
   134  
   135  	for name, tc := range cases {
   136  		t.Run(name, func(t *testing.T) {
   137  			b := New()
   138  			configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(tc.config))
   139  
   140  			configSchema, diags := b.PrepareConfig(configSchema)
   141  			if len(diags) > 0 {
   142  				t.Fatal(diags.ErrWithWarnings())
   143  			}
   144  
   145  			confDiags := b.Configure(configSchema)
   146  			diags = diags.Append(confDiags)
   147  
   148  			if diff := cmp.Diff(diags, tc.expectedDiags, cmp.Comparer(diagnosticComparer)); diff != "" {
   149  				t.Errorf("unexpected diagnostics difference: %s", diff)
   150  			}
   151  		})
   152  	}
   153  }
   154  
   155  func TestBackendConfig_RegionEnvVar(t *testing.T) {
   156  	testACC(t)
   157  	config := map[string]interface{}{
   158  		"bucket": "tf-test",
   159  		"key":    "state",
   160  	}
   161  
   162  	cases := map[string]struct {
   163  		vars map[string]string
   164  	}{
   165  		"AWS_REGION": {
   166  			vars: map[string]string{
   167  				"AWS_REGION": "us-west-1",
   168  			},
   169  		},
   170  
   171  		"AWS_DEFAULT_REGION": {
   172  			vars: map[string]string{
   173  				"AWS_DEFAULT_REGION": "us-west-1",
   174  			},
   175  		},
   176  	}
   177  
   178  	for name, tc := range cases {
   179  		t.Run(name, func(t *testing.T) {
   180  			for k, v := range tc.vars {
   181  				os.Setenv(k, v)
   182  			}
   183  			t.Cleanup(func() {
   184  				for k := range tc.vars {
   185  					os.Unsetenv(k)
   186  				}
   187  			})
   188  
   189  			b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
   190  
   191  			if aws.StringValue(b.s3Client.Config.Region) != "us-west-1" {
   192  				t.Fatalf("Incorrect region was populated")
   193  			}
   194  		})
   195  	}
   196  }
   197  
   198  func TestBackendConfig_DynamoDBEndpoint(t *testing.T) {
   199  	testACC(t)
   200  
   201  	cases := map[string]struct {
   202  		config   map[string]any
   203  		vars     map[string]string
   204  		expected string
   205  	}{
   206  		"none": {
   207  			expected: "",
   208  		},
   209  		"config": {
   210  			config: map[string]any{
   211  				"dynamodb_endpoint": "dynamo.test",
   212  			},
   213  			expected: "dynamo.test",
   214  		},
   215  		"envvar": {
   216  			vars: map[string]string{
   217  				"AWS_DYNAMODB_ENDPOINT": "dynamo.test",
   218  			},
   219  			expected: "dynamo.test",
   220  		},
   221  	}
   222  
   223  	for name, tc := range cases {
   224  		t.Run(name, func(t *testing.T) {
   225  			config := map[string]interface{}{
   226  				"region": "us-west-1",
   227  				"bucket": "tf-test",
   228  				"key":    "state",
   229  			}
   230  
   231  			if tc.vars != nil {
   232  				for k, v := range tc.vars {
   233  					os.Setenv(k, v)
   234  				}
   235  				t.Cleanup(func() {
   236  					for k := range tc.vars {
   237  						os.Unsetenv(k)
   238  					}
   239  				})
   240  			}
   241  
   242  			if tc.config != nil {
   243  				for k, v := range tc.config {
   244  					config[k] = v
   245  				}
   246  			}
   247  
   248  			b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
   249  
   250  			checkClientEndpoint(t, b.dynClient.Config, tc.expected)
   251  		})
   252  	}
   253  }
   254  
   255  func TestBackendConfig_S3Endpoint(t *testing.T) {
   256  	testACC(t)
   257  
   258  	cases := map[string]struct {
   259  		config   map[string]any
   260  		vars     map[string]string
   261  		expected string
   262  	}{
   263  		"none": {
   264  			expected: "",
   265  		},
   266  		"config": {
   267  			config: map[string]any{
   268  				"endpoint": "s3.test",
   269  			},
   270  			expected: "s3.test",
   271  		},
   272  		"envvar": {
   273  			vars: map[string]string{
   274  				"AWS_S3_ENDPOINT": "s3.test",
   275  			},
   276  			expected: "s3.test",
   277  		},
   278  	}
   279  
   280  	for name, tc := range cases {
   281  		t.Run(name, func(t *testing.T) {
   282  			config := map[string]interface{}{
   283  				"region": "us-west-1",
   284  				"bucket": "tf-test",
   285  				"key":    "state",
   286  			}
   287  
   288  			if tc.vars != nil {
   289  				for k, v := range tc.vars {
   290  					os.Setenv(k, v)
   291  				}
   292  				t.Cleanup(func() {
   293  					for k := range tc.vars {
   294  						os.Unsetenv(k)
   295  					}
   296  				})
   297  			}
   298  
   299  			if tc.config != nil {
   300  				for k, v := range tc.config {
   301  					config[k] = v
   302  				}
   303  			}
   304  
   305  			b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
   306  
   307  			checkClientEndpoint(t, b.s3Client.Config, tc.expected)
   308  		})
   309  	}
   310  }
   311  
   312  func TestBackendConfig_STSEndpoint(t *testing.T) {
   313  	testACC(t)
   314  
   315  	stsMocks := []*awsbase.MockEndpoint{
   316  		{
   317  			Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   318  				"Action":          []string{"AssumeRole"},
   319  				"DurationSeconds": []string{"900"},
   320  				"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   321  				"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   322  				"Version":         []string{"2011-06-15"},
   323  			}.Encode()},
   324  			Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   325  		},
   326  		{
   327  			Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   328  			Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   329  		},
   330  	}
   331  
   332  	cases := map[string]struct {
   333  		setConfig     bool
   334  		setEnvVars    bool
   335  		expectedDiags tfdiags.Diagnostics
   336  	}{
   337  		"none": {
   338  			expectedDiags: tfdiags.Diagnostics{
   339  				tfdiags.Sourceless(
   340  					tfdiags.Error,
   341  					"Failed to configure AWS client",
   342  					``,
   343  				),
   344  			},
   345  		},
   346  		"config": {
   347  			setConfig: true,
   348  		},
   349  		"envvar": {
   350  			setEnvVars: true,
   351  		},
   352  	}
   353  
   354  	for name, tc := range cases {
   355  		t.Run(name, func(t *testing.T) {
   356  			config := map[string]interface{}{
   357  				"region":       "us-west-1",
   358  				"bucket":       "tf-test",
   359  				"key":          "state",
   360  				"role_arn":     awsbase.MockStsAssumeRoleArn,
   361  				"session_name": awsbase.MockStsAssumeRoleSessionName,
   362  			}
   363  
   364  			closeSts, mockStsSession, err := awsbase.GetMockedAwsApiSession("STS", stsMocks)
   365  			if err != nil {
   366  				t.Fatalf("unexpected error creating mock STS server: %s", err)
   367  			}
   368  			defer closeSts()
   369  
   370  			if tc.setEnvVars {
   371  				os.Setenv("AWS_STS_ENDPOINT", aws.StringValue(mockStsSession.Config.Endpoint))
   372  				t.Cleanup(func() {
   373  					os.Unsetenv("AWS_STS_ENDPOINT")
   374  				})
   375  			}
   376  
   377  			if tc.setConfig {
   378  				config["sts_endpoint"] = aws.StringValue(mockStsSession.Config.Endpoint)
   379  			}
   380  
   381  			b := New()
   382  			configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config))
   383  
   384  			configSchema, diags := b.PrepareConfig(configSchema)
   385  			if len(diags) > 0 {
   386  				t.Fatal(diags.ErrWithWarnings())
   387  			}
   388  
   389  			confDiags := b.Configure(configSchema)
   390  			diags = diags.Append(confDiags)
   391  
   392  			if diff := cmp.Diff(diags, tc.expectedDiags, cmp.Comparer(diagnosticSummaryComparer)); diff != "" {
   393  				t.Errorf("unexpected diagnostics difference: %s", diff)
   394  			}
   395  		})
   396  	}
   397  }
   398  
   399  func TestBackendConfig_AssumeRole(t *testing.T) {
   400  	testACC(t)
   401  
   402  	testCases := []struct {
   403  		Config           map[string]interface{}
   404  		Description      string
   405  		MockStsEndpoints []*awsbase.MockEndpoint
   406  	}{
   407  		{
   408  			Config: map[string]interface{}{
   409  				"bucket":       "tf-test",
   410  				"key":          "state",
   411  				"region":       "us-west-1",
   412  				"role_arn":     awsbase.MockStsAssumeRoleArn,
   413  				"session_name": awsbase.MockStsAssumeRoleSessionName,
   414  			},
   415  			Description: "role_arn",
   416  			MockStsEndpoints: []*awsbase.MockEndpoint{
   417  				{
   418  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   419  						"Action":          []string{"AssumeRole"},
   420  						"DurationSeconds": []string{"900"},
   421  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   422  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   423  						"Version":         []string{"2011-06-15"},
   424  					}.Encode()},
   425  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   426  				},
   427  				{
   428  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   429  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   430  				},
   431  			},
   432  		},
   433  		{
   434  			Config: map[string]interface{}{
   435  				"assume_role_duration_seconds": 3600,
   436  				"bucket":                       "tf-test",
   437  				"key":                          "state",
   438  				"region":                       "us-west-1",
   439  				"role_arn":                     awsbase.MockStsAssumeRoleArn,
   440  				"session_name":                 awsbase.MockStsAssumeRoleSessionName,
   441  			},
   442  			Description: "assume_role_duration_seconds",
   443  			MockStsEndpoints: []*awsbase.MockEndpoint{
   444  				{
   445  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   446  						"Action":          []string{"AssumeRole"},
   447  						"DurationSeconds": []string{"3600"},
   448  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   449  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   450  						"Version":         []string{"2011-06-15"},
   451  					}.Encode()},
   452  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   453  				},
   454  				{
   455  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   456  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   457  				},
   458  			},
   459  		},
   460  		{
   461  			Config: map[string]interface{}{
   462  				"bucket":       "tf-test",
   463  				"external_id":  awsbase.MockStsAssumeRoleExternalId,
   464  				"key":          "state",
   465  				"region":       "us-west-1",
   466  				"role_arn":     awsbase.MockStsAssumeRoleArn,
   467  				"session_name": awsbase.MockStsAssumeRoleSessionName,
   468  			},
   469  			Description: "external_id",
   470  			MockStsEndpoints: []*awsbase.MockEndpoint{
   471  				{
   472  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   473  						"Action":          []string{"AssumeRole"},
   474  						"DurationSeconds": []string{"900"},
   475  						"ExternalId":      []string{awsbase.MockStsAssumeRoleExternalId},
   476  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   477  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   478  						"Version":         []string{"2011-06-15"},
   479  					}.Encode()},
   480  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   481  				},
   482  				{
   483  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   484  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   485  				},
   486  			},
   487  		},
   488  		{
   489  			Config: map[string]interface{}{
   490  				"assume_role_policy": awsbase.MockStsAssumeRolePolicy,
   491  				"bucket":             "tf-test",
   492  				"key":                "state",
   493  				"region":             "us-west-1",
   494  				"role_arn":           awsbase.MockStsAssumeRoleArn,
   495  				"session_name":       awsbase.MockStsAssumeRoleSessionName,
   496  			},
   497  			Description: "assume_role_policy",
   498  			MockStsEndpoints: []*awsbase.MockEndpoint{
   499  				{
   500  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   501  						"Action":          []string{"AssumeRole"},
   502  						"DurationSeconds": []string{"900"},
   503  						"Policy":          []string{awsbase.MockStsAssumeRolePolicy},
   504  						"RoleArn":         []string{awsbase.MockStsAssumeRoleArn},
   505  						"RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName},
   506  						"Version":         []string{"2011-06-15"},
   507  					}.Encode()},
   508  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   509  				},
   510  				{
   511  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   512  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   513  				},
   514  			},
   515  		},
   516  		{
   517  			Config: map[string]interface{}{
   518  				"assume_role_policy_arns": []interface{}{awsbase.MockStsAssumeRolePolicyArn},
   519  				"bucket":                  "tf-test",
   520  				"key":                     "state",
   521  				"region":                  "us-west-1",
   522  				"role_arn":                awsbase.MockStsAssumeRoleArn,
   523  				"session_name":            awsbase.MockStsAssumeRoleSessionName,
   524  			},
   525  			Description: "assume_role_policy_arns",
   526  			MockStsEndpoints: []*awsbase.MockEndpoint{
   527  				{
   528  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   529  						"Action":                  []string{"AssumeRole"},
   530  						"DurationSeconds":         []string{"900"},
   531  						"PolicyArns.member.1.arn": []string{awsbase.MockStsAssumeRolePolicyArn},
   532  						"RoleArn":                 []string{awsbase.MockStsAssumeRoleArn},
   533  						"RoleSessionName":         []string{awsbase.MockStsAssumeRoleSessionName},
   534  						"Version":                 []string{"2011-06-15"},
   535  					}.Encode()},
   536  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   537  				},
   538  				{
   539  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   540  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   541  				},
   542  			},
   543  		},
   544  		{
   545  			Config: map[string]interface{}{
   546  				"assume_role_tags": map[string]interface{}{
   547  					awsbase.MockStsAssumeRoleTagKey: awsbase.MockStsAssumeRoleTagValue,
   548  				},
   549  				"bucket":       "tf-test",
   550  				"key":          "state",
   551  				"region":       "us-west-1",
   552  				"role_arn":     awsbase.MockStsAssumeRoleArn,
   553  				"session_name": awsbase.MockStsAssumeRoleSessionName,
   554  			},
   555  			Description: "assume_role_tags",
   556  			MockStsEndpoints: []*awsbase.MockEndpoint{
   557  				{
   558  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   559  						"Action":              []string{"AssumeRole"},
   560  						"DurationSeconds":     []string{"900"},
   561  						"RoleArn":             []string{awsbase.MockStsAssumeRoleArn},
   562  						"RoleSessionName":     []string{awsbase.MockStsAssumeRoleSessionName},
   563  						"Tags.member.1.Key":   []string{awsbase.MockStsAssumeRoleTagKey},
   564  						"Tags.member.1.Value": []string{awsbase.MockStsAssumeRoleTagValue},
   565  						"Version":             []string{"2011-06-15"},
   566  					}.Encode()},
   567  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   568  				},
   569  				{
   570  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   571  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   572  				},
   573  			},
   574  		},
   575  		{
   576  			Config: map[string]interface{}{
   577  				"assume_role_tags": map[string]interface{}{
   578  					awsbase.MockStsAssumeRoleTagKey: awsbase.MockStsAssumeRoleTagValue,
   579  				},
   580  				"assume_role_transitive_tag_keys": []interface{}{awsbase.MockStsAssumeRoleTagKey},
   581  				"bucket":                          "tf-test",
   582  				"key":                             "state",
   583  				"region":                          "us-west-1",
   584  				"role_arn":                        awsbase.MockStsAssumeRoleArn,
   585  				"session_name":                    awsbase.MockStsAssumeRoleSessionName,
   586  			},
   587  			Description: "assume_role_transitive_tag_keys",
   588  			MockStsEndpoints: []*awsbase.MockEndpoint{
   589  				{
   590  					Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   591  						"Action":                     []string{"AssumeRole"},
   592  						"DurationSeconds":            []string{"900"},
   593  						"RoleArn":                    []string{awsbase.MockStsAssumeRoleArn},
   594  						"RoleSessionName":            []string{awsbase.MockStsAssumeRoleSessionName},
   595  						"Tags.member.1.Key":          []string{awsbase.MockStsAssumeRoleTagKey},
   596  						"Tags.member.1.Value":        []string{awsbase.MockStsAssumeRoleTagValue},
   597  						"TransitiveTagKeys.member.1": []string{awsbase.MockStsAssumeRoleTagKey},
   598  						"Version":                    []string{"2011-06-15"},
   599  					}.Encode()},
   600  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   601  				},
   602  				{
   603  					Request:  &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   604  					Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   605  				},
   606  			},
   607  		},
   608  	}
   609  
   610  	for _, testCase := range testCases {
   611  		testCase := testCase
   612  
   613  		t.Run(testCase.Description, func(t *testing.T) {
   614  			closeSts, mockStsSession, err := awsbase.GetMockedAwsApiSession("STS", testCase.MockStsEndpoints)
   615  			defer closeSts()
   616  
   617  			if err != nil {
   618  				t.Fatalf("unexpected error creating mock STS server: %s", err)
   619  			}
   620  
   621  			if mockStsSession != nil && mockStsSession.Config != nil {
   622  				testCase.Config["sts_endpoint"] = aws.StringValue(mockStsSession.Config.Endpoint)
   623  			}
   624  
   625  			b := New()
   626  			diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(testCase.Config)))
   627  
   628  			if diags.HasErrors() {
   629  				for _, diag := range diags {
   630  					t.Errorf("unexpected error: %s", diag.Description().Summary)
   631  				}
   632  			}
   633  		})
   634  	}
   635  }
   636  
   637  func TestBackendConfig_PrepareConfigValidation(t *testing.T) {
   638  	cases := map[string]struct {
   639  		config      cty.Value
   640  		expectedErr string
   641  	}{
   642  		"null bucket": {
   643  			config: cty.ObjectVal(map[string]cty.Value{
   644  				"bucket": cty.NullVal(cty.String),
   645  				"key":    cty.StringVal("test"),
   646  				"region": cty.StringVal("us-west-2"),
   647  			}),
   648  			expectedErr: `The "bucket" attribute value must not be empty.`,
   649  		},
   650  		"empty bucket": {
   651  			config: cty.ObjectVal(map[string]cty.Value{
   652  				"bucket": cty.StringVal(""),
   653  				"key":    cty.StringVal("test"),
   654  				"region": cty.StringVal("us-west-2"),
   655  			}),
   656  			expectedErr: `The "bucket" attribute value must not be empty.`,
   657  		},
   658  		"null key": {
   659  			config: cty.ObjectVal(map[string]cty.Value{
   660  				"bucket": cty.StringVal("test"),
   661  				"key":    cty.NullVal(cty.String),
   662  				"region": cty.StringVal("us-west-2"),
   663  			}),
   664  			expectedErr: `The "key" attribute value must not be empty.`,
   665  		},
   666  		"empty key": {
   667  			config: cty.ObjectVal(map[string]cty.Value{
   668  				"bucket": cty.StringVal("test"),
   669  				"key":    cty.StringVal(""),
   670  				"region": cty.StringVal("us-west-2"),
   671  			}),
   672  			expectedErr: `The "key" attribute value must not be empty.`,
   673  		},
   674  		"key with leading slash": {
   675  			config: cty.ObjectVal(map[string]cty.Value{
   676  				"bucket": cty.StringVal("test"),
   677  				"key":    cty.StringVal("/leading-slash"),
   678  				"region": cty.StringVal("us-west-2"),
   679  			}),
   680  			expectedErr: `The "key" attribute value must not start or end with with "/".`,
   681  		},
   682  		"key with trailing slash": {
   683  			config: cty.ObjectVal(map[string]cty.Value{
   684  				"bucket": cty.StringVal("test"),
   685  				"key":    cty.StringVal("trailing-slash/"),
   686  				"region": cty.StringVal("us-west-2"),
   687  			}),
   688  			expectedErr: `The "key" attribute value must not start or end with with "/".`,
   689  		},
   690  		"null region": {
   691  			config: cty.ObjectVal(map[string]cty.Value{
   692  				"bucket": cty.StringVal("test"),
   693  				"key":    cty.StringVal("test"),
   694  				"region": cty.NullVal(cty.String),
   695  			}),
   696  			expectedErr: `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`,
   697  		},
   698  		"empty region": {
   699  			config: cty.ObjectVal(map[string]cty.Value{
   700  				"bucket": cty.StringVal("test"),
   701  				"key":    cty.StringVal("test"),
   702  				"region": cty.StringVal(""),
   703  			}),
   704  			expectedErr: `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`,
   705  		},
   706  		"workspace_key_prefix with leading slash": {
   707  			config: cty.ObjectVal(map[string]cty.Value{
   708  				"bucket":               cty.StringVal("test"),
   709  				"key":                  cty.StringVal("test"),
   710  				"region":               cty.StringVal("us-west-2"),
   711  				"workspace_key_prefix": cty.StringVal("/env"),
   712  			}),
   713  			expectedErr: `The "workspace_key_prefix" attribute value must not start with "/".`,
   714  		},
   715  		"workspace_key_prefix with trailing slash": {
   716  			config: cty.ObjectVal(map[string]cty.Value{
   717  				"bucket":               cty.StringVal("test"),
   718  				"key":                  cty.StringVal("test"),
   719  				"region":               cty.StringVal("us-west-2"),
   720  				"workspace_key_prefix": cty.StringVal("env/"),
   721  			}),
   722  			expectedErr: `The "workspace_key_prefix" attribute value must not start with "/".`,
   723  		},
   724  		"encyrption key conflict": {
   725  			config: cty.ObjectVal(map[string]cty.Value{
   726  				"bucket":               cty.StringVal("test"),
   727  				"key":                  cty.StringVal("test"),
   728  				"region":               cty.StringVal("us-west-2"),
   729  				"workspace_key_prefix": cty.StringVal("env"),
   730  				"sse_customer_key":     cty.StringVal("1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o="),
   731  				"kms_key_id":           cty.StringVal("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"),
   732  			}),
   733  			expectedErr: `Only one of "kms_key_id" and "sse_customer_key" can be set`,
   734  		},
   735  	}
   736  
   737  	for name, tc := range cases {
   738  		t.Run(name, func(t *testing.T) {
   739  			oldEnv := stashEnv()
   740  			defer popEnv(oldEnv)
   741  
   742  			b := New()
   743  
   744  			_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))
   745  			if tc.expectedErr != "" {
   746  				if valDiags.Err() != nil {
   747  					actualErr := valDiags.Err().Error()
   748  					if !strings.Contains(actualErr, tc.expectedErr) {
   749  						t.Fatalf("unexpected validation result: %v", valDiags.Err())
   750  					}
   751  				} else {
   752  					t.Fatal("expected an error, got none")
   753  				}
   754  			} else if valDiags.Err() != nil {
   755  				t.Fatalf("expected no error, got %s", valDiags.Err())
   756  			}
   757  		})
   758  	}
   759  }
   760  
   761  func TestBackendConfig_PrepareConfigWithEnvVars(t *testing.T) {
   762  	cases := map[string]struct {
   763  		config      cty.Value
   764  		vars        map[string]string
   765  		expectedErr string
   766  	}{
   767  		"region env var AWS_REGION": {
   768  			config: cty.ObjectVal(map[string]cty.Value{
   769  				"bucket": cty.StringVal("test"),
   770  				"key":    cty.StringVal("test"),
   771  				"region": cty.NullVal(cty.String),
   772  			}),
   773  			vars: map[string]string{
   774  				"AWS_REGION": "us-west-1",
   775  			},
   776  		},
   777  		"region env var AWS_DEFAULT_REGION": {
   778  			config: cty.ObjectVal(map[string]cty.Value{
   779  				"bucket": cty.StringVal("test"),
   780  				"key":    cty.StringVal("test"),
   781  				"region": cty.NullVal(cty.String),
   782  			}),
   783  			vars: map[string]string{
   784  				"AWS_DEFAULT_REGION": "us-west-1",
   785  			},
   786  		},
   787  		"encyrption key conflict": {
   788  			config: cty.ObjectVal(map[string]cty.Value{
   789  				"bucket":               cty.StringVal("test"),
   790  				"key":                  cty.StringVal("test"),
   791  				"region":               cty.StringVal("us-west-2"),
   792  				"workspace_key_prefix": cty.StringVal("env"),
   793  				"kms_key_id":           cty.StringVal("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"),
   794  			}),
   795  			vars: map[string]string{
   796  				"AWS_SSE_CUSTOMER_KEY": "1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o=",
   797  			},
   798  			expectedErr: `Only one of "kms_key_id" and the environment variable "AWS_SSE_CUSTOMER_KEY" can be set`,
   799  		},
   800  	}
   801  
   802  	for name, tc := range cases {
   803  		t.Run(name, func(t *testing.T) {
   804  			oldEnv := stashEnv()
   805  			defer popEnv(oldEnv)
   806  
   807  			b := New()
   808  
   809  			for k, v := range tc.vars {
   810  				os.Setenv(k, v)
   811  			}
   812  
   813  			_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))
   814  			if tc.expectedErr != "" {
   815  				if valDiags.Err() != nil {
   816  					actualErr := valDiags.Err().Error()
   817  					if !strings.Contains(actualErr, tc.expectedErr) {
   818  						t.Fatalf("unexpected validation result: %v", valDiags.Err())
   819  					}
   820  				} else {
   821  					t.Fatal("expected an error, got none")
   822  				}
   823  			} else if valDiags.Err() != nil {
   824  				t.Fatalf("expected no error, got %s", valDiags.Err())
   825  			}
   826  		})
   827  	}
   828  }
   829  
   830  func TestBackend(t *testing.T) {
   831  	testACC(t)
   832  
   833  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   834  	keyName := "testState"
   835  
   836  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   837  		"bucket":  bucketName,
   838  		"key":     keyName,
   839  		"encrypt": true,
   840  		"region":  "us-west-1",
   841  	})).(*Backend)
   842  
   843  	createS3Bucket(t, b.s3Client, bucketName)
   844  	defer deleteS3Bucket(t, b.s3Client, bucketName)
   845  
   846  	backend.TestBackendStates(t, b)
   847  }
   848  
   849  func TestBackendLocked(t *testing.T) {
   850  	testACC(t)
   851  
   852  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   853  	keyName := "test/state"
   854  
   855  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   856  		"bucket":         bucketName,
   857  		"key":            keyName,
   858  		"encrypt":        true,
   859  		"dynamodb_table": bucketName,
   860  		"region":         "us-west-1",
   861  	})).(*Backend)
   862  
   863  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   864  		"bucket":         bucketName,
   865  		"key":            keyName,
   866  		"encrypt":        true,
   867  		"dynamodb_table": bucketName,
   868  		"region":         "us-west-1",
   869  	})).(*Backend)
   870  
   871  	createS3Bucket(t, b1.s3Client, bucketName)
   872  	defer deleteS3Bucket(t, b1.s3Client, bucketName)
   873  	createDynamoDBTable(t, b1.dynClient, bucketName)
   874  	defer deleteDynamoDBTable(t, b1.dynClient, bucketName)
   875  
   876  	backend.TestBackendStateLocks(t, b1, b2)
   877  	backend.TestBackendStateForceUnlock(t, b1, b2)
   878  }
   879  
   880  func TestBackendSSECustomerKeyConfig(t *testing.T) {
   881  	testACC(t)
   882  
   883  	testCases := map[string]struct {
   884  		customerKey string
   885  		expectedErr string
   886  	}{
   887  		"invalid length": {
   888  			customerKey: "test",
   889  			expectedErr: `sse_customer_key must be 44 characters in length`,
   890  		},
   891  		"invalid encoding": {
   892  			customerKey: "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka",
   893  			expectedErr: `sse_customer_key must be base64 encoded`,
   894  		},
   895  		"valid": {
   896  			customerKey: "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
   897  		},
   898  	}
   899  
   900  	for name, testCase := range testCases {
   901  		testCase := testCase
   902  
   903  		t.Run(name, func(t *testing.T) {
   904  			bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   905  			config := map[string]interface{}{
   906  				"bucket":           bucketName,
   907  				"encrypt":          true,
   908  				"key":              "test-SSE-C",
   909  				"sse_customer_key": testCase.customerKey,
   910  				"region":           "us-west-1",
   911  			}
   912  
   913  			b := New().(*Backend)
   914  			diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config)))
   915  
   916  			if testCase.expectedErr != "" {
   917  				if diags.Err() != nil {
   918  					actualErr := diags.Err().Error()
   919  					if !strings.Contains(actualErr, testCase.expectedErr) {
   920  						t.Fatalf("unexpected validation result: %v", diags.Err())
   921  					}
   922  				} else {
   923  					t.Fatal("expected an error, got none")
   924  				}
   925  			} else {
   926  				if diags.Err() != nil {
   927  					t.Fatalf("expected no error, got %s", diags.Err())
   928  				}
   929  				if string(b.customerEncryptionKey) != string(must(base64.StdEncoding.DecodeString(testCase.customerKey))) {
   930  					t.Fatal("unexpected value for customer encryption key")
   931  				}
   932  
   933  				createS3Bucket(t, b.s3Client, bucketName)
   934  				defer deleteS3Bucket(t, b.s3Client, bucketName)
   935  
   936  				backend.TestBackendStates(t, b)
   937  			}
   938  		})
   939  	}
   940  }
   941  
   942  func TestBackendSSECustomerKeyEnvVar(t *testing.T) {
   943  	testACC(t)
   944  
   945  	testCases := map[string]struct {
   946  		customerKey string
   947  		expectedErr string
   948  	}{
   949  		"invalid length": {
   950  			customerKey: "test",
   951  			expectedErr: `The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`,
   952  		},
   953  		"invalid encoding": {
   954  			customerKey: "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka",
   955  			expectedErr: `The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded`,
   956  		},
   957  		"valid": {
   958  			customerKey: "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
   959  		},
   960  	}
   961  
   962  	for name, testCase := range testCases {
   963  		testCase := testCase
   964  
   965  		t.Run(name, func(t *testing.T) {
   966  			bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
   967  			config := map[string]interface{}{
   968  				"bucket":  bucketName,
   969  				"encrypt": true,
   970  				"key":     "test-SSE-C",
   971  				"region":  "us-west-1",
   972  			}
   973  
   974  			os.Setenv("AWS_SSE_CUSTOMER_KEY", testCase.customerKey)
   975  			t.Cleanup(func() {
   976  				os.Unsetenv("AWS_SSE_CUSTOMER_KEY")
   977  			})
   978  
   979  			b := New().(*Backend)
   980  			diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config)))
   981  
   982  			if testCase.expectedErr != "" {
   983  				if diags.Err() != nil {
   984  					actualErr := diags.Err().Error()
   985  					if !strings.Contains(actualErr, testCase.expectedErr) {
   986  						t.Fatalf("unexpected validation result: %v", diags.Err())
   987  					}
   988  				} else {
   989  					t.Fatal("expected an error, got none")
   990  				}
   991  			} else {
   992  				if diags.Err() != nil {
   993  					t.Fatalf("expected no error, got %s", diags.Err())
   994  				}
   995  				if string(b.customerEncryptionKey) != string(must(base64.StdEncoding.DecodeString(testCase.customerKey))) {
   996  					t.Fatal("unexpected value for customer encryption key")
   997  				}
   998  
   999  				createS3Bucket(t, b.s3Client, bucketName)
  1000  				defer deleteS3Bucket(t, b.s3Client, bucketName)
  1001  
  1002  				backend.TestBackendStates(t, b)
  1003  			}
  1004  		})
  1005  	}
  1006  }
  1007  
  1008  // add some extra junk in S3 to try and confuse the env listing.
  1009  func TestBackendExtraPaths(t *testing.T) {
  1010  	testACC(t)
  1011  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
  1012  	keyName := "test/state/tfstate"
  1013  
  1014  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
  1015  		"bucket":  bucketName,
  1016  		"key":     keyName,
  1017  		"encrypt": true,
  1018  	})).(*Backend)
  1019  
  1020  	createS3Bucket(t, b.s3Client, bucketName)
  1021  	defer deleteS3Bucket(t, b.s3Client, bucketName)
  1022  
  1023  	// put multiple states in old env paths.
  1024  	s1 := states.NewState()
  1025  	s2 := states.NewState()
  1026  
  1027  	// RemoteClient to Put things in various paths
  1028  	client := &RemoteClient{
  1029  		s3Client:             b.s3Client,
  1030  		dynClient:            b.dynClient,
  1031  		bucketName:           b.bucketName,
  1032  		path:                 b.path("s1"),
  1033  		serverSideEncryption: b.serverSideEncryption,
  1034  		acl:                  b.acl,
  1035  		kmsKeyID:             b.kmsKeyID,
  1036  		ddbTable:             b.ddbTable,
  1037  	}
  1038  
  1039  	// Write the first state
  1040  	stateMgr := &remote.State{Client: client}
  1041  	if err := stateMgr.WriteState(s1); err != nil {
  1042  		t.Fatal(err)
  1043  	}
  1044  	if err := stateMgr.PersistState(nil); err != nil {
  1045  		t.Fatal(err)
  1046  	}
  1047  
  1048  	// Write the second state
  1049  	// Note a new state manager - otherwise, because these
  1050  	// states are equal, the state will not Put to the remote
  1051  	client.path = b.path("s2")
  1052  	stateMgr2 := &remote.State{Client: client}
  1053  	if err := stateMgr2.WriteState(s2); err != nil {
  1054  		t.Fatal(err)
  1055  	}
  1056  	if err := stateMgr2.PersistState(nil); err != nil {
  1057  		t.Fatal(err)
  1058  	}
  1059  
  1060  	s2Lineage := stateMgr2.StateSnapshotMeta().Lineage
  1061  
  1062  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1063  		t.Fatal(err)
  1064  	}
  1065  
  1066  	// put a state in an env directory name
  1067  	client.path = b.workspaceKeyPrefix + "/error"
  1068  	if err := stateMgr.WriteState(states.NewState()); err != nil {
  1069  		t.Fatal(err)
  1070  	}
  1071  	if err := stateMgr.PersistState(nil); err != nil {
  1072  		t.Fatal(err)
  1073  	}
  1074  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1075  		t.Fatal(err)
  1076  	}
  1077  
  1078  	// add state with the wrong key for an existing env
  1079  	client.path = b.workspaceKeyPrefix + "/s2/notTestState"
  1080  	if err := stateMgr.WriteState(states.NewState()); err != nil {
  1081  		t.Fatal(err)
  1082  	}
  1083  	if err := stateMgr.PersistState(nil); err != nil {
  1084  		t.Fatal(err)
  1085  	}
  1086  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1087  		t.Fatal(err)
  1088  	}
  1089  
  1090  	// remove the state with extra subkey
  1091  	if err := client.Delete(); err != nil {
  1092  		t.Fatal(err)
  1093  	}
  1094  
  1095  	// delete the real workspace
  1096  	if err := b.DeleteWorkspace("s2", true); err != nil {
  1097  		t.Fatal(err)
  1098  	}
  1099  
  1100  	if err := checkStateList(b, []string{"default", "s1"}); err != nil {
  1101  		t.Fatal(err)
  1102  	}
  1103  
  1104  	// fetch that state again, which should produce a new lineage
  1105  	s2Mgr, err := b.StateMgr("s2")
  1106  	if err != nil {
  1107  		t.Fatal(err)
  1108  	}
  1109  	if err := s2Mgr.RefreshState(); err != nil {
  1110  		t.Fatal(err)
  1111  	}
  1112  
  1113  	if s2Mgr.(*remote.State).StateSnapshotMeta().Lineage == s2Lineage {
  1114  		t.Fatal("state s2 was not deleted")
  1115  	}
  1116  	_ = s2Mgr.State() // We need the side-effect
  1117  	s2Lineage = stateMgr.StateSnapshotMeta().Lineage
  1118  
  1119  	// add a state with a key that matches an existing environment dir name
  1120  	client.path = b.workspaceKeyPrefix + "/s2/"
  1121  	if err := stateMgr.WriteState(states.NewState()); err != nil {
  1122  		t.Fatal(err)
  1123  	}
  1124  	if err := stateMgr.PersistState(nil); err != nil {
  1125  		t.Fatal(err)
  1126  	}
  1127  
  1128  	// make sure s2 is OK
  1129  	s2Mgr, err = b.StateMgr("s2")
  1130  	if err != nil {
  1131  		t.Fatal(err)
  1132  	}
  1133  	if err := s2Mgr.RefreshState(); err != nil {
  1134  		t.Fatal(err)
  1135  	}
  1136  
  1137  	if stateMgr.StateSnapshotMeta().Lineage != s2Lineage {
  1138  		t.Fatal("we got the wrong state for s2")
  1139  	}
  1140  
  1141  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1142  		t.Fatal(err)
  1143  	}
  1144  }
  1145  
  1146  // ensure we can separate the workspace prefix when it also matches the prefix
  1147  // of the workspace name itself.
  1148  func TestBackendPrefixInWorkspace(t *testing.T) {
  1149  	testACC(t)
  1150  	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
  1151  
  1152  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
  1153  		"bucket":               bucketName,
  1154  		"key":                  "test-env.tfstate",
  1155  		"workspace_key_prefix": "env",
  1156  	})).(*Backend)
  1157  
  1158  	createS3Bucket(t, b.s3Client, bucketName)
  1159  	defer deleteS3Bucket(t, b.s3Client, bucketName)
  1160  
  1161  	// get a state that contains the prefix as a substring
  1162  	sMgr, err := b.StateMgr("env-1")
  1163  	if err != nil {
  1164  		t.Fatal(err)
  1165  	}
  1166  	if err := sMgr.RefreshState(); err != nil {
  1167  		t.Fatal(err)
  1168  	}
  1169  
  1170  	if err := checkStateList(b, []string{"default", "env-1"}); err != nil {
  1171  		t.Fatal(err)
  1172  	}
  1173  }
  1174  
  1175  func TestKeyEnv(t *testing.T) {
  1176  	testACC(t)
  1177  	keyName := "some/paths/tfstate"
  1178  
  1179  	bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix())
  1180  	b0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
  1181  		"bucket":               bucket0Name,
  1182  		"key":                  keyName,
  1183  		"encrypt":              true,
  1184  		"workspace_key_prefix": "",
  1185  	})).(*Backend)
  1186  
  1187  	createS3Bucket(t, b0.s3Client, bucket0Name)
  1188  	defer deleteS3Bucket(t, b0.s3Client, bucket0Name)
  1189  
  1190  	bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix())
  1191  	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
  1192  		"bucket":               bucket1Name,
  1193  		"key":                  keyName,
  1194  		"encrypt":              true,
  1195  		"workspace_key_prefix": "project/env:",
  1196  	})).(*Backend)
  1197  
  1198  	createS3Bucket(t, b1.s3Client, bucket1Name)
  1199  	defer deleteS3Bucket(t, b1.s3Client, bucket1Name)
  1200  
  1201  	bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix())
  1202  	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
  1203  		"bucket":  bucket2Name,
  1204  		"key":     keyName,
  1205  		"encrypt": true,
  1206  	})).(*Backend)
  1207  
  1208  	createS3Bucket(t, b2.s3Client, bucket2Name)
  1209  	defer deleteS3Bucket(t, b2.s3Client, bucket2Name)
  1210  
  1211  	if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil {
  1212  		t.Fatal(err)
  1213  	}
  1214  
  1215  	if err := testGetWorkspaceForKey(b0, "ws1/some/paths/tfstate", "ws1"); err != nil {
  1216  		t.Fatal(err)
  1217  	}
  1218  
  1219  	if err := testGetWorkspaceForKey(b1, "project/env:/ws1/some/paths/tfstate", "ws1"); err != nil {
  1220  		t.Fatal(err)
  1221  	}
  1222  
  1223  	if err := testGetWorkspaceForKey(b1, "project/env:/ws2/some/paths/tfstate", "ws2"); err != nil {
  1224  		t.Fatal(err)
  1225  	}
  1226  
  1227  	if err := testGetWorkspaceForKey(b2, "env:/ws3/some/paths/tfstate", "ws3"); err != nil {
  1228  		t.Fatal(err)
  1229  	}
  1230  
  1231  	backend.TestBackendStates(t, b0)
  1232  	backend.TestBackendStates(t, b1)
  1233  	backend.TestBackendStates(t, b2)
  1234  }
  1235  
  1236  func testGetWorkspaceForKey(b *Backend, key string, expected string) error {
  1237  	if actual := b.keyEnv(key); actual != expected {
  1238  		return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual)
  1239  	}
  1240  	return nil
  1241  }
  1242  
  1243  func checkStateList(b backend.Backend, expected []string) error {
  1244  	states, err := b.Workspaces()
  1245  	if err != nil {
  1246  		return err
  1247  	}
  1248  
  1249  	if !reflect.DeepEqual(states, expected) {
  1250  		return fmt.Errorf("incorrect states listed: %q", states)
  1251  	}
  1252  	return nil
  1253  }
  1254  
  1255  func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
  1256  	createBucketReq := &s3.CreateBucketInput{
  1257  		Bucket: &bucketName,
  1258  	}
  1259  
  1260  	// Be clear about what we're doing in case the user needs to clean
  1261  	// this up later.
  1262  	t.Logf("creating S3 bucket %s in %s", bucketName, aws.StringValue(s3Client.Config.Region))
  1263  	_, err := s3Client.CreateBucket(createBucketReq)
  1264  	if err != nil {
  1265  		t.Fatal("failed to create test S3 bucket:", err)
  1266  	}
  1267  }
  1268  
  1269  func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
  1270  	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)"
  1271  
  1272  	// first we have to get rid of the env objects, or we can't delete the bucket
  1273  	resp, err := s3Client.ListObjects(&s3.ListObjectsInput{Bucket: &bucketName})
  1274  	if err != nil {
  1275  		t.Logf(warning, err)
  1276  		return
  1277  	}
  1278  	for _, obj := range resp.Contents {
  1279  		if _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{Bucket: &bucketName, Key: obj.Key}); err != nil {
  1280  			// this will need cleanup no matter what, so just warn and exit
  1281  			t.Logf(warning, err)
  1282  			return
  1283  		}
  1284  	}
  1285  
  1286  	if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil {
  1287  		t.Logf(warning, err)
  1288  	}
  1289  }
  1290  
  1291  // create the dynamoDB table, and wait until we can query it.
  1292  func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
  1293  	createInput := &dynamodb.CreateTableInput{
  1294  		AttributeDefinitions: []*dynamodb.AttributeDefinition{
  1295  			{
  1296  				AttributeName: aws.String("LockID"),
  1297  				AttributeType: aws.String("S"),
  1298  			},
  1299  		},
  1300  		KeySchema: []*dynamodb.KeySchemaElement{
  1301  			{
  1302  				AttributeName: aws.String("LockID"),
  1303  				KeyType:       aws.String("HASH"),
  1304  			},
  1305  		},
  1306  		ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
  1307  			ReadCapacityUnits:  aws.Int64(5),
  1308  			WriteCapacityUnits: aws.Int64(5),
  1309  		},
  1310  		TableName: aws.String(tableName),
  1311  	}
  1312  
  1313  	_, err := dynClient.CreateTable(createInput)
  1314  	if err != nil {
  1315  		t.Fatal(err)
  1316  	}
  1317  
  1318  	// now wait until it's ACTIVE
  1319  	start := time.Now()
  1320  	time.Sleep(time.Second)
  1321  
  1322  	describeInput := &dynamodb.DescribeTableInput{
  1323  		TableName: aws.String(tableName),
  1324  	}
  1325  
  1326  	for {
  1327  		resp, err := dynClient.DescribeTable(describeInput)
  1328  		if err != nil {
  1329  			t.Fatal(err)
  1330  		}
  1331  
  1332  		if *resp.Table.TableStatus == "ACTIVE" {
  1333  			return
  1334  		}
  1335  
  1336  		if time.Since(start) > time.Minute {
  1337  			t.Fatalf("timed out creating DynamoDB table %s", tableName)
  1338  		}
  1339  
  1340  		time.Sleep(3 * time.Second)
  1341  	}
  1342  
  1343  }
  1344  
  1345  func deleteDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
  1346  	params := &dynamodb.DeleteTableInput{
  1347  		TableName: aws.String(tableName),
  1348  	}
  1349  	_, err := dynClient.DeleteTable(params)
  1350  	if err != nil {
  1351  		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)
  1352  	}
  1353  }
  1354  
  1355  func populateSchema(t *testing.T, schema *configschema.Block, value cty.Value) cty.Value {
  1356  	ty := schema.ImpliedType()
  1357  	var path cty.Path
  1358  	val, err := unmarshal(value, ty, path)
  1359  	if err != nil {
  1360  		t.Fatalf("populating schema: %s", err)
  1361  	}
  1362  	return val
  1363  }
  1364  
  1365  func unmarshal(value cty.Value, ty cty.Type, path cty.Path) (cty.Value, error) {
  1366  	switch {
  1367  	case ty.IsPrimitiveType():
  1368  		return value, nil
  1369  	// case ty.IsListType():
  1370  	// 	return unmarshalList(value, ty.ElementType(), path)
  1371  	case ty.IsSetType():
  1372  		return unmarshalSet(value, ty.ElementType(), path)
  1373  	case ty.IsMapType():
  1374  		return unmarshalMap(value, ty.ElementType(), path)
  1375  	// case ty.IsTupleType():
  1376  	// 	return unmarshalTuple(value, ty.TupleElementTypes(), path)
  1377  	case ty.IsObjectType():
  1378  		return unmarshalObject(value, ty.AttributeTypes(), path)
  1379  	default:
  1380  		return cty.NilVal, path.NewErrorf("unsupported type %s", ty.FriendlyName())
  1381  	}
  1382  }
  1383  
  1384  func unmarshalSet(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
  1385  	if dec.IsNull() {
  1386  		return dec, nil
  1387  	}
  1388  
  1389  	length := dec.LengthInt()
  1390  
  1391  	if length == 0 {
  1392  		return cty.SetValEmpty(ety), nil
  1393  	}
  1394  
  1395  	vals := make([]cty.Value, 0, length)
  1396  	dec.ForEachElement(func(key, val cty.Value) (stop bool) {
  1397  		vals = append(vals, val)
  1398  		return
  1399  	})
  1400  
  1401  	return cty.SetVal(vals), nil
  1402  }
  1403  
  1404  func unmarshalMap(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
  1405  	if dec.IsNull() {
  1406  		return dec, nil
  1407  	}
  1408  
  1409  	length := dec.LengthInt()
  1410  
  1411  	if length == 0 {
  1412  		return cty.MapValEmpty(ety), nil
  1413  	}
  1414  
  1415  	vals := make(map[string]cty.Value, length)
  1416  	dec.ForEachElement(func(key, val cty.Value) (stop bool) {
  1417  		k := stringValue(key)
  1418  		vals[k] = val
  1419  		return
  1420  	})
  1421  
  1422  	return cty.MapVal(vals), nil
  1423  }
  1424  
  1425  func unmarshalObject(dec cty.Value, atys map[string]cty.Type, path cty.Path) (cty.Value, error) {
  1426  	if dec.IsNull() {
  1427  		return dec, nil
  1428  	}
  1429  	valueTy := dec.Type()
  1430  
  1431  	vals := make(map[string]cty.Value, len(atys))
  1432  	path = append(path, nil)
  1433  	for key, aty := range atys {
  1434  		path[len(path)-1] = cty.IndexStep{
  1435  			Key: cty.StringVal(key),
  1436  		}
  1437  
  1438  		if !valueTy.HasAttribute(key) {
  1439  			vals[key] = cty.NullVal(aty)
  1440  		} else {
  1441  			val, err := unmarshal(dec.GetAttr(key), aty, path)
  1442  			if err != nil {
  1443  				return cty.DynamicVal, err
  1444  			}
  1445  			vals[key] = val
  1446  		}
  1447  	}
  1448  
  1449  	return cty.ObjectVal(vals), nil
  1450  }
  1451  
  1452  func must[T any](v T, err error) T {
  1453  	if err != nil {
  1454  		panic(err)
  1455  	} else {
  1456  		return v
  1457  	}
  1458  }