github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/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  func checkStateList(b backend.Backend, expected []string) error {
   253  	states, err := b.States()
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	if !reflect.DeepEqual(states, expected) {
   259  		return fmt.Errorf("incorrect states listed: %q", states)
   260  	}
   261  	return nil
   262  }
   263  
   264  func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
   265  	createBucketReq := &s3.CreateBucketInput{
   266  		Bucket: &bucketName,
   267  	}
   268  
   269  	// Be clear about what we're doing in case the user needs to clean
   270  	// this up later.
   271  	t.Logf("creating S3 bucket %s in %s", bucketName, *s3Client.Config.Region)
   272  	_, err := s3Client.CreateBucket(createBucketReq)
   273  	if err != nil {
   274  		t.Fatal("failed to create test S3 bucket:", err)
   275  	}
   276  }
   277  
   278  func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
   279  	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)"
   280  
   281  	// first we have to get rid of the env objects, or we can't delete the bucket
   282  	resp, err := s3Client.ListObjects(&s3.ListObjectsInput{Bucket: &bucketName})
   283  	if err != nil {
   284  		t.Logf(warning, err)
   285  		return
   286  	}
   287  	for _, obj := range resp.Contents {
   288  		if _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{Bucket: &bucketName, Key: obj.Key}); err != nil {
   289  			// this will need cleanup no matter what, so just warn and exit
   290  			t.Logf(warning, err)
   291  			return
   292  		}
   293  	}
   294  
   295  	if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil {
   296  		t.Logf(warning, err)
   297  	}
   298  }
   299  
   300  // create the dynamoDB table, and wait until we can query it.
   301  func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
   302  	createInput := &dynamodb.CreateTableInput{
   303  		AttributeDefinitions: []*dynamodb.AttributeDefinition{
   304  			{
   305  				AttributeName: aws.String("LockID"),
   306  				AttributeType: aws.String("S"),
   307  			},
   308  		},
   309  		KeySchema: []*dynamodb.KeySchemaElement{
   310  			{
   311  				AttributeName: aws.String("LockID"),
   312  				KeyType:       aws.String("HASH"),
   313  			},
   314  		},
   315  		ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
   316  			ReadCapacityUnits:  aws.Int64(5),
   317  			WriteCapacityUnits: aws.Int64(5),
   318  		},
   319  		TableName: aws.String(tableName),
   320  	}
   321  
   322  	_, err := dynClient.CreateTable(createInput)
   323  	if err != nil {
   324  		t.Fatal(err)
   325  	}
   326  
   327  	// now wait until it's ACTIVE
   328  	start := time.Now()
   329  	time.Sleep(time.Second)
   330  
   331  	describeInput := &dynamodb.DescribeTableInput{
   332  		TableName: aws.String(tableName),
   333  	}
   334  
   335  	for {
   336  		resp, err := dynClient.DescribeTable(describeInput)
   337  		if err != nil {
   338  			t.Fatal(err)
   339  		}
   340  
   341  		if *resp.Table.TableStatus == "ACTIVE" {
   342  			return
   343  		}
   344  
   345  		if time.Since(start) > time.Minute {
   346  			t.Fatalf("timed out creating DynamoDB table %s", tableName)
   347  		}
   348  
   349  		time.Sleep(3 * time.Second)
   350  	}
   351  
   352  }
   353  
   354  func deleteDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
   355  	params := &dynamodb.DeleteTableInput{
   356  		TableName: aws.String(tableName),
   357  	}
   358  	_, err := dynClient.DeleteTable(params)
   359  	if err != nil {
   360  		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)
   361  	}
   362  }