github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote-state/s3/backend_test.go (about) 1 package s3 2 3 import ( 4 "fmt" 5 "os" 6 "reflect" 7 "testing" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/service/dynamodb" 12 "github.com/aws/aws-sdk-go/service/s3" 13 "github.com/hashicorp/terraform/backend" 14 "github.com/hashicorp/terraform/configs/hcl2shim" 15 "github.com/hashicorp/terraform/state/remote" 16 "github.com/hashicorp/terraform/states" 17 ) 18 19 // verify that we are doing ACC tests or the S3 tests specifically 20 func testACC(t *testing.T) { 21 skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_S3_TEST") == "" 22 if skip { 23 t.Log("s3 backend tests require setting TF_ACC or TF_S3_TEST") 24 t.Skip() 25 } 26 if os.Getenv("AWS_DEFAULT_REGION") == "" { 27 os.Setenv("AWS_DEFAULT_REGION", "us-west-2") 28 } 29 } 30 31 func TestBackend_impl(t *testing.T) { 32 var _ backend.Backend = new(Backend) 33 } 34 35 func TestBackendConfig(t *testing.T) { 36 testACC(t) 37 config := map[string]interface{}{ 38 "region": "us-west-1", 39 "bucket": "tf-test", 40 "key": "state", 41 "encrypt": true, 42 "dynamodb_table": "dynamoTable", 43 } 44 45 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) 46 47 if *b.s3Client.Config.Region != "us-west-1" { 48 t.Fatalf("Incorrect region was populated") 49 } 50 if b.bucketName != "tf-test" { 51 t.Fatalf("Incorrect bucketName was populated") 52 } 53 if b.keyName != "state" { 54 t.Fatalf("Incorrect keyName was populated") 55 } 56 57 credentials, err := b.s3Client.Config.Credentials.Get() 58 if err != nil { 59 t.Fatalf("Error when requesting credentials") 60 } 61 if credentials.AccessKeyID == "" { 62 t.Fatalf("No Access Key Id was populated") 63 } 64 if credentials.SecretAccessKey == "" { 65 t.Fatalf("No Secret Access Key was populated") 66 } 67 } 68 69 func TestBackendConfig_invalidKey(t *testing.T) { 70 testACC(t) 71 cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ 72 "region": "us-west-1", 73 "bucket": "tf-test", 74 "key": "/leading-slash", 75 "encrypt": true, 76 "dynamodb_table": "dynamoTable", 77 }) 78 79 _, diags := New().PrepareConfig(cfg) 80 if !diags.HasErrors() { 81 t.Fatal("expected config validation error") 82 } 83 } 84 85 func TestBackendConfig_invalidSSECustomerKeyLength(t *testing.T) { 86 testACC(t) 87 cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ 88 "region": "us-west-1", 89 "bucket": "tf-test", 90 "encrypt": true, 91 "key": "state", 92 "dynamodb_table": "dynamoTable", 93 "sse_customer_key": "key", 94 }) 95 96 _, diags := New().PrepareConfig(cfg) 97 if !diags.HasErrors() { 98 t.Fatal("expected error for invalid sse_customer_key length") 99 } 100 } 101 102 func TestBackendConfig_invalidSSECustomerKeyEncoding(t *testing.T) { 103 testACC(t) 104 cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ 105 "region": "us-west-1", 106 "bucket": "tf-test", 107 "encrypt": true, 108 "key": "state", 109 "dynamodb_table": "dynamoTable", 110 "sse_customer_key": "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka", 111 }) 112 113 diags := New().Configure(cfg) 114 if !diags.HasErrors() { 115 t.Fatal("expected error for failing to decode sse_customer_key") 116 } 117 } 118 119 func TestBackendConfig_conflictingEncryptionSchema(t *testing.T) { 120 testACC(t) 121 cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ 122 "region": "us-west-1", 123 "bucket": "tf-test", 124 "key": "state", 125 "encrypt": true, 126 "dynamodb_table": "dynamoTable", 127 "sse_customer_key": "1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o=", 128 "kms_key_id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", 129 }) 130 131 diags := New().Configure(cfg) 132 if !diags.HasErrors() { 133 t.Fatal("expected error for simultaneous usage of kms_key_id and sse_customer_key") 134 } 135 } 136 137 func TestBackend(t *testing.T) { 138 testACC(t) 139 140 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 141 keyName := "testState" 142 143 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 144 "bucket": bucketName, 145 "key": keyName, 146 "encrypt": true, 147 })).(*Backend) 148 149 createS3Bucket(t, b.s3Client, bucketName) 150 defer deleteS3Bucket(t, b.s3Client, bucketName) 151 152 backend.TestBackendStates(t, b) 153 } 154 155 func TestBackendLocked(t *testing.T) { 156 testACC(t) 157 158 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 159 keyName := "test/state" 160 161 b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 162 "bucket": bucketName, 163 "key": keyName, 164 "encrypt": true, 165 "dynamodb_table": bucketName, 166 })).(*Backend) 167 168 b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 169 "bucket": bucketName, 170 "key": keyName, 171 "encrypt": true, 172 "dynamodb_table": bucketName, 173 })).(*Backend) 174 175 createS3Bucket(t, b1.s3Client, bucketName) 176 defer deleteS3Bucket(t, b1.s3Client, bucketName) 177 createDynamoDBTable(t, b1.dynClient, bucketName) 178 defer deleteDynamoDBTable(t, b1.dynClient, bucketName) 179 180 backend.TestBackendStateLocks(t, b1, b2) 181 backend.TestBackendStateForceUnlock(t, b1, b2) 182 } 183 184 func TestBackendSSECustomerKey(t *testing.T) { 185 testACC(t) 186 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 187 188 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 189 "bucket": bucketName, 190 "encrypt": true, 191 "key": "test-SSE-C", 192 "sse_customer_key": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=", 193 })).(*Backend) 194 195 createS3Bucket(t, b.s3Client, bucketName) 196 defer deleteS3Bucket(t, b.s3Client, bucketName) 197 198 backend.TestBackendStates(t, b) 199 } 200 201 // add some extra junk in S3 to try and confuse the env listing. 202 func TestBackendExtraPaths(t *testing.T) { 203 testACC(t) 204 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 205 keyName := "test/state/tfstate" 206 207 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 208 "bucket": bucketName, 209 "key": keyName, 210 "encrypt": true, 211 })).(*Backend) 212 213 createS3Bucket(t, b.s3Client, bucketName) 214 defer deleteS3Bucket(t, b.s3Client, bucketName) 215 216 // put multiple states in old env paths. 217 s1 := states.NewState() 218 s2 := states.NewState() 219 220 // RemoteClient to Put things in various paths 221 client := &RemoteClient{ 222 s3Client: b.s3Client, 223 dynClient: b.dynClient, 224 bucketName: b.bucketName, 225 path: b.path("s1"), 226 serverSideEncryption: b.serverSideEncryption, 227 acl: b.acl, 228 kmsKeyID: b.kmsKeyID, 229 ddbTable: b.ddbTable, 230 } 231 232 // Write the first state 233 stateMgr := &remote.State{Client: client} 234 stateMgr.WriteState(s1) 235 if err := stateMgr.PersistState(); err != nil { 236 t.Fatal(err) 237 } 238 239 // Write the second state 240 // Note a new state manager - otherwise, because these 241 // states are equal, the state will not Put to the remote 242 client.path = b.path("s2") 243 stateMgr2 := &remote.State{Client: client} 244 stateMgr2.WriteState(s2) 245 if err := stateMgr2.PersistState(); err != nil { 246 t.Fatal(err) 247 } 248 249 s2Lineage := stateMgr2.StateSnapshotMeta().Lineage 250 251 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 252 t.Fatal(err) 253 } 254 255 // put a state in an env directory name 256 client.path = b.workspaceKeyPrefix + "/error" 257 stateMgr.WriteState(states.NewState()) 258 if err := stateMgr.PersistState(); err != nil { 259 t.Fatal(err) 260 } 261 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 262 t.Fatal(err) 263 } 264 265 // add state with the wrong key for an existing env 266 client.path = b.workspaceKeyPrefix + "/s2/notTestState" 267 stateMgr.WriteState(states.NewState()) 268 if err := stateMgr.PersistState(); err != nil { 269 t.Fatal(err) 270 } 271 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 272 t.Fatal(err) 273 } 274 275 // remove the state with extra subkey 276 if err := client.Delete(); err != nil { 277 t.Fatal(err) 278 } 279 280 // delete the real workspace 281 if err := b.DeleteWorkspace("s2"); err != nil { 282 t.Fatal(err) 283 } 284 285 if err := checkStateList(b, []string{"default", "s1"}); err != nil { 286 t.Fatal(err) 287 } 288 289 // fetch that state again, which should produce a new lineage 290 s2Mgr, err := b.StateMgr("s2") 291 if err != nil { 292 t.Fatal(err) 293 } 294 if err := s2Mgr.RefreshState(); err != nil { 295 t.Fatal(err) 296 } 297 298 if s2Mgr.(*remote.State).StateSnapshotMeta().Lineage == s2Lineage { 299 t.Fatal("state s2 was not deleted") 300 } 301 s2 = s2Mgr.State() 302 s2Lineage = stateMgr.StateSnapshotMeta().Lineage 303 304 // add a state with a key that matches an existing environment dir name 305 client.path = b.workspaceKeyPrefix + "/s2/" 306 stateMgr.WriteState(states.NewState()) 307 if err := stateMgr.PersistState(); err != nil { 308 t.Fatal(err) 309 } 310 311 // make sure s2 is OK 312 s2Mgr, err = b.StateMgr("s2") 313 if err != nil { 314 t.Fatal(err) 315 } 316 if err := s2Mgr.RefreshState(); err != nil { 317 t.Fatal(err) 318 } 319 320 if stateMgr.StateSnapshotMeta().Lineage != s2Lineage { 321 t.Fatal("we got the wrong state for s2") 322 } 323 324 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 325 t.Fatal(err) 326 } 327 } 328 329 // ensure we can separate the workspace prefix when it also matches the prefix 330 // of the workspace name itself. 331 func TestBackendPrefixInWorkspace(t *testing.T) { 332 testACC(t) 333 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 334 335 b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 336 "bucket": bucketName, 337 "key": "test-env.tfstate", 338 "workspace_key_prefix": "env", 339 })).(*Backend) 340 341 createS3Bucket(t, b.s3Client, bucketName) 342 defer deleteS3Bucket(t, b.s3Client, bucketName) 343 344 // get a state that contains the prefix as a substring 345 sMgr, err := b.StateMgr("env-1") 346 if err != nil { 347 t.Fatal(err) 348 } 349 if err := sMgr.RefreshState(); err != nil { 350 t.Fatal(err) 351 } 352 353 if err := checkStateList(b, []string{"default", "env-1"}); err != nil { 354 t.Fatal(err) 355 } 356 } 357 358 func TestKeyEnv(t *testing.T) { 359 testACC(t) 360 keyName := "some/paths/tfstate" 361 362 bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix()) 363 b0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 364 "bucket": bucket0Name, 365 "key": keyName, 366 "encrypt": true, 367 "workspace_key_prefix": "", 368 })).(*Backend) 369 370 createS3Bucket(t, b0.s3Client, bucket0Name) 371 defer deleteS3Bucket(t, b0.s3Client, bucket0Name) 372 373 bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix()) 374 b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 375 "bucket": bucket1Name, 376 "key": keyName, 377 "encrypt": true, 378 "workspace_key_prefix": "project/env:", 379 })).(*Backend) 380 381 createS3Bucket(t, b1.s3Client, bucket1Name) 382 defer deleteS3Bucket(t, b1.s3Client, bucket1Name) 383 384 bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix()) 385 b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ 386 "bucket": bucket2Name, 387 "key": keyName, 388 "encrypt": true, 389 })).(*Backend) 390 391 createS3Bucket(t, b2.s3Client, bucket2Name) 392 defer deleteS3Bucket(t, b2.s3Client, bucket2Name) 393 394 if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil { 395 t.Fatal(err) 396 } 397 398 if err := testGetWorkspaceForKey(b0, "ws1/some/paths/tfstate", "ws1"); err != nil { 399 t.Fatal(err) 400 } 401 402 if err := testGetWorkspaceForKey(b1, "project/env:/ws1/some/paths/tfstate", "ws1"); err != nil { 403 t.Fatal(err) 404 } 405 406 if err := testGetWorkspaceForKey(b1, "project/env:/ws2/some/paths/tfstate", "ws2"); err != nil { 407 t.Fatal(err) 408 } 409 410 if err := testGetWorkspaceForKey(b2, "env:/ws3/some/paths/tfstate", "ws3"); err != nil { 411 t.Fatal(err) 412 } 413 414 backend.TestBackendStates(t, b0) 415 backend.TestBackendStates(t, b1) 416 backend.TestBackendStates(t, b2) 417 } 418 419 func testGetWorkspaceForKey(b *Backend, key string, expected string) error { 420 if actual := b.keyEnv(key); actual != expected { 421 return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual) 422 } 423 return nil 424 } 425 426 func checkStateList(b backend.Backend, expected []string) error { 427 states, err := b.Workspaces() 428 if err != nil { 429 return err 430 } 431 432 if !reflect.DeepEqual(states, expected) { 433 return fmt.Errorf("incorrect states listed: %q", states) 434 } 435 return nil 436 } 437 438 func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { 439 createBucketReq := &s3.CreateBucketInput{ 440 Bucket: &bucketName, 441 } 442 443 // Be clear about what we're doing in case the user needs to clean 444 // this up later. 445 t.Logf("creating S3 bucket %s in %s", bucketName, *s3Client.Config.Region) 446 _, err := s3Client.CreateBucket(createBucketReq) 447 if err != nil { 448 t.Fatal("failed to create test S3 bucket:", err) 449 } 450 } 451 452 func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { 453 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)" 454 455 // first we have to get rid of the env objects, or we can't delete the bucket 456 resp, err := s3Client.ListObjects(&s3.ListObjectsInput{Bucket: &bucketName}) 457 if err != nil { 458 t.Logf(warning, err) 459 return 460 } 461 for _, obj := range resp.Contents { 462 if _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{Bucket: &bucketName, Key: obj.Key}); err != nil { 463 // this will need cleanup no matter what, so just warn and exit 464 t.Logf(warning, err) 465 return 466 } 467 } 468 469 if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil { 470 t.Logf(warning, err) 471 } 472 } 473 474 // create the dynamoDB table, and wait until we can query it. 475 func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) { 476 createInput := &dynamodb.CreateTableInput{ 477 AttributeDefinitions: []*dynamodb.AttributeDefinition{ 478 { 479 AttributeName: aws.String("LockID"), 480 AttributeType: aws.String("S"), 481 }, 482 }, 483 KeySchema: []*dynamodb.KeySchemaElement{ 484 { 485 AttributeName: aws.String("LockID"), 486 KeyType: aws.String("HASH"), 487 }, 488 }, 489 ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ 490 ReadCapacityUnits: aws.Int64(5), 491 WriteCapacityUnits: aws.Int64(5), 492 }, 493 TableName: aws.String(tableName), 494 } 495 496 _, err := dynClient.CreateTable(createInput) 497 if err != nil { 498 t.Fatal(err) 499 } 500 501 // now wait until it's ACTIVE 502 start := time.Now() 503 time.Sleep(time.Second) 504 505 describeInput := &dynamodb.DescribeTableInput{ 506 TableName: aws.String(tableName), 507 } 508 509 for { 510 resp, err := dynClient.DescribeTable(describeInput) 511 if err != nil { 512 t.Fatal(err) 513 } 514 515 if *resp.Table.TableStatus == "ACTIVE" { 516 return 517 } 518 519 if time.Since(start) > time.Minute { 520 t.Fatalf("timed out creating DynamoDB table %s", tableName) 521 } 522 523 time.Sleep(3 * time.Second) 524 } 525 526 } 527 528 func deleteDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) { 529 params := &dynamodb.DeleteTableInput{ 530 TableName: aws.String(tableName), 531 } 532 _, err := dynClient.DeleteTable(params) 533 if err != nil { 534 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) 535 } 536 }