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  }