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 }