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 }