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