github.com/kcburge/terraform@v0.11.12-beta1/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/config" 15 "github.com/hashicorp/terraform/state/remote" 16 "github.com/hashicorp/terraform/terraform" 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(), 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 := 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 rawCfg, err := config.NewRawConfig(cfg) 80 if err != nil { 81 t.Fatal(err) 82 } 83 resCfg := terraform.NewResourceConfig(rawCfg) 84 85 _, errs := New().Validate(resCfg) 86 if len(errs) != 1 { 87 t.Fatal("expected config validation error") 88 } 89 } 90 91 func TestBackend(t *testing.T) { 92 testACC(t) 93 94 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 95 keyName := "testState" 96 97 b := backend.TestBackendConfig(t, New(), map[string]interface{}{ 98 "bucket": bucketName, 99 "key": keyName, 100 "encrypt": true, 101 }).(*Backend) 102 103 createS3Bucket(t, b.s3Client, bucketName) 104 defer deleteS3Bucket(t, b.s3Client, bucketName) 105 106 backend.TestBackendStates(t, b) 107 } 108 109 func TestBackendLocked(t *testing.T) { 110 testACC(t) 111 112 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 113 keyName := "test/state" 114 115 b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ 116 "bucket": bucketName, 117 "key": keyName, 118 "encrypt": true, 119 "dynamodb_table": bucketName, 120 }).(*Backend) 121 122 b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ 123 "bucket": bucketName, 124 "key": keyName, 125 "encrypt": true, 126 "dynamodb_table": bucketName, 127 }).(*Backend) 128 129 createS3Bucket(t, b1.s3Client, bucketName) 130 defer deleteS3Bucket(t, b1.s3Client, bucketName) 131 createDynamoDBTable(t, b1.dynClient, bucketName) 132 defer deleteDynamoDBTable(t, b1.dynClient, bucketName) 133 134 backend.TestBackendStateLocks(t, b1, b2) 135 backend.TestBackendStateForceUnlock(t, b1, b2) 136 } 137 138 // add some extra junk in S3 to try and confuse the env listing. 139 func TestBackendExtraPaths(t *testing.T) { 140 testACC(t) 141 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 142 keyName := "test/state/tfstate" 143 144 b := backend.TestBackendConfig(t, New(), map[string]interface{}{ 145 "bucket": bucketName, 146 "key": keyName, 147 "encrypt": true, 148 }).(*Backend) 149 150 createS3Bucket(t, b.s3Client, bucketName) 151 defer deleteS3Bucket(t, b.s3Client, bucketName) 152 153 // put multiple states in old env paths. 154 s1 := terraform.NewState() 155 s2 := terraform.NewState() 156 157 // RemoteClient to Put things in various paths 158 client := &RemoteClient{ 159 s3Client: b.s3Client, 160 dynClient: b.dynClient, 161 bucketName: b.bucketName, 162 path: b.path("s1"), 163 serverSideEncryption: b.serverSideEncryption, 164 acl: b.acl, 165 kmsKeyID: b.kmsKeyID, 166 ddbTable: b.ddbTable, 167 } 168 169 stateMgr := &remote.State{Client: client} 170 stateMgr.WriteState(s1) 171 if err := stateMgr.PersistState(); err != nil { 172 t.Fatal(err) 173 } 174 175 client.path = b.path("s2") 176 stateMgr.WriteState(s2) 177 if err := stateMgr.PersistState(); err != nil { 178 t.Fatal(err) 179 } 180 181 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 182 t.Fatal(err) 183 } 184 185 // put a state in an env directory name 186 client.path = b.workspaceKeyPrefix + "/error" 187 stateMgr.WriteState(terraform.NewState()) 188 if err := stateMgr.PersistState(); err != nil { 189 t.Fatal(err) 190 } 191 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 192 t.Fatal(err) 193 } 194 195 // add state with the wrong key for an existing env 196 client.path = b.workspaceKeyPrefix + "/s2/notTestState" 197 stateMgr.WriteState(terraform.NewState()) 198 if err := stateMgr.PersistState(); err != nil { 199 t.Fatal(err) 200 } 201 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 202 t.Fatal(err) 203 } 204 205 // remove the state with extra subkey 206 if err := b.DeleteState("s2"); err != nil { 207 t.Fatal(err) 208 } 209 210 if err := checkStateList(b, []string{"default", "s1"}); err != nil { 211 t.Fatal(err) 212 } 213 214 // fetch that state again, which should produce a new lineage 215 s2Mgr, err := b.State("s2") 216 if err != nil { 217 t.Fatal(err) 218 } 219 if err := s2Mgr.RefreshState(); err != nil { 220 t.Fatal(err) 221 } 222 223 if s2Mgr.State().Lineage == s2.Lineage { 224 t.Fatal("state s2 was not deleted") 225 } 226 s2 = s2Mgr.State() 227 228 // add a state with a key that matches an existing environment dir name 229 client.path = b.workspaceKeyPrefix + "/s2/" 230 stateMgr.WriteState(terraform.NewState()) 231 if err := stateMgr.PersistState(); err != nil { 232 t.Fatal(err) 233 } 234 235 // make sure s2 is OK 236 s2Mgr, err = b.State("s2") 237 if err != nil { 238 t.Fatal(err) 239 } 240 if err := s2Mgr.RefreshState(); err != nil { 241 t.Fatal(err) 242 } 243 244 if s2Mgr.State().Lineage != s2.Lineage { 245 t.Fatal("we got the wrong state for s2") 246 } 247 248 if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { 249 t.Fatal(err) 250 } 251 } 252 253 // ensure we can separate the workspace prefix when it also matches the prefix 254 // of the workspace name itself. 255 func TestBackendPrefixInWorkspace(t *testing.T) { 256 testACC(t) 257 bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) 258 259 b := backend.TestBackendConfig(t, New(), map[string]interface{}{ 260 "bucket": bucketName, 261 "key": "test-env.tfstate", 262 "workspace_key_prefix": "env", 263 }).(*Backend) 264 265 createS3Bucket(t, b.s3Client, bucketName) 266 defer deleteS3Bucket(t, b.s3Client, bucketName) 267 268 // get a state that contains the prefix as a substring 269 sMgr, err := b.State("env-1") 270 if err != nil { 271 t.Fatal(err) 272 } 273 if err := sMgr.RefreshState(); err != nil { 274 t.Fatal(err) 275 } 276 277 if err := checkStateList(b, []string{"default", "env-1"}); err != nil { 278 t.Fatal(err) 279 } 280 } 281 282 func TestKeyEnv(t *testing.T) { 283 testACC(t) 284 keyName := "some/paths/tfstate" 285 286 bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix()) 287 b0 := backend.TestBackendConfig(t, New(), map[string]interface{}{ 288 "bucket": bucket0Name, 289 "key": keyName, 290 "encrypt": true, 291 "workspace_key_prefix": "", 292 }).(*Backend) 293 294 createS3Bucket(t, b0.s3Client, bucket0Name) 295 defer deleteS3Bucket(t, b0.s3Client, bucket0Name) 296 297 bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix()) 298 b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ 299 "bucket": bucket1Name, 300 "key": keyName, 301 "encrypt": true, 302 "workspace_key_prefix": "project/env:", 303 }).(*Backend) 304 305 createS3Bucket(t, b1.s3Client, bucket1Name) 306 defer deleteS3Bucket(t, b1.s3Client, bucket1Name) 307 308 bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix()) 309 b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ 310 "bucket": bucket2Name, 311 "key": keyName, 312 "encrypt": true, 313 }).(*Backend) 314 315 createS3Bucket(t, b2.s3Client, bucket2Name) 316 defer deleteS3Bucket(t, b2.s3Client, bucket2Name) 317 318 if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil { 319 t.Fatal(err) 320 } 321 322 if err := testGetWorkspaceForKey(b0, "ws1/some/paths/tfstate", "ws1"); err != nil { 323 t.Fatal(err) 324 } 325 326 if err := testGetWorkspaceForKey(b1, "project/env:/ws1/some/paths/tfstate", "ws1"); err != nil { 327 t.Fatal(err) 328 } 329 330 if err := testGetWorkspaceForKey(b1, "project/env:/ws2/some/paths/tfstate", "ws2"); err != nil { 331 t.Fatal(err) 332 } 333 334 if err := testGetWorkspaceForKey(b2, "env:/ws3/some/paths/tfstate", "ws3"); err != nil { 335 t.Fatal(err) 336 } 337 338 backend.TestBackendStates(t, b0) 339 backend.TestBackendStates(t, b1) 340 backend.TestBackendStates(t, b2) 341 } 342 343 func testGetWorkspaceForKey(b *Backend, key string, expected string) error { 344 if actual := b.keyEnv(key); actual != expected { 345 return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual) 346 } 347 return nil 348 } 349 350 func checkStateList(b backend.Backend, expected []string) error { 351 states, err := b.States() 352 if err != nil { 353 return err 354 } 355 356 if !reflect.DeepEqual(states, expected) { 357 return fmt.Errorf("incorrect states listed: %q", states) 358 } 359 return nil 360 } 361 362 func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { 363 createBucketReq := &s3.CreateBucketInput{ 364 Bucket: &bucketName, 365 } 366 367 // Be clear about what we're doing in case the user needs to clean 368 // this up later. 369 t.Logf("creating S3 bucket %s in %s", bucketName, *s3Client.Config.Region) 370 _, err := s3Client.CreateBucket(createBucketReq) 371 if err != nil { 372 t.Fatal("failed to create test S3 bucket:", err) 373 } 374 } 375 376 func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { 377 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)" 378 379 // first we have to get rid of the env objects, or we can't delete the bucket 380 resp, err := s3Client.ListObjects(&s3.ListObjectsInput{Bucket: &bucketName}) 381 if err != nil { 382 t.Logf(warning, err) 383 return 384 } 385 for _, obj := range resp.Contents { 386 if _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{Bucket: &bucketName, Key: obj.Key}); err != nil { 387 // this will need cleanup no matter what, so just warn and exit 388 t.Logf(warning, err) 389 return 390 } 391 } 392 393 if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil { 394 t.Logf(warning, err) 395 } 396 } 397 398 // create the dynamoDB table, and wait until we can query it. 399 func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) { 400 createInput := &dynamodb.CreateTableInput{ 401 AttributeDefinitions: []*dynamodb.AttributeDefinition{ 402 { 403 AttributeName: aws.String("LockID"), 404 AttributeType: aws.String("S"), 405 }, 406 }, 407 KeySchema: []*dynamodb.KeySchemaElement{ 408 { 409 AttributeName: aws.String("LockID"), 410 KeyType: aws.String("HASH"), 411 }, 412 }, 413 ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ 414 ReadCapacityUnits: aws.Int64(5), 415 WriteCapacityUnits: aws.Int64(5), 416 }, 417 TableName: aws.String(tableName), 418 } 419 420 _, err := dynClient.CreateTable(createInput) 421 if err != nil { 422 t.Fatal(err) 423 } 424 425 // now wait until it's ACTIVE 426 start := time.Now() 427 time.Sleep(time.Second) 428 429 describeInput := &dynamodb.DescribeTableInput{ 430 TableName: aws.String(tableName), 431 } 432 433 for { 434 resp, err := dynClient.DescribeTable(describeInput) 435 if err != nil { 436 t.Fatal(err) 437 } 438 439 if *resp.Table.TableStatus == "ACTIVE" { 440 return 441 } 442 443 if time.Since(start) > time.Minute { 444 t.Fatalf("timed out creating DynamoDB table %s", tableName) 445 } 446 447 time.Sleep(3 * time.Second) 448 } 449 450 } 451 452 func deleteDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) { 453 params := &dynamodb.DeleteTableInput{ 454 TableName: aws.String(tableName), 455 } 456 _, err := dynClient.DeleteTable(params) 457 if err != nil { 458 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) 459 } 460 }