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