github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/s3/backend_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package s3
     7  
     8  import (
     9  	"context"
    10  	"encoding/base64"
    11  	"fmt"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"reflect"
    16  	"strings"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/aws/aws-sdk-go-v2/aws"
    21  	awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
    22  	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
    23  	dtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
    24  	"github.com/aws/aws-sdk-go-v2/service/s3"
    25  	types "github.com/aws/aws-sdk-go-v2/service/s3/types"
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/hashicorp/aws-sdk-go-base/v2/mockdata"
    28  	"github.com/hashicorp/aws-sdk-go-base/v2/servicemocks"
    29  	"github.com/zclconf/go-cty/cty"
    30  
    31  	"github.com/opentofu/opentofu/internal/backend"
    32  	"github.com/opentofu/opentofu/internal/configs/configschema"
    33  	"github.com/opentofu/opentofu/internal/configs/hcl2shim"
    34  	"github.com/opentofu/opentofu/internal/encryption"
    35  	"github.com/opentofu/opentofu/internal/states"
    36  	"github.com/opentofu/opentofu/internal/states/remote"
    37  	"github.com/opentofu/opentofu/internal/tfdiags"
    38  )
    39  
    40  const testBucketPrefix = "tofu-test"
    41  
    42  var (
    43  	mockStsGetCallerIdentityRequestBody = url.Values{
    44  		"Action":  []string{"GetCallerIdentity"},
    45  		"Version": []string{"2011-06-15"},
    46  	}.Encode()
    47  )
    48  
    49  // verify that we are doing ACC tests or the S3 tests specifically
    50  func testACC(t *testing.T) {
    51  	t.Helper()
    52  	skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_S3_TEST") == ""
    53  	if skip {
    54  		t.Log("s3 backend tests require setting TF_ACC or TF_S3_TEST")
    55  		t.Skip()
    56  	}
    57  	t.Setenv("AWS_DEFAULT_REGION", "us-west-2")
    58  }
    59  
    60  func TestBackend_impl(t *testing.T) {
    61  	var _ backend.Backend = new(Backend)
    62  }
    63  
    64  func TestBackendConfig_original(t *testing.T) {
    65  	testACC(t)
    66  	config := map[string]interface{}{
    67  		"region":         "us-west-1",
    68  		"bucket":         testBucketPrefix,
    69  		"key":            "state",
    70  		"encrypt":        true,
    71  		"dynamodb_table": "dynamoTable",
    72  	}
    73  
    74  	b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config)).(*Backend)
    75  
    76  	if b.awsConfig.Region != "us-west-1" {
    77  		t.Fatalf("Incorrect region was populated")
    78  	}
    79  	if b.awsConfig.RetryMaxAttempts != 5 {
    80  		t.Fatalf("Default max_retries was not set")
    81  	}
    82  	if b.bucketName != testBucketPrefix {
    83  		t.Fatalf("Incorrect bucketName was populated")
    84  	}
    85  	if b.keyName != "state" {
    86  		t.Fatalf("Incorrect keyName was populated")
    87  	}
    88  
    89  	credentials, err := b.awsConfig.Credentials.Retrieve(context.TODO())
    90  	if err != nil {
    91  		t.Fatalf("Error when requesting credentials")
    92  	}
    93  	if credentials.AccessKeyID == "" {
    94  		t.Fatalf("No Access Key Id was populated")
    95  	}
    96  	if credentials.SecretAccessKey == "" {
    97  		t.Fatalf("No Secret Access Key was populated")
    98  	}
    99  }
   100  
   101  func TestBackendConfig_InvalidRegion(t *testing.T) {
   102  	testACC(t)
   103  
   104  	cases := map[string]struct {
   105  		config        map[string]any
   106  		expectedDiags tfdiags.Diagnostics
   107  	}{
   108  		"with region validation": {
   109  			config: map[string]interface{}{
   110  				"region":                      "nonesuch",
   111  				"bucket":                      testBucketPrefix,
   112  				"key":                         "state",
   113  				"skip_credentials_validation": true,
   114  			},
   115  			expectedDiags: tfdiags.Diagnostics{
   116  				tfdiags.AttributeValue(
   117  					tfdiags.Error,
   118  					"Invalid region value",
   119  					`Invalid AWS Region: nonesuch`,
   120  					cty.Path{cty.GetAttrStep{Name: "region"}},
   121  				),
   122  			},
   123  		},
   124  		"skip region validation": {
   125  			config: map[string]interface{}{
   126  				"region":                      "nonesuch",
   127  				"bucket":                      testBucketPrefix,
   128  				"key":                         "state",
   129  				"skip_region_validation":      true,
   130  				"skip_credentials_validation": true,
   131  			},
   132  			expectedDiags: nil,
   133  		},
   134  	}
   135  
   136  	for name, tc := range cases {
   137  		t.Run(name, func(t *testing.T) {
   138  			b := New(encryption.StateEncryptionDisabled())
   139  			configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(tc.config))
   140  
   141  			configSchema, diags := b.PrepareConfig(configSchema)
   142  			if len(diags) > 0 {
   143  				t.Fatal(diags.ErrWithWarnings())
   144  			}
   145  
   146  			confDiags := b.Configure(configSchema)
   147  			diags = diags.Append(confDiags)
   148  
   149  			if diff := cmp.Diff(diags, tc.expectedDiags, cmp.Comparer(diagnosticComparer)); diff != "" {
   150  				t.Errorf("unexpected diagnostics difference: %s", diff)
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  func TestBackendConfig_RegionEnvVar(t *testing.T) {
   157  	testACC(t)
   158  	config := map[string]interface{}{
   159  		"bucket": testBucketPrefix,
   160  		"key":    "state",
   161  	}
   162  
   163  	cases := map[string]struct {
   164  		vars map[string]string
   165  	}{
   166  		"AWS_REGION": {
   167  			vars: map[string]string{
   168  				"AWS_REGION": "us-west-1",
   169  			},
   170  		},
   171  
   172  		"AWS_DEFAULT_REGION": {
   173  			vars: map[string]string{
   174  				"AWS_DEFAULT_REGION": "us-west-1",
   175  			},
   176  		},
   177  	}
   178  
   179  	for name, tc := range cases {
   180  		t.Run(name, func(t *testing.T) {
   181  			for k, v := range tc.vars {
   182  				t.Setenv(k, v)
   183  			}
   184  
   185  			b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config)).(*Backend)
   186  
   187  			if b.awsConfig.Region != "us-west-1" {
   188  				t.Fatalf("Incorrect region was populated")
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  func TestBackendConfig_DynamoDBEndpoint(t *testing.T) {
   195  	testACC(t)
   196  
   197  	cases := map[string]struct {
   198  		config   map[string]any
   199  		vars     map[string]string
   200  		expected string
   201  	}{
   202  		"none": {
   203  			expected: "",
   204  		},
   205  		"config": {
   206  			config: map[string]any{
   207  				"dynamodb_endpoint": "dynamo.test",
   208  			},
   209  			expected: "dynamo.test",
   210  		},
   211  		"envvar": {
   212  			vars: map[string]string{
   213  				"AWS_DYNAMODB_ENDPOINT": "dynamo.test",
   214  			},
   215  			expected: "dynamo.test",
   216  		},
   217  	}
   218  
   219  	for name, tc := range cases {
   220  		t.Run(name, func(t *testing.T) {
   221  			config := map[string]interface{}{
   222  				"region": "us-west-1",
   223  				"bucket": testBucketPrefix,
   224  				"key":    "state",
   225  			}
   226  
   227  			if tc.vars != nil {
   228  				for k, v := range tc.vars {
   229  					t.Setenv(k, v)
   230  				}
   231  			}
   232  
   233  			if tc.config != nil {
   234  				for k, v := range tc.config {
   235  					config[k] = v
   236  				}
   237  			}
   238  
   239  			backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config))
   240  		})
   241  	}
   242  }
   243  
   244  func TestBackendConfig_S3Endpoint(t *testing.T) {
   245  	testACC(t)
   246  
   247  	cases := map[string]struct {
   248  		config   map[string]any
   249  		vars     map[string]string
   250  		expected string
   251  	}{
   252  		"none": {
   253  			expected: "",
   254  		},
   255  		"config": {
   256  			config: map[string]any{
   257  				"endpoint": "s3.test",
   258  			},
   259  			expected: "s3.test",
   260  		},
   261  		"envvar": {
   262  			vars: map[string]string{
   263  				"AWS_S3_ENDPOINT": "s3.test",
   264  			},
   265  			expected: "s3.test",
   266  		},
   267  	}
   268  
   269  	for name, tc := range cases {
   270  		t.Run(name, func(t *testing.T) {
   271  			config := map[string]interface{}{
   272  				"region": "us-west-1",
   273  				"bucket": testBucketPrefix,
   274  				"key":    "state",
   275  			}
   276  
   277  			if tc.vars != nil {
   278  				for k, v := range tc.vars {
   279  					t.Setenv(k, v)
   280  				}
   281  			}
   282  
   283  			if tc.config != nil {
   284  				for k, v := range tc.config {
   285  					config[k] = v
   286  				}
   287  			}
   288  
   289  			backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config))
   290  		})
   291  	}
   292  }
   293  
   294  func TestBackendConfig_STSEndpoint(t *testing.T) {
   295  	testACC(t)
   296  
   297  	stsMocks := []*servicemocks.MockEndpoint{
   298  		{
   299  			Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   300  				"Action":          []string{"AssumeRole"},
   301  				"DurationSeconds": []string{"900"},
   302  				"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
   303  				"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
   304  				"Version":         []string{"2011-06-15"},
   305  			}.Encode()},
   306  			Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   307  		},
   308  		{
   309  			Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   310  			Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   311  		},
   312  	}
   313  
   314  	cases := map[string]struct {
   315  		setConfig     bool
   316  		setEnvVars    bool
   317  		expectedDiags tfdiags.Diagnostics
   318  	}{
   319  		"none": {
   320  			expectedDiags: tfdiags.Diagnostics{
   321  				tfdiags.Sourceless(
   322  					tfdiags.Error,
   323  					"Cannot assume IAM Role",
   324  					``,
   325  				),
   326  			},
   327  		},
   328  		"config": {
   329  			setConfig: true,
   330  		},
   331  		"envvar": {
   332  			setEnvVars: true,
   333  		},
   334  	}
   335  
   336  	for name, tc := range cases {
   337  		t.Run(name, func(t *testing.T) {
   338  			config := map[string]interface{}{
   339  				"region": "us-west-1",
   340  				"bucket": testBucketPrefix,
   341  				"key":    "state",
   342  				"assume_role": map[string]interface{}{
   343  					"role_arn":     servicemocks.MockStsAssumeRoleArn,
   344  					"session_name": servicemocks.MockStsAssumeRoleSessionName,
   345  				},
   346  			}
   347  
   348  			closeSts, _, endpoint := mockdata.GetMockedAwsApiSession("STS", stsMocks)
   349  			defer closeSts()
   350  
   351  			if tc.setEnvVars {
   352  				t.Setenv("AWS_STS_ENDPOINT", endpoint)
   353  			}
   354  
   355  			if tc.setConfig {
   356  				config["sts_endpoint"] = endpoint
   357  			}
   358  
   359  			b := New(encryption.StateEncryptionDisabled())
   360  			configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config))
   361  
   362  			configSchema, diags := b.PrepareConfig(configSchema)
   363  			if len(diags) > 0 {
   364  				t.Fatal(diags.ErrWithWarnings())
   365  			}
   366  
   367  			confDiags := b.Configure(configSchema)
   368  			diags = diags.Append(confDiags)
   369  
   370  			if diff := cmp.Diff(diags, tc.expectedDiags, cmp.Comparer(diagnosticSummaryComparer)); diff != "" {
   371  				t.Errorf("unexpected diagnostics difference: %s", diff)
   372  			}
   373  		})
   374  	}
   375  }
   376  
   377  func TestBackendConfig_AssumeRole(t *testing.T) {
   378  	testACC(t)
   379  
   380  	testCases := []struct {
   381  		Config           map[string]interface{}
   382  		Description      string
   383  		MockStsEndpoints []*servicemocks.MockEndpoint
   384  	}{
   385  		{
   386  			Config: map[string]interface{}{
   387  				"bucket":       testBucketPrefix,
   388  				"key":          "state",
   389  				"region":       "us-west-1",
   390  				"role_arn":     servicemocks.MockStsAssumeRoleArn,
   391  				"session_name": servicemocks.MockStsAssumeRoleSessionName,
   392  			},
   393  			Description: "role_arn",
   394  			MockStsEndpoints: []*servicemocks.MockEndpoint{
   395  				{
   396  					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   397  						"Action":          []string{"AssumeRole"},
   398  						"DurationSeconds": []string{"900"},
   399  						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
   400  						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
   401  						"Version":         []string{"2011-06-15"},
   402  					}.Encode()},
   403  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   404  				},
   405  				{
   406  					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   407  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   408  				},
   409  			},
   410  		},
   411  		{
   412  			Config: map[string]interface{}{
   413  				"assume_role_duration_seconds": 3600,
   414  				"bucket":                       testBucketPrefix,
   415  				"key":                          "state",
   416  				"region":                       "us-west-1",
   417  				"role_arn":                     servicemocks.MockStsAssumeRoleArn,
   418  				"session_name":                 servicemocks.MockStsAssumeRoleSessionName,
   419  			},
   420  			Description: "assume_role_duration_seconds",
   421  			MockStsEndpoints: []*servicemocks.MockEndpoint{
   422  				{
   423  					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   424  						"Action":          []string{"AssumeRole"},
   425  						"DurationSeconds": []string{"3600"},
   426  						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
   427  						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
   428  						"Version":         []string{"2011-06-15"},
   429  					}.Encode()},
   430  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   431  				},
   432  				{
   433  					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   434  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   435  				},
   436  			},
   437  		},
   438  		{
   439  			Config: map[string]interface{}{
   440  				"bucket":       testBucketPrefix,
   441  				"external_id":  servicemocks.MockStsAssumeRoleExternalId,
   442  				"key":          "state",
   443  				"region":       "us-west-1",
   444  				"role_arn":     servicemocks.MockStsAssumeRoleArn,
   445  				"session_name": servicemocks.MockStsAssumeRoleSessionName,
   446  			},
   447  			Description: "external_id",
   448  			MockStsEndpoints: []*servicemocks.MockEndpoint{
   449  				{
   450  					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   451  						"Action":          []string{"AssumeRole"},
   452  						"DurationSeconds": []string{"900"},
   453  						"ExternalId":      []string{servicemocks.MockStsAssumeRoleExternalId},
   454  						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
   455  						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
   456  						"Version":         []string{"2011-06-15"},
   457  					}.Encode()},
   458  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   459  				},
   460  				{
   461  					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   462  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   463  				},
   464  			},
   465  		},
   466  		{
   467  			Config: map[string]interface{}{
   468  				"assume_role_policy": servicemocks.MockStsAssumeRolePolicy,
   469  				"bucket":             "tofu-test",
   470  				"key":                "state",
   471  				"region":             "us-west-1",
   472  				"role_arn":           servicemocks.MockStsAssumeRoleArn,
   473  				"session_name":       servicemocks.MockStsAssumeRoleSessionName,
   474  			},
   475  			Description: "assume_role_policy",
   476  			MockStsEndpoints: []*servicemocks.MockEndpoint{
   477  				{
   478  					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   479  						"Action":          []string{"AssumeRole"},
   480  						"DurationSeconds": []string{"900"},
   481  						"Policy":          []string{servicemocks.MockStsAssumeRolePolicy},
   482  						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
   483  						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
   484  						"Version":         []string{"2011-06-15"},
   485  					}.Encode()},
   486  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   487  				},
   488  				{
   489  					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   490  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   491  				},
   492  			},
   493  		},
   494  		{
   495  			Config: map[string]interface{}{
   496  				"assume_role_policy_arns": []interface{}{servicemocks.MockStsAssumeRolePolicyArn},
   497  				"bucket":                  "tofu-test",
   498  				"key":                     "state",
   499  				"region":                  "us-west-1",
   500  				"role_arn":                servicemocks.MockStsAssumeRoleArn,
   501  				"session_name":            servicemocks.MockStsAssumeRoleSessionName,
   502  			},
   503  			Description: "assume_role_policy_arns",
   504  			MockStsEndpoints: []*servicemocks.MockEndpoint{
   505  				{
   506  					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   507  						"Action":                  []string{"AssumeRole"},
   508  						"DurationSeconds":         []string{"900"},
   509  						"PolicyArns.member.1.arn": []string{servicemocks.MockStsAssumeRolePolicyArn},
   510  						"RoleArn":                 []string{servicemocks.MockStsAssumeRoleArn},
   511  						"RoleSessionName":         []string{servicemocks.MockStsAssumeRoleSessionName},
   512  						"Version":                 []string{"2011-06-15"},
   513  					}.Encode()},
   514  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   515  				},
   516  				{
   517  					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   518  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   519  				},
   520  			},
   521  		},
   522  		{
   523  			Config: map[string]interface{}{
   524  				"assume_role_tags": map[string]interface{}{
   525  					servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue,
   526  				},
   527  				"bucket":       "tofu-test",
   528  				"key":          "state",
   529  				"region":       "us-west-1",
   530  				"role_arn":     servicemocks.MockStsAssumeRoleArn,
   531  				"session_name": servicemocks.MockStsAssumeRoleSessionName,
   532  			},
   533  			Description: "assume_role_tags",
   534  			MockStsEndpoints: []*servicemocks.MockEndpoint{
   535  				{
   536  					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   537  						"Action":              []string{"AssumeRole"},
   538  						"DurationSeconds":     []string{"900"},
   539  						"RoleArn":             []string{servicemocks.MockStsAssumeRoleArn},
   540  						"RoleSessionName":     []string{servicemocks.MockStsAssumeRoleSessionName},
   541  						"Tags.member.1.Key":   []string{servicemocks.MockStsAssumeRoleTagKey},
   542  						"Tags.member.1.Value": []string{servicemocks.MockStsAssumeRoleTagValue},
   543  						"Version":             []string{"2011-06-15"},
   544  					}.Encode()},
   545  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   546  				},
   547  				{
   548  					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   549  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   550  				},
   551  			},
   552  		},
   553  		{
   554  			Config: map[string]interface{}{
   555  				"assume_role_tags": map[string]interface{}{
   556  					servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue,
   557  				},
   558  				"assume_role_transitive_tag_keys": []interface{}{servicemocks.MockStsAssumeRoleTagKey},
   559  				"bucket":                          "tofu-test",
   560  				"key":                             "state",
   561  				"region":                          "us-west-1",
   562  				"role_arn":                        servicemocks.MockStsAssumeRoleArn,
   563  				"session_name":                    servicemocks.MockStsAssumeRoleSessionName,
   564  			},
   565  			Description: "assume_role_transitive_tag_keys",
   566  			MockStsEndpoints: []*servicemocks.MockEndpoint{
   567  				{
   568  					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
   569  						"Action":                     []string{"AssumeRole"},
   570  						"DurationSeconds":            []string{"900"},
   571  						"RoleArn":                    []string{servicemocks.MockStsAssumeRoleArn},
   572  						"RoleSessionName":            []string{servicemocks.MockStsAssumeRoleSessionName},
   573  						"Tags.member.1.Key":          []string{servicemocks.MockStsAssumeRoleTagKey},
   574  						"Tags.member.1.Value":        []string{servicemocks.MockStsAssumeRoleTagValue},
   575  						"TransitiveTagKeys.member.1": []string{servicemocks.MockStsAssumeRoleTagKey},
   576  						"Version":                    []string{"2011-06-15"},
   577  					}.Encode()},
   578  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
   579  				},
   580  				{
   581  					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
   582  					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
   583  				},
   584  			},
   585  		},
   586  	}
   587  
   588  	for _, testCase := range testCases {
   589  		testCase := testCase
   590  
   591  		t.Run(testCase.Description, func(t *testing.T) {
   592  			closeSts, _, endpoint := mockdata.GetMockedAwsApiSession("STS", testCase.MockStsEndpoints)
   593  			defer closeSts()
   594  
   595  			testCase.Config["sts_endpoint"] = endpoint
   596  
   597  			b := New(encryption.StateEncryptionDisabled())
   598  			diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(testCase.Config)))
   599  
   600  			if diags.HasErrors() {
   601  				for _, diag := range diags {
   602  					t.Errorf("unexpected error: %s", diag.Description().Summary)
   603  				}
   604  			}
   605  		})
   606  	}
   607  }
   608  
   609  func TestBackendConfig_PrepareConfigValidation(t *testing.T) {
   610  	cases := map[string]struct {
   611  		config      cty.Value
   612  		expectedErr string
   613  	}{
   614  		"null bucket": {
   615  			config: cty.ObjectVal(map[string]cty.Value{
   616  				"bucket": cty.NullVal(cty.String),
   617  				"key":    cty.StringVal("test"),
   618  				"region": cty.StringVal("us-west-2"),
   619  			}),
   620  			expectedErr: `The "bucket" attribute value must not be empty.`,
   621  		},
   622  		"empty bucket": {
   623  			config: cty.ObjectVal(map[string]cty.Value{
   624  				"bucket": cty.StringVal(""),
   625  				"key":    cty.StringVal("test"),
   626  				"region": cty.StringVal("us-west-2"),
   627  			}),
   628  			expectedErr: `The "bucket" attribute value must not be empty.`,
   629  		},
   630  		"null key": {
   631  			config: cty.ObjectVal(map[string]cty.Value{
   632  				"bucket": cty.StringVal("test"),
   633  				"key":    cty.NullVal(cty.String),
   634  				"region": cty.StringVal("us-west-2"),
   635  			}),
   636  			expectedErr: `The "key" attribute value must not be empty.`,
   637  		},
   638  		"empty key": {
   639  			config: cty.ObjectVal(map[string]cty.Value{
   640  				"bucket": cty.StringVal("test"),
   641  				"key":    cty.StringVal(""),
   642  				"region": cty.StringVal("us-west-2"),
   643  			}),
   644  			expectedErr: `The "key" attribute value must not be empty.`,
   645  		},
   646  		"key with leading slash": {
   647  			config: cty.ObjectVal(map[string]cty.Value{
   648  				"bucket": cty.StringVal("test"),
   649  				"key":    cty.StringVal("/leading-slash"),
   650  				"region": cty.StringVal("us-west-2"),
   651  			}),
   652  			expectedErr: `The "key" attribute value must not start or end with with "/".`,
   653  		},
   654  		"key with trailing slash": {
   655  			config: cty.ObjectVal(map[string]cty.Value{
   656  				"bucket": cty.StringVal("test"),
   657  				"key":    cty.StringVal("trailing-slash/"),
   658  				"region": cty.StringVal("us-west-2"),
   659  			}),
   660  			expectedErr: `The "key" attribute value must not start or end with with "/".`,
   661  		},
   662  		"null region": {
   663  			config: cty.ObjectVal(map[string]cty.Value{
   664  				"bucket": cty.StringVal("test"),
   665  				"key":    cty.StringVal("test"),
   666  				"region": cty.NullVal(cty.String),
   667  			}),
   668  			expectedErr: `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`,
   669  		},
   670  		"empty region": {
   671  			config: cty.ObjectVal(map[string]cty.Value{
   672  				"bucket": cty.StringVal("test"),
   673  				"key":    cty.StringVal("test"),
   674  				"region": cty.StringVal(""),
   675  			}),
   676  			expectedErr: `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`,
   677  		},
   678  		"workspace_key_prefix with leading slash": {
   679  			config: cty.ObjectVal(map[string]cty.Value{
   680  				"bucket":               cty.StringVal("test"),
   681  				"key":                  cty.StringVal("test"),
   682  				"region":               cty.StringVal("us-west-2"),
   683  				"workspace_key_prefix": cty.StringVal("/env"),
   684  			}),
   685  			expectedErr: `The "workspace_key_prefix" attribute value must not start with "/".`,
   686  		},
   687  		"workspace_key_prefix with trailing slash": {
   688  			config: cty.ObjectVal(map[string]cty.Value{
   689  				"bucket":               cty.StringVal("test"),
   690  				"key":                  cty.StringVal("test"),
   691  				"region":               cty.StringVal("us-west-2"),
   692  				"workspace_key_prefix": cty.StringVal("env/"),
   693  			}),
   694  			expectedErr: `The "workspace_key_prefix" attribute value must not start with "/".`,
   695  		},
   696  		"encyrption key conflict": {
   697  			config: cty.ObjectVal(map[string]cty.Value{
   698  				"bucket":               cty.StringVal("test"),
   699  				"key":                  cty.StringVal("test"),
   700  				"region":               cty.StringVal("us-west-2"),
   701  				"workspace_key_prefix": cty.StringVal("env"),
   702  				"sse_customer_key":     cty.StringVal("1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o="),
   703  				"kms_key_id":           cty.StringVal("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"),
   704  			}),
   705  			expectedErr: `Only one of "kms_key_id" and "sse_customer_key" can be set`,
   706  		},
   707  		"allowed forbidden account ids conflict": {
   708  			config: cty.ObjectVal(map[string]cty.Value{
   709  				"bucket":                cty.StringVal("test"),
   710  				"key":                   cty.StringVal("test"),
   711  				"region":                cty.StringVal("us-west-2"),
   712  				"allowed_account_ids":   cty.SetVal([]cty.Value{cty.StringVal("111111111111")}),
   713  				"forbidden_account_ids": cty.SetVal([]cty.Value{cty.StringVal("111111111111")}),
   714  			}),
   715  			expectedErr: "Invalid Attribute Combination: Only one of allowed_account_ids, forbidden_account_ids can be set.",
   716  		},
   717  		"invalid retry mode": {
   718  			config: cty.ObjectVal(map[string]cty.Value{
   719  				"bucket":     cty.StringVal("test"),
   720  				"key":        cty.StringVal("test"),
   721  				"region":     cty.StringVal("us-west-2"),
   722  				"retry_mode": cty.StringVal("xyz"),
   723  			}),
   724  			expectedErr: `Invalid retry mode: Valid values are "standard" and "adaptive".`,
   725  		},
   726  		"s3 endpoint conflict": {
   727  			config: cty.ObjectVal(map[string]cty.Value{
   728  				"bucket":   cty.StringVal("test"),
   729  				"key":      cty.StringVal("test"),
   730  				"region":   cty.StringVal("us-west-2"),
   731  				"endpoint": cty.StringVal("x1"),
   732  				"endpoints": cty.ObjectVal(map[string]cty.Value{
   733  					"s3": cty.StringVal("x2"),
   734  				}),
   735  			}),
   736  			expectedErr: `Invalid Attribute Combination: Only one of endpoints.s3, endpoint can be set.`,
   737  		},
   738  		"iam endpoint conflict": {
   739  			config: cty.ObjectVal(map[string]cty.Value{
   740  				"bucket":       cty.StringVal("test"),
   741  				"key":          cty.StringVal("test"),
   742  				"region":       cty.StringVal("us-west-2"),
   743  				"iam_endpoint": cty.StringVal("x1"),
   744  				"endpoints": cty.ObjectVal(map[string]cty.Value{
   745  					"iam": cty.StringVal("x2"),
   746  				}),
   747  			}),
   748  			expectedErr: `Invalid Attribute Combination: Only one of endpoints.iam, iam_endpoint can be set.`,
   749  		},
   750  		"sts endpoint conflict": {
   751  			config: cty.ObjectVal(map[string]cty.Value{
   752  				"bucket":       cty.StringVal("test"),
   753  				"key":          cty.StringVal("test"),
   754  				"region":       cty.StringVal("us-west-2"),
   755  				"sts_endpoint": cty.StringVal("x1"),
   756  				"endpoints": cty.ObjectVal(map[string]cty.Value{
   757  					"sts": cty.StringVal("x2"),
   758  				}),
   759  			}),
   760  			expectedErr: `Invalid Attribute Combination: Only one of endpoints.sts, sts_endpoint can be set.`,
   761  		},
   762  		"dynamodb endpoint conflict": {
   763  			config: cty.ObjectVal(map[string]cty.Value{
   764  				"bucket":            cty.StringVal("test"),
   765  				"key":               cty.StringVal("test"),
   766  				"region":            cty.StringVal("us-west-2"),
   767  				"dynamodb_endpoint": cty.StringVal("x1"),
   768  				"endpoints": cty.ObjectVal(map[string]cty.Value{
   769  					"dynamodb": cty.StringVal("x2"),
   770  				}),
   771  			}),
   772  			expectedErr: `Invalid Attribute Combination: Only one of endpoints.dynamodb, dynamodb_endpoint can be set.`,
   773  		},
   774  	}
   775  
   776  	for name, tc := range cases {
   777  		t.Run(name, func(t *testing.T) {
   778  			servicemocks.StashEnv(t)
   779  
   780  			b := New(encryption.StateEncryptionDisabled())
   781  
   782  			_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))
   783  			if tc.expectedErr != "" {
   784  				if valDiags.Err() != nil {
   785  					actualErr := valDiags.Err().Error()
   786  					if !strings.Contains(actualErr, tc.expectedErr) {
   787  						t.Fatalf("unexpected validation result: %v", valDiags.Err())
   788  					}
   789  				} else {
   790  					t.Fatal("expected an error, got none")
   791  				}
   792  			} else if valDiags.Err() != nil {
   793  				t.Fatalf("expected no error, got %s", valDiags.Err())
   794  			}
   795  		})
   796  	}
   797  }
   798  
   799  func TestBackendConfig_PrepareConfigValidationWarnings(t *testing.T) {
   800  	cases := map[string]struct {
   801  		config       cty.Value
   802  		expectedWarn string
   803  	}{
   804  		"deprecated force path style": {
   805  			config: cty.ObjectVal(map[string]cty.Value{
   806  				"bucket":           cty.StringVal("test"),
   807  				"key":              cty.StringVal("test"),
   808  				"region":           cty.StringVal("us-west-2"),
   809  				"force_path_style": cty.BoolVal(false),
   810  			}),
   811  			expectedWarn: `Deprecated Parameter: Parameter "force_path_style" is deprecated. Use "use_path_style" instead.`,
   812  		},
   813  	}
   814  
   815  	for name, tc := range cases {
   816  		t.Run(name, func(t *testing.T) {
   817  			servicemocks.StashEnv(t)
   818  
   819  			b := New(encryption.StateEncryptionDisabled())
   820  
   821  			_, diags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))
   822  			if tc.expectedWarn != "" {
   823  				if err := diags.ErrWithWarnings(); err != nil {
   824  					if !strings.Contains(err.Error(), tc.expectedWarn) {
   825  						t.Fatalf("unexpected validation result: %v", err)
   826  					}
   827  				} else {
   828  					t.Fatal("expected a warning, got none")
   829  				}
   830  			} else if err := diags.ErrWithWarnings(); err != nil {
   831  				t.Fatalf("expected no warnings, got %s", err)
   832  			}
   833  		})
   834  	}
   835  }
   836  
   837  func TestBackendConfig_PrepareConfigWithEnvVars(t *testing.T) {
   838  	cases := map[string]struct {
   839  		config      cty.Value
   840  		vars        map[string]string
   841  		expectedErr string
   842  	}{
   843  		"region env var AWS_REGION": {
   844  			config: cty.ObjectVal(map[string]cty.Value{
   845  				"bucket": cty.StringVal("test"),
   846  				"key":    cty.StringVal("test"),
   847  				"region": cty.NullVal(cty.String),
   848  			}),
   849  			vars: map[string]string{
   850  				"AWS_REGION": "us-west-1",
   851  			},
   852  		},
   853  		"region env var AWS_DEFAULT_REGION": {
   854  			config: cty.ObjectVal(map[string]cty.Value{
   855  				"bucket": cty.StringVal("test"),
   856  				"key":    cty.StringVal("test"),
   857  				"region": cty.NullVal(cty.String),
   858  			}),
   859  			vars: map[string]string{
   860  				"AWS_DEFAULT_REGION": "us-west-1",
   861  			},
   862  		},
   863  		"encyrption key conflict": {
   864  			config: cty.ObjectVal(map[string]cty.Value{
   865  				"bucket":               cty.StringVal("test"),
   866  				"key":                  cty.StringVal("test"),
   867  				"region":               cty.StringVal("us-west-2"),
   868  				"workspace_key_prefix": cty.StringVal("env"),
   869  				"kms_key_id":           cty.StringVal("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"),
   870  			}),
   871  			vars: map[string]string{
   872  				"AWS_SSE_CUSTOMER_KEY": "1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o=",
   873  			},
   874  			expectedErr: `Only one of "kms_key_id" and the environment variable "AWS_SSE_CUSTOMER_KEY" can be set`,
   875  		},
   876  	}
   877  
   878  	for name, tc := range cases {
   879  		t.Run(name, func(t *testing.T) {
   880  			servicemocks.StashEnv(t)
   881  
   882  			b := New(encryption.StateEncryptionDisabled())
   883  
   884  			for k, v := range tc.vars {
   885  				t.Setenv(k, v)
   886  			}
   887  
   888  			_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))
   889  			if tc.expectedErr != "" {
   890  				if valDiags.Err() != nil {
   891  					actualErr := valDiags.Err().Error()
   892  					if !strings.Contains(actualErr, tc.expectedErr) {
   893  						t.Fatalf("unexpected validation result: %v", valDiags.Err())
   894  					}
   895  				} else {
   896  					t.Fatal("expected an error, got none")
   897  				}
   898  			} else if valDiags.Err() != nil {
   899  				t.Fatalf("expected no error, got %s", valDiags.Err())
   900  			}
   901  		})
   902  	}
   903  }
   904  
   905  // TestBackendConfig_proxy tests proxy configuration
   906  func TestBackendConfig_proxy(t *testing.T) {
   907  	testACC(t)
   908  
   909  	newURL := func(rawURL string) *url.URL {
   910  		o, err := url.Parse(rawURL)
   911  		if err != nil {
   912  			panic(err)
   913  		}
   914  		return o
   915  	}
   916  
   917  	cases := map[string]struct {
   918  		config       cty.Value
   919  		calledURL    string
   920  		envVars      map[string]string
   921  		wantProxyURL *url.URL
   922  
   923  		// wantErrSubstr contains the part indicating proxy address
   924  		wantErrSubstr string
   925  	}{
   926  		"shall set proxy using http_proxy config attr": {
   927  			config: cty.ObjectVal(map[string]cty.Value{
   928  				"bucket":     cty.StringVal("test"),
   929  				"key":        cty.StringVal("test"),
   930  				"http_proxy": cty.StringVal("http://foo.bar"),
   931  			}),
   932  			calledURL:    "http://qux.quxx",
   933  			wantProxyURL: newURL("http://foo.bar"),
   934  		},
   935  		"shall set proxy using HTTP_PROXY envvar": {
   936  			config: cty.ObjectVal(map[string]cty.Value{
   937  				"bucket": cty.StringVal("test"),
   938  				"key":    cty.StringVal("test"),
   939  			}),
   940  			envVars: map[string]string{
   941  				"HTTP_PROXY": "http://foo.com",
   942  			},
   943  			calledURL:    "http://qux.quxx",
   944  			wantProxyURL: newURL("http://foo.com"),
   945  		},
   946  		"shall set proxy using http_proxy config attr when HTTP_PROXY envvar is also set": {
   947  			config: cty.ObjectVal(map[string]cty.Value{
   948  				"bucket":     cty.StringVal("test"),
   949  				"key":        cty.StringVal("test"),
   950  				"http_proxy": cty.StringVal("http://foo.bar"),
   951  			}),
   952  			envVars: map[string]string{
   953  				"HTTP_PROXY": "http://foo.com",
   954  			},
   955  			calledURL:    "http://qux.quxx",
   956  			wantProxyURL: newURL("http://foo.bar"),
   957  		},
   958  		"shall set proxy using https_proxy config attr": {
   959  			config: cty.ObjectVal(map[string]cty.Value{
   960  				"bucket":      cty.StringVal("test"),
   961  				"key":         cty.StringVal("test"),
   962  				"https_proxy": cty.StringVal("https://foo.bar"),
   963  			}),
   964  			calledURL:     "https://qux.quxx",
   965  			wantErrSubstr: "proxyconnect tcp: dial tcp: lookup foo.bar",
   966  		},
   967  		"shall set proxy using HTTPS_PROXY envvar": {
   968  			config: cty.ObjectVal(map[string]cty.Value{
   969  				"bucket": cty.StringVal("test"),
   970  				"key":    cty.StringVal("test"),
   971  			}),
   972  			envVars: map[string]string{
   973  				"HTTPS_PROXY": "https://foo.baz",
   974  			},
   975  			calledURL:     "https://qux.quxx",
   976  			wantErrSubstr: "proxyconnect tcp: dial tcp: lookup foo.baz",
   977  		},
   978  		"shall set proxy using https_proxy config attr when HTTPS_PROXY envvar is also set": {
   979  			config: cty.ObjectVal(map[string]cty.Value{
   980  				"bucket":      cty.StringVal("test"),
   981  				"key":         cty.StringVal("test"),
   982  				"https_proxy": cty.StringVal("https://foo.bar"),
   983  			}),
   984  			envVars: map[string]string{
   985  				"HTTPS_PROXY": "https://foo.com",
   986  			},
   987  			calledURL:     "https://qux.quxx",
   988  			wantErrSubstr: "proxyconnect tcp: dial tcp: lookup foo.bar",
   989  		},
   990  		"shall satisfy no_proxy config attr": {
   991  			config: cty.ObjectVal(map[string]cty.Value{
   992  				"bucket":   cty.StringVal("test"),
   993  				"key":      cty.StringVal("test"),
   994  				"no_proxy": cty.StringVal("http://foo.bar,1.2.3.4"),
   995  			}),
   996  			calledURL: "http://foo.bar",
   997  		},
   998  		"shall satisfy no proxy set using NO_PROXY envvar": {
   999  			config: cty.ObjectVal(map[string]cty.Value{
  1000  				"bucket": cty.StringVal("test"),
  1001  				"key":    cty.StringVal("test"),
  1002  			}),
  1003  			envVars: map[string]string{
  1004  				"NO_PROXY": "http://foo.bar,1.2.3.4",
  1005  			},
  1006  			calledURL: "http://foo.bar",
  1007  		},
  1008  		"shall satisfy no_proxy config attr when envvar NO_PROXY is also set": {
  1009  			config: cty.ObjectVal(map[string]cty.Value{
  1010  				"bucket":   cty.StringVal("test"),
  1011  				"key":      cty.StringVal("test"),
  1012  				"no_proxy": cty.StringVal("http://foo.qux,1.2.3.4"),
  1013  			}),
  1014  			envVars: map[string]string{
  1015  				"NO_PROXY": "http://foo.bar",
  1016  			},
  1017  			calledURL: "http://foo.qux",
  1018  		},
  1019  		"shall satisfy use http_proxy when no_proxy is also set to identical value": {
  1020  			config: cty.ObjectVal(map[string]cty.Value{
  1021  				"bucket":     cty.StringVal("test"),
  1022  				"key":        cty.StringVal("test"),
  1023  				"http_proxy": cty.StringVal("http://foo.bar"),
  1024  				"no_proxy":   cty.StringVal("http://foo.bar"),
  1025  			}),
  1026  			calledURL:    "http://qux.quxx",
  1027  			wantProxyURL: newURL("http://foo.bar"),
  1028  		},
  1029  		"shall satisfy use https_proxy when no_proxy is also set to identical value": {
  1030  			config: cty.ObjectVal(map[string]cty.Value{
  1031  				"bucket":      cty.StringVal("test"),
  1032  				"key":         cty.StringVal("test"),
  1033  				"https_proxy": cty.StringVal("https://foo.bar"),
  1034  				"no_proxy":    cty.StringVal("http://foo.bar"),
  1035  			}),
  1036  			calledURL:     "https://qux.quxx",
  1037  			wantErrSubstr: "proxyconnect tcp: dial tcp: lookup foo.bar",
  1038  		},
  1039  	}
  1040  
  1041  	for name, tc := range cases {
  1042  		t.Run(name, func(t *testing.T) {
  1043  			for k, v := range tc.envVars {
  1044  				t.Setenv(k, v)
  1045  			}
  1046  
  1047  			b := New(encryption.StateEncryptionDisabled())
  1048  
  1049  			got := b.Configure(populateSchema(t, b.ConfigSchema(), tc.config))
  1050  			if got.HasErrors() != (tc.wantErrSubstr != "") {
  1051  				t.Fatalf("unexpected error: %v", got.Err())
  1052  			}
  1053  
  1054  			switch got.HasErrors() {
  1055  			case true:
  1056  				if !strings.Contains(got.Err().Error(), tc.wantErrSubstr) {
  1057  					t.Fatalf("unexpected error: want= %s, got= %s", tc.wantErrSubstr, got.Err().Error())
  1058  				}
  1059  			case false:
  1060  				gotProxyURL, err := b.(*Backend).awsConfig.HTTPClient.(*awshttp.BuildableClient).GetTransport().Proxy(&http.Request{
  1061  					URL: newURL(tc.calledURL),
  1062  				})
  1063  				if err != nil {
  1064  					t.Fatalf("unexpected err: %v", err)
  1065  				}
  1066  
  1067  				if !reflect.DeepEqual(gotProxyURL, tc.wantProxyURL) {
  1068  					t.Fatalf("unexpected proxy URL: want= %s, got= %s", tc.wantProxyURL, gotProxyURL)
  1069  				}
  1070  			}
  1071  		})
  1072  	}
  1073  }
  1074  
  1075  func TestBackend(t *testing.T) {
  1076  	testACC(t)
  1077  
  1078  	bucketName := fmt.Sprintf("%s-%x", testBucketPrefix, time.Now().Unix())
  1079  	keyName := "testState"
  1080  
  1081  	b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1082  		"bucket":  bucketName,
  1083  		"key":     keyName,
  1084  		"encrypt": true,
  1085  		"region":  "us-west-1",
  1086  	})).(*Backend)
  1087  
  1088  	ctx := context.TODO()
  1089  	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
  1090  	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName)
  1091  
  1092  	backend.TestBackendStates(t, b)
  1093  }
  1094  
  1095  func TestBackendLocked(t *testing.T) {
  1096  	testACC(t)
  1097  
  1098  	bucketName := fmt.Sprintf("%s-%x", testBucketPrefix, time.Now().Unix())
  1099  	keyName := "test/state"
  1100  
  1101  	b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1102  		"bucket":         bucketName,
  1103  		"key":            keyName,
  1104  		"encrypt":        true,
  1105  		"dynamodb_table": bucketName,
  1106  		"region":         "us-west-1",
  1107  	})).(*Backend)
  1108  
  1109  	b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1110  		"bucket":         bucketName,
  1111  		"key":            keyName,
  1112  		"encrypt":        true,
  1113  		"dynamodb_table": bucketName,
  1114  		"region":         "us-west-1",
  1115  	})).(*Backend)
  1116  
  1117  	ctx := context.TODO()
  1118  	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
  1119  	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName)
  1120  	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
  1121  	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)
  1122  
  1123  	backend.TestBackendStateLocks(t, b1, b2)
  1124  	backend.TestBackendStateForceUnlock(t, b1, b2)
  1125  }
  1126  
  1127  func TestBackendSSECustomerKeyConfig(t *testing.T) {
  1128  	testACC(t)
  1129  
  1130  	testCases := map[string]struct {
  1131  		customerKey string
  1132  		expectedErr string
  1133  	}{
  1134  		"invalid length": {
  1135  			customerKey: "test",
  1136  			expectedErr: `sse_customer_key must be 44 characters in length`,
  1137  		},
  1138  		"invalid encoding": {
  1139  			customerKey: "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka",
  1140  			expectedErr: `sse_customer_key must be base64 encoded`,
  1141  		},
  1142  		"valid": {
  1143  			customerKey: "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
  1144  		},
  1145  	}
  1146  
  1147  	for name, testCase := range testCases {
  1148  		testCase := testCase
  1149  
  1150  		t.Run(name, func(t *testing.T) {
  1151  			bucketName := fmt.Sprintf("%s-%x", testBucketPrefix, time.Now().Unix())
  1152  			config := map[string]interface{}{
  1153  				"bucket":           bucketName,
  1154  				"encrypt":          true,
  1155  				"key":              "test-SSE-C",
  1156  				"sse_customer_key": testCase.customerKey,
  1157  				"region":           "us-west-1",
  1158  			}
  1159  
  1160  			b := New(encryption.StateEncryptionDisabled()).(*Backend)
  1161  			diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config)))
  1162  
  1163  			if testCase.expectedErr != "" {
  1164  				if diags.Err() != nil {
  1165  					actualErr := diags.Err().Error()
  1166  					if !strings.Contains(actualErr, testCase.expectedErr) {
  1167  						t.Fatalf("unexpected validation result: %v", diags.Err())
  1168  					}
  1169  				} else {
  1170  					t.Fatal("expected an error, got none")
  1171  				}
  1172  			} else {
  1173  				if diags.Err() != nil {
  1174  					t.Fatalf("expected no error, got %s", diags.Err())
  1175  				}
  1176  				if string(b.customerEncryptionKey) != string(must(base64.StdEncoding.DecodeString(testCase.customerKey))) {
  1177  					t.Fatal("unexpected value for customer encryption key")
  1178  				}
  1179  
  1180  				ctx := context.TODO()
  1181  				createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
  1182  				defer deleteS3Bucket(ctx, t, b.s3Client, bucketName)
  1183  
  1184  				backend.TestBackendStates(t, b)
  1185  			}
  1186  		})
  1187  	}
  1188  }
  1189  
  1190  func TestBackendSSECustomerKeyEnvVar(t *testing.T) {
  1191  	testACC(t)
  1192  
  1193  	testCases := map[string]struct {
  1194  		customerKey string
  1195  		expectedErr string
  1196  	}{
  1197  		"invalid length": {
  1198  			customerKey: "test",
  1199  			expectedErr: `The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`,
  1200  		},
  1201  		"invalid encoding": {
  1202  			customerKey: "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka",
  1203  			expectedErr: `The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded`,
  1204  		},
  1205  		"valid": {
  1206  			customerKey: "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
  1207  		},
  1208  	}
  1209  
  1210  	for name, testCase := range testCases {
  1211  		testCase := testCase
  1212  
  1213  		t.Run(name, func(t *testing.T) {
  1214  			bucketName := fmt.Sprintf("%s-%x", testBucketPrefix, time.Now().Unix())
  1215  			config := map[string]interface{}{
  1216  				"bucket":  bucketName,
  1217  				"encrypt": true,
  1218  				"key":     "test-SSE-C",
  1219  				"region":  "us-west-1",
  1220  			}
  1221  
  1222  			t.Setenv("AWS_SSE_CUSTOMER_KEY", testCase.customerKey)
  1223  
  1224  			b := New(encryption.StateEncryptionDisabled()).(*Backend)
  1225  			diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config)))
  1226  
  1227  			if testCase.expectedErr != "" {
  1228  				if diags.Err() != nil {
  1229  					actualErr := diags.Err().Error()
  1230  					if !strings.Contains(actualErr, testCase.expectedErr) {
  1231  						t.Fatalf("unexpected validation result: %v", diags.Err())
  1232  					}
  1233  				} else {
  1234  					t.Fatal("expected an error, got none")
  1235  				}
  1236  			} else {
  1237  				if diags.Err() != nil {
  1238  					t.Fatalf("expected no error, got %s", diags.Err())
  1239  				}
  1240  				if string(b.customerEncryptionKey) != string(must(base64.StdEncoding.DecodeString(testCase.customerKey))) {
  1241  					t.Fatal("unexpected value for customer encryption key")
  1242  				}
  1243  
  1244  				ctx := context.TODO()
  1245  				createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
  1246  				defer deleteS3Bucket(ctx, t, b.s3Client, bucketName)
  1247  
  1248  				backend.TestBackendStates(t, b)
  1249  			}
  1250  		})
  1251  	}
  1252  }
  1253  
  1254  // add some extra junk in S3 to try and confuse the env listing.
  1255  func TestBackendExtraPaths(t *testing.T) {
  1256  	testACC(t)
  1257  	bucketName := fmt.Sprintf("%s-%x", testBucketPrefix, time.Now().Unix())
  1258  	keyName := "test/state/tfstate"
  1259  
  1260  	b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1261  		"bucket":  bucketName,
  1262  		"key":     keyName,
  1263  		"encrypt": true,
  1264  	})).(*Backend)
  1265  
  1266  	ctx := context.TODO()
  1267  	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
  1268  	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName)
  1269  
  1270  	// put multiple states in old env paths.
  1271  	s1 := states.NewState()
  1272  	s2 := states.NewState()
  1273  
  1274  	// RemoteClient to Put things in various paths
  1275  	client := &RemoteClient{
  1276  		s3Client:             b.s3Client,
  1277  		dynClient:            b.dynClient,
  1278  		bucketName:           b.bucketName,
  1279  		path:                 b.path("s1"),
  1280  		serverSideEncryption: b.serverSideEncryption,
  1281  		acl:                  b.acl,
  1282  		kmsKeyID:             b.kmsKeyID,
  1283  		ddbTable:             b.ddbTable,
  1284  	}
  1285  
  1286  	// Write the first state
  1287  	stateMgr := &remote.State{Client: client}
  1288  	if err := stateMgr.WriteState(s1); err != nil {
  1289  		t.Fatal(err)
  1290  	}
  1291  	if err := stateMgr.PersistState(nil); err != nil {
  1292  		t.Fatal(err)
  1293  	}
  1294  
  1295  	// Write the second state
  1296  	// Note a new state manager - otherwise, because these
  1297  	// states are equal, the state will not Put to the remote
  1298  	client.path = b.path("s2")
  1299  	stateMgr2 := &remote.State{Client: client}
  1300  	if err := stateMgr2.WriteState(s2); err != nil {
  1301  		t.Fatal(err)
  1302  	}
  1303  	if err := stateMgr2.PersistState(nil); err != nil {
  1304  		t.Fatal(err)
  1305  	}
  1306  
  1307  	s2Lineage := stateMgr2.StateSnapshotMeta().Lineage
  1308  
  1309  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1310  		t.Fatal(err)
  1311  	}
  1312  
  1313  	// put a state in an env directory name
  1314  	client.path = b.workspaceKeyPrefix + "/error"
  1315  	if err := stateMgr.WriteState(states.NewState()); err != nil {
  1316  		t.Fatal(err)
  1317  	}
  1318  	if err := stateMgr.PersistState(nil); err != nil {
  1319  		t.Fatal(err)
  1320  	}
  1321  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1322  		t.Fatal(err)
  1323  	}
  1324  
  1325  	// add state with the wrong key for an existing env
  1326  	client.path = b.workspaceKeyPrefix + "/s2/notTestState"
  1327  	if err := stateMgr.WriteState(states.NewState()); err != nil {
  1328  		t.Fatal(err)
  1329  	}
  1330  	if err := stateMgr.PersistState(nil); err != nil {
  1331  		t.Fatal(err)
  1332  	}
  1333  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1334  		t.Fatal(err)
  1335  	}
  1336  
  1337  	// remove the state with extra subkey
  1338  	if err := client.Delete(); err != nil {
  1339  		t.Fatal(err)
  1340  	}
  1341  
  1342  	// delete the real workspace
  1343  	if err := b.DeleteWorkspace("s2", true); err != nil {
  1344  		t.Fatal(err)
  1345  	}
  1346  
  1347  	if err := checkStateList(b, []string{"default", "s1"}); err != nil {
  1348  		t.Fatal(err)
  1349  	}
  1350  
  1351  	// fetch that state again, which should produce a new lineage
  1352  	s2Mgr, err := b.StateMgr("s2")
  1353  	if err != nil {
  1354  		t.Fatal(err)
  1355  	}
  1356  	if err := s2Mgr.RefreshState(); err != nil {
  1357  		t.Fatal(err)
  1358  	}
  1359  
  1360  	if s2Mgr.(*remote.State).StateSnapshotMeta().Lineage == s2Lineage {
  1361  		t.Fatal("state s2 was not deleted")
  1362  	}
  1363  	_ = s2Mgr.State() // We need the side-effect
  1364  	s2Lineage = stateMgr.StateSnapshotMeta().Lineage
  1365  
  1366  	// add a state with a key that matches an existing environment dir name
  1367  	client.path = b.workspaceKeyPrefix + "/s2/"
  1368  	if err := stateMgr.WriteState(states.NewState()); err != nil {
  1369  		t.Fatal(err)
  1370  	}
  1371  	if err := stateMgr.PersistState(nil); err != nil {
  1372  		t.Fatal(err)
  1373  	}
  1374  
  1375  	// make sure s2 is OK
  1376  	s2Mgr, err = b.StateMgr("s2")
  1377  	if err != nil {
  1378  		t.Fatal(err)
  1379  	}
  1380  	if err := s2Mgr.RefreshState(); err != nil {
  1381  		t.Fatal(err)
  1382  	}
  1383  
  1384  	if stateMgr.StateSnapshotMeta().Lineage != s2Lineage {
  1385  		t.Fatal("we got the wrong state for s2")
  1386  	}
  1387  
  1388  	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
  1389  		t.Fatal(err)
  1390  	}
  1391  }
  1392  
  1393  // ensure we can separate the workspace prefix when it also matches the prefix
  1394  // of the workspace name itself.
  1395  func TestBackendPrefixInWorkspace(t *testing.T) {
  1396  	testACC(t)
  1397  	bucketName := fmt.Sprintf("%s-%x", testBucketPrefix, time.Now().Unix())
  1398  
  1399  	b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1400  		"bucket":               bucketName,
  1401  		"key":                  "test-env.tfstate",
  1402  		"workspace_key_prefix": "env",
  1403  	})).(*Backend)
  1404  
  1405  	ctx := context.TODO()
  1406  	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
  1407  	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName)
  1408  
  1409  	// get a state that contains the prefix as a substring
  1410  	sMgr, err := b.StateMgr("env-1")
  1411  	if err != nil {
  1412  		t.Fatal(err)
  1413  	}
  1414  	if err := sMgr.RefreshState(); err != nil {
  1415  		t.Fatal(err)
  1416  	}
  1417  
  1418  	if err := checkStateList(b, []string{"default", "env-1"}); err != nil {
  1419  		t.Fatal(err)
  1420  	}
  1421  }
  1422  
  1423  func TestKeyEnv(t *testing.T) {
  1424  	testACC(t)
  1425  	keyName := "some/paths/tfstate"
  1426  
  1427  	bucket0Name := fmt.Sprintf("%s-%x-0", testBucketPrefix, time.Now().Unix())
  1428  	b0 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1429  		"bucket":               bucket0Name,
  1430  		"key":                  keyName,
  1431  		"encrypt":              true,
  1432  		"workspace_key_prefix": "",
  1433  	})).(*Backend)
  1434  
  1435  	ctx := context.TODO()
  1436  	createS3Bucket(ctx, t, b0.s3Client, bucket0Name, b0.awsConfig.Region)
  1437  	defer deleteS3Bucket(ctx, t, b0.s3Client, bucket0Name)
  1438  
  1439  	bucket1Name := fmt.Sprintf("%s-%x-1", testBucketPrefix, time.Now().Unix())
  1440  	b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1441  		"bucket":               bucket1Name,
  1442  		"key":                  keyName,
  1443  		"encrypt":              true,
  1444  		"workspace_key_prefix": "project/env:",
  1445  	})).(*Backend)
  1446  
  1447  	createS3Bucket(ctx, t, b1.s3Client, bucket1Name, b1.awsConfig.Region)
  1448  	defer deleteS3Bucket(ctx, t, b1.s3Client, bucket1Name)
  1449  
  1450  	bucket2Name := fmt.Sprintf("%s-%x-2", testBucketPrefix, time.Now().Unix())
  1451  	b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
  1452  		"bucket":  bucket2Name,
  1453  		"key":     keyName,
  1454  		"encrypt": true,
  1455  	})).(*Backend)
  1456  
  1457  	createS3Bucket(ctx, t, b2.s3Client, bucket2Name, b2.awsConfig.Region)
  1458  	defer deleteS3Bucket(ctx, t, b2.s3Client, bucket2Name)
  1459  
  1460  	if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil {
  1461  		t.Fatal(err)
  1462  	}
  1463  
  1464  	if err := testGetWorkspaceForKey(b0, "ws1/some/paths/tfstate", "ws1"); err != nil {
  1465  		t.Fatal(err)
  1466  	}
  1467  
  1468  	if err := testGetWorkspaceForKey(b1, "project/env:/ws1/some/paths/tfstate", "ws1"); err != nil {
  1469  		t.Fatal(err)
  1470  	}
  1471  
  1472  	if err := testGetWorkspaceForKey(b1, "project/env:/ws2/some/paths/tfstate", "ws2"); err != nil {
  1473  		t.Fatal(err)
  1474  	}
  1475  
  1476  	if err := testGetWorkspaceForKey(b2, "env:/ws3/some/paths/tfstate", "ws3"); err != nil {
  1477  		t.Fatal(err)
  1478  	}
  1479  
  1480  	backend.TestBackendStates(t, b0)
  1481  	backend.TestBackendStates(t, b1)
  1482  	backend.TestBackendStates(t, b2)
  1483  }
  1484  
  1485  func Test_pathString(t *testing.T) {
  1486  	tests := []struct {
  1487  		name     string
  1488  		path     cty.Path
  1489  		expected string
  1490  	}{
  1491  		{
  1492  			name:     "Simple Path",
  1493  			path:     cty.Path{cty.GetAttrStep{Name: "attr"}},
  1494  			expected: "attr",
  1495  		},
  1496  		{
  1497  			name: "Nested Path",
  1498  			path: cty.Path{
  1499  				cty.GetAttrStep{Name: "parent"},
  1500  				cty.GetAttrStep{Name: "child"},
  1501  			},
  1502  			expected: "parent.child",
  1503  		},
  1504  		{
  1505  			name: "Indexed Path",
  1506  			path: cty.Path{
  1507  				cty.GetAttrStep{Name: "array"},
  1508  				cty.IndexStep{Key: cty.NumberIntVal(0)},
  1509  			},
  1510  			expected: "array[0]",
  1511  		},
  1512  		{
  1513  			name: "Mixed Path",
  1514  			path: cty.Path{
  1515  				cty.GetAttrStep{Name: "parent"},
  1516  				cty.IndexStep{Key: cty.StringVal("key")},
  1517  				cty.GetAttrStep{Name: "child"},
  1518  			},
  1519  			expected: "parent[key].child",
  1520  		},
  1521  	}
  1522  
  1523  	for _, test := range tests {
  1524  		t.Run(test.name, func(t *testing.T) {
  1525  			result := pathString(test.path)
  1526  			if result != test.expected {
  1527  				t.Errorf("Expected: %s, Got: %s", test.expected, result)
  1528  			}
  1529  		})
  1530  	}
  1531  }
  1532  
  1533  func TestBackend_includeProtoIfNessesary(t *testing.T) {
  1534  	tests := []struct {
  1535  		name     string
  1536  		provided string
  1537  		expected string
  1538  	}{
  1539  		{
  1540  			name:     "Unmodified S3",
  1541  			provided: "https://s3.us-east-1.amazonaws.com",
  1542  			expected: "https://s3.us-east-1.amazonaws.com",
  1543  		},
  1544  		{
  1545  			name:     "Modified S3",
  1546  			provided: "s3.us-east-1.amazonaws.com",
  1547  			expected: "https://s3.us-east-1.amazonaws.com",
  1548  		},
  1549  		{
  1550  			name:     "Unmodified With Port",
  1551  			provided: "http://localhost:9000/",
  1552  			expected: "http://localhost:9000/",
  1553  		},
  1554  		{
  1555  			name:     "Modified With Port",
  1556  			provided: "localhost:9000/",
  1557  			expected: "https://localhost:9000/",
  1558  		},
  1559  		{
  1560  			name:     "Umodified with strange proto",
  1561  			provided: "ftp://localhost:9000/",
  1562  			expected: "ftp://localhost:9000/",
  1563  		},
  1564  	}
  1565  
  1566  	for _, test := range tests {
  1567  		t.Run(test.name, func(t *testing.T) {
  1568  			result := includeProtoIfNessesary(test.provided)
  1569  			if result != test.expected {
  1570  				t.Errorf("Expected: %s, Got: %s", test.expected, result)
  1571  			}
  1572  		})
  1573  	}
  1574  }
  1575  
  1576  func TestBackend_schemaCoercionMinimal(t *testing.T) {
  1577  	example := cty.ObjectVal(map[string]cty.Value{
  1578  		"bucket": cty.StringVal("my-bucket"),
  1579  		"key":    cty.StringVal("state.tf"),
  1580  	})
  1581  	schema := New(encryption.StateEncryptionDisabled()).ConfigSchema()
  1582  	_, err := schema.CoerceValue(example)
  1583  	if err != nil {
  1584  		t.Errorf("Unexpected error: %s", err.Error())
  1585  	}
  1586  }
  1587  
  1588  func testGetWorkspaceForKey(b *Backend, key string, expected string) error {
  1589  	if actual := b.keyEnv(key); actual != expected {
  1590  		return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual)
  1591  	}
  1592  	return nil
  1593  }
  1594  
  1595  func checkStateList(b backend.Backend, expected []string) error {
  1596  	states, err := b.Workspaces()
  1597  	if err != nil {
  1598  		return err
  1599  	}
  1600  
  1601  	if !reflect.DeepEqual(states, expected) {
  1602  		return fmt.Errorf("incorrect states listed: %q", states)
  1603  	}
  1604  	return nil
  1605  }
  1606  
  1607  func createS3Bucket(ctx context.Context, t *testing.T, s3Client *s3.Client, bucketName, region string) {
  1608  	createBucketReq := &s3.CreateBucketInput{
  1609  		Bucket: &bucketName,
  1610  	}
  1611  
  1612  	// Regions outside of us-east-1 require the appropriate LocationConstraint
  1613  	// to be specified in order to create the bucket in the desired region.
  1614  	// https://docs.aws.amazon.com/cli/latest/reference/s3api/create-bucket.html
  1615  	if region != "us-east-1" {
  1616  		createBucketReq.CreateBucketConfiguration = &types.CreateBucketConfiguration{
  1617  			LocationConstraint: types.BucketLocationConstraint(region),
  1618  		}
  1619  	}
  1620  
  1621  	// Be clear about what we're doing in case the user needs to clean
  1622  	// this up later.
  1623  	t.Logf("creating S3 bucket %s in %s", bucketName, region)
  1624  	_, err := s3Client.CreateBucket(ctx, createBucketReq)
  1625  	if err != nil {
  1626  		t.Fatal("failed to create test S3 bucket:", err)
  1627  	}
  1628  }
  1629  
  1630  func deleteS3Bucket(ctx context.Context, t *testing.T, s3Client *s3.Client, bucketName string) {
  1631  	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)"
  1632  
  1633  	// first we have to get rid of the env objects, or we can't delete the bucket
  1634  	resp, err := s3Client.ListObjects(ctx, &s3.ListObjectsInput{Bucket: &bucketName})
  1635  	if err != nil {
  1636  		t.Logf(warning, err)
  1637  		return
  1638  	}
  1639  	for _, obj := range resp.Contents {
  1640  		if _, err := s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &bucketName, Key: obj.Key}); err != nil {
  1641  			// this will need cleanup no matter what, so just warn and exit
  1642  			t.Logf(warning, err)
  1643  			return
  1644  		}
  1645  	}
  1646  
  1647  	if _, err := s3Client.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucketName}); err != nil {
  1648  		t.Logf(warning, err)
  1649  	}
  1650  }
  1651  
  1652  // create the dynamoDB table, and wait until we can query it.
  1653  func createDynamoDBTable(ctx context.Context, t *testing.T, dynClient *dynamodb.Client, tableName string) {
  1654  	createInput := &dynamodb.CreateTableInput{
  1655  		AttributeDefinitions: []dtypes.AttributeDefinition{
  1656  			{
  1657  				AttributeName: aws.String("LockID"),
  1658  				AttributeType: dtypes.ScalarAttributeTypeS,
  1659  			},
  1660  		},
  1661  		KeySchema: []dtypes.KeySchemaElement{
  1662  			{
  1663  				AttributeName: aws.String("LockID"),
  1664  				KeyType:       dtypes.KeyTypeHash,
  1665  			},
  1666  		},
  1667  		ProvisionedThroughput: &dtypes.ProvisionedThroughput{
  1668  			ReadCapacityUnits:  aws.Int64(5),
  1669  			WriteCapacityUnits: aws.Int64(5),
  1670  		},
  1671  		TableName: aws.String(tableName),
  1672  	}
  1673  
  1674  	_, err := dynClient.CreateTable(ctx, createInput)
  1675  	if err != nil {
  1676  		t.Fatal(err)
  1677  	}
  1678  
  1679  	// now wait until it's ACTIVE
  1680  	start := time.Now()
  1681  	time.Sleep(time.Second)
  1682  
  1683  	describeInput := &dynamodb.DescribeTableInput{
  1684  		TableName: aws.String(tableName),
  1685  	}
  1686  
  1687  	for {
  1688  		resp, err := dynClient.DescribeTable(ctx, describeInput)
  1689  		if err != nil {
  1690  			t.Fatal(err)
  1691  		}
  1692  
  1693  		if resp.Table.TableStatus == dtypes.TableStatusActive {
  1694  			return
  1695  		}
  1696  
  1697  		if time.Since(start) > time.Minute {
  1698  			t.Fatalf("timed out creating DynamoDB table %s", tableName)
  1699  		}
  1700  
  1701  		time.Sleep(3 * time.Second)
  1702  	}
  1703  
  1704  }
  1705  
  1706  func deleteDynamoDBTable(ctx context.Context, t *testing.T, dynClient *dynamodb.Client, tableName string) {
  1707  	params := &dynamodb.DeleteTableInput{
  1708  		TableName: aws.String(tableName),
  1709  	}
  1710  	_, err := dynClient.DeleteTable(ctx, params)
  1711  	if err != nil {
  1712  		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)
  1713  	}
  1714  }
  1715  
  1716  func populateSchema(t *testing.T, schema *configschema.Block, value cty.Value) cty.Value {
  1717  	ty := schema.ImpliedType()
  1718  	var path cty.Path
  1719  	val, err := unmarshal(value, ty, path)
  1720  	if err != nil {
  1721  		t.Fatalf("populating schema: %s", err)
  1722  	}
  1723  	return val
  1724  }
  1725  
  1726  func unmarshal(value cty.Value, ty cty.Type, path cty.Path) (cty.Value, error) {
  1727  	switch {
  1728  	case ty.IsPrimitiveType():
  1729  		return value, nil
  1730  	// case ty.IsListType():
  1731  	// 	return unmarshalList(value, ty.ElementType(), path)
  1732  	case ty.IsSetType():
  1733  		return unmarshalSet(value, ty.ElementType(), path)
  1734  	case ty.IsMapType():
  1735  		return unmarshalMap(value, ty.ElementType(), path)
  1736  	// case ty.IsTupleType():
  1737  	// 	return unmarshalTuple(value, ty.TupleElementTypes(), path)
  1738  	case ty.IsObjectType():
  1739  		return unmarshalObject(value, ty.AttributeTypes(), path)
  1740  	default:
  1741  		return cty.NilVal, path.NewErrorf("unsupported type %s", ty.FriendlyName())
  1742  	}
  1743  }
  1744  
  1745  func unmarshalSet(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
  1746  	if dec.IsNull() {
  1747  		return dec, nil
  1748  	}
  1749  
  1750  	length := dec.LengthInt()
  1751  
  1752  	if length == 0 {
  1753  		return cty.SetValEmpty(ety), nil
  1754  	}
  1755  
  1756  	vals := make([]cty.Value, 0, length)
  1757  	dec.ForEachElement(func(key, val cty.Value) (stop bool) {
  1758  		vals = append(vals, val)
  1759  		return
  1760  	})
  1761  
  1762  	return cty.SetVal(vals), nil
  1763  }
  1764  
  1765  func unmarshalMap(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
  1766  	if dec.IsNull() {
  1767  		return dec, nil
  1768  	}
  1769  
  1770  	length := dec.LengthInt()
  1771  
  1772  	if length == 0 {
  1773  		return cty.MapValEmpty(ety), nil
  1774  	}
  1775  
  1776  	vals := make(map[string]cty.Value, length)
  1777  	dec.ForEachElement(func(key, val cty.Value) (stop bool) {
  1778  		k := stringValue(key)
  1779  		vals[k] = val
  1780  		return
  1781  	})
  1782  
  1783  	return cty.MapVal(vals), nil
  1784  }
  1785  
  1786  func unmarshalObject(dec cty.Value, atys map[string]cty.Type, path cty.Path) (cty.Value, error) {
  1787  	if dec.IsNull() {
  1788  		return dec, nil
  1789  	}
  1790  	valueTy := dec.Type()
  1791  
  1792  	vals := make(map[string]cty.Value, len(atys))
  1793  	path = append(path, nil)
  1794  	for key, aty := range atys {
  1795  		path[len(path)-1] = cty.IndexStep{
  1796  			Key: cty.StringVal(key),
  1797  		}
  1798  
  1799  		if !valueTy.HasAttribute(key) {
  1800  			vals[key] = cty.NullVal(aty)
  1801  		} else {
  1802  			val, err := unmarshal(dec.GetAttr(key), aty, path)
  1803  			if err != nil {
  1804  				return cty.DynamicVal, err
  1805  			}
  1806  			vals[key] = val
  1807  		}
  1808  	}
  1809  
  1810  	return cty.ObjectVal(vals), nil
  1811  }
  1812  
  1813  func must[T any](v T, err error) T {
  1814  	if err != nil {
  1815  		panic(err)
  1816  	} else {
  1817  		return v
  1818  	}
  1819  }