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  }