2017-03-21 18:43:31 +01:00
package s3
import (
"fmt"
"os"
2017-04-12 19:30:49 +02:00
"reflect"
2017-03-21 18:43:31 +01:00
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/terraform/backend"
2018-03-21 02:43:02 +01:00
"github.com/hashicorp/terraform/config/hcl2shim"
2017-04-12 19:30:49 +02:00
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
2017-03-21 18:43:31 +01:00
)
// verify that we are doing ACC tests or the S3 tests specifically
func testACC ( t * testing . T ) {
skip := os . Getenv ( "TF_ACC" ) == "" && os . Getenv ( "TF_S3_TEST" ) == ""
if skip {
t . Log ( "s3 backend tests require setting TF_ACC or TF_S3_TEST" )
t . Skip ( )
}
if os . Getenv ( "AWS_DEFAULT_REGION" ) == "" {
os . Setenv ( "AWS_DEFAULT_REGION" , "us-west-2" )
}
}
func TestBackend_impl ( t * testing . T ) {
var _ backend . Backend = new ( Backend )
}
func TestBackendConfig ( t * testing . T ) {
2017-04-05 18:37:42 +02:00
testACC ( t )
2017-03-21 18:43:31 +01:00
config := map [ string ] interface { } {
2017-05-26 01:12:20 +02:00
"region" : "us-west-1" ,
"bucket" : "tf-test" ,
"key" : "state" ,
"encrypt" : true ,
"dynamodb_table" : "dynamoTable" ,
2017-03-21 18:43:31 +01:00
}
2018-03-21 02:43:02 +01:00
b := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( config ) ) . ( * Backend )
2017-03-21 18:43:31 +01:00
2017-03-22 20:52:55 +01:00
if * b . s3Client . Config . Region != "us-west-1" {
2017-03-21 18:43:31 +01:00
t . Fatalf ( "Incorrect region was populated" )
}
2017-03-22 20:52:55 +01:00
if b . bucketName != "tf-test" {
2017-03-21 18:43:31 +01:00
t . Fatalf ( "Incorrect bucketName was populated" )
}
2017-03-22 20:52:55 +01:00
if b . keyName != "state" {
2017-03-21 18:43:31 +01:00
t . Fatalf ( "Incorrect keyName was populated" )
}
2017-03-22 20:52:55 +01:00
credentials , err := b . s3Client . Config . Credentials . Get ( )
2017-03-21 18:43:31 +01:00
if err != nil {
t . Fatalf ( "Error when requesting credentials" )
}
2017-04-05 18:37:42 +02:00
if credentials . AccessKeyID == "" {
t . Fatalf ( "No Access Key Id was populated" )
2017-03-21 18:43:31 +01:00
}
2017-04-05 18:37:42 +02:00
if credentials . SecretAccessKey == "" {
t . Fatalf ( "No Secret Access Key was populated" )
2017-03-21 18:43:31 +01:00
}
}
2017-08-05 00:26:19 +02:00
func TestBackendConfig_invalidKey ( t * testing . T ) {
testACC ( t )
2018-03-21 02:43:02 +01:00
cfg := hcl2shim . HCL2ValueFromConfigValue ( map [ string ] interface { } {
2017-08-05 00:26:19 +02:00
"region" : "us-west-1" ,
"bucket" : "tf-test" ,
"key" : "/leading-slash" ,
"encrypt" : true ,
"dynamodb_table" : "dynamoTable" ,
2018-03-21 02:43:02 +01:00
} )
2017-08-05 00:26:19 +02:00
2018-03-21 02:43:02 +01:00
diags := New ( ) . ValidateConfig ( cfg )
if ! diags . HasErrors ( ) {
2017-08-05 00:26:19 +02:00
t . Fatal ( "expected config validation error" )
}
}
2017-03-21 18:43:31 +01:00
func TestBackend ( t * testing . T ) {
testACC ( t )
bucketName := fmt . Sprintf ( "terraform-remote-s3-test-%x" , time . Now ( ) . Unix ( ) )
keyName := "testState"
2018-03-21 02:43:02 +01:00
b := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
2017-03-21 18:43:31 +01:00
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2017-03-21 18:43:31 +01:00
2017-03-22 20:52:55 +01:00
createS3Bucket ( t , b . s3Client , bucketName )
defer deleteS3Bucket ( t , b . s3Client , bucketName )
2017-03-21 18:43:31 +01:00
2018-02-21 03:05:58 +01:00
backend . TestBackendStates ( t , b )
2017-03-21 18:43:31 +01:00
}
func TestBackendLocked ( t * testing . T ) {
testACC ( t )
bucketName := fmt . Sprintf ( "terraform-remote-s3-test-%x" , time . Now ( ) . Unix ( ) )
2017-04-12 19:30:49 +02:00
keyName := "test/state"
2017-03-21 18:43:31 +01:00
2018-03-21 02:43:02 +01:00
b1 := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
2017-05-26 01:12:20 +02:00
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
"dynamodb_table" : bucketName ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2017-03-21 18:43:31 +01:00
2018-03-21 02:43:02 +01:00
b2 := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
2017-05-26 01:12:20 +02:00
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
"dynamodb_table" : bucketName ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2017-03-21 18:43:31 +01:00
2017-03-22 20:52:55 +01:00
createS3Bucket ( t , b1 . s3Client , bucketName )
defer deleteS3Bucket ( t , b1 . s3Client , bucketName )
createDynamoDBTable ( t , b1 . dynClient , bucketName )
defer deleteDynamoDBTable ( t , b1 . dynClient , bucketName )
2017-03-21 18:43:31 +01:00
2018-02-21 03:05:58 +01:00
backend . TestBackendStateLocks ( t , b1 , b2 )
backend . TestBackendStateForceUnlock ( t , b1 , b2 )
2017-03-21 18:43:31 +01:00
}
2017-04-12 19:30:49 +02:00
// add some extra junk in S3 to try and confuse the env listing.
func TestBackendExtraPaths ( t * testing . T ) {
testACC ( t )
bucketName := fmt . Sprintf ( "terraform-remote-s3-test-%x" , time . Now ( ) . Unix ( ) )
keyName := "test/state/tfstate"
2018-03-21 02:43:02 +01:00
b := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
2017-04-12 19:30:49 +02:00
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2017-04-12 19:30:49 +02:00
createS3Bucket ( t , b . s3Client , bucketName )
defer deleteS3Bucket ( t , b . s3Client , bucketName )
// put multiple states in old env paths.
s1 := terraform . NewState ( )
s2 := terraform . NewState ( )
// RemoteClient to Put things in various paths
client := & RemoteClient {
s3Client : b . s3Client ,
dynClient : b . dynClient ,
bucketName : b . bucketName ,
path : b . path ( "s1" ) ,
serverSideEncryption : b . serverSideEncryption ,
acl : b . acl ,
kmsKeyID : b . kmsKeyID ,
2017-05-26 01:12:20 +02:00
ddbTable : b . ddbTable ,
2017-04-12 19:30:49 +02:00
}
stateMgr := & remote . State { Client : client }
stateMgr . WriteState ( s1 )
if err := stateMgr . PersistState ( ) ; err != nil {
t . Fatal ( err )
}
client . path = b . path ( "s2" )
stateMgr . WriteState ( s2 )
if err := stateMgr . PersistState ( ) ; err != nil {
t . Fatal ( err )
}
if err := checkStateList ( b , [ ] string { "default" , "s1" , "s2" } ) ; err != nil {
t . Fatal ( err )
}
// put a state in an env directory name
2017-06-22 19:17:37 +02:00
client . path = b . workspaceKeyPrefix + "/error"
2017-04-12 19:30:49 +02:00
stateMgr . WriteState ( terraform . NewState ( ) )
if err := stateMgr . PersistState ( ) ; err != nil {
t . Fatal ( err )
}
if err := checkStateList ( b , [ ] string { "default" , "s1" , "s2" } ) ; err != nil {
t . Fatal ( err )
}
// add state with the wrong key for an existing env
2017-06-22 19:17:37 +02:00
client . path = b . workspaceKeyPrefix + "/s2/notTestState"
2017-04-12 19:30:49 +02:00
stateMgr . WriteState ( terraform . NewState ( ) )
if err := stateMgr . PersistState ( ) ; err != nil {
t . Fatal ( err )
}
if err := checkStateList ( b , [ ] string { "default" , "s1" , "s2" } ) ; err != nil {
t . Fatal ( err )
}
// remove the state with extra subkey
if err := b . DeleteState ( "s2" ) ; err != nil {
t . Fatal ( err )
}
if err := checkStateList ( b , [ ] string { "default" , "s1" } ) ; err != nil {
t . Fatal ( err )
}
// fetch that state again, which should produce a new lineage
s2Mgr , err := b . State ( "s2" )
if err != nil {
t . Fatal ( err )
}
if err := s2Mgr . RefreshState ( ) ; err != nil {
t . Fatal ( err )
}
if s2Mgr . State ( ) . Lineage == s2 . Lineage {
t . Fatal ( "state s2 was not deleted" )
}
s2 = s2Mgr . State ( )
// add a state with a key that matches an existing environment dir name
2017-06-22 19:17:37 +02:00
client . path = b . workspaceKeyPrefix + "/s2/"
2017-04-12 19:30:49 +02:00
stateMgr . WriteState ( terraform . NewState ( ) )
if err := stateMgr . PersistState ( ) ; err != nil {
t . Fatal ( err )
}
// make sure s2 is OK
s2Mgr , err = b . State ( "s2" )
if err != nil {
t . Fatal ( err )
}
if err := s2Mgr . RefreshState ( ) ; err != nil {
t . Fatal ( err )
}
if s2Mgr . State ( ) . Lineage != s2 . Lineage {
t . Fatal ( "we got the wrong state for s2" )
}
if err := checkStateList ( b , [ ] string { "default" , "s1" , "s2" } ) ; err != nil {
t . Fatal ( err )
}
}
2018-01-11 15:33:20 +01:00
// ensure we can separate the workspace prefix when it also matches the prefix
// of the workspace name itself.
func TestBackendPrefixInWorkspace ( t * testing . T ) {
testACC ( t )
bucketName := fmt . Sprintf ( "terraform-remote-s3-test-%x" , time . Now ( ) . Unix ( ) )
2018-03-21 02:43:02 +01:00
b := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
"bucket" : bucketName ,
"key" : "test-env.tfstate" ,
2018-01-11 15:33:20 +01:00
"workspace_key_prefix" : "env" ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2018-01-11 15:33:20 +01:00
createS3Bucket ( t , b . s3Client , bucketName )
defer deleteS3Bucket ( t , b . s3Client , bucketName )
// get a state that contains the prefix as a substring
sMgr , err := b . State ( "env-1" )
if err != nil {
t . Fatal ( err )
}
if err := sMgr . RefreshState ( ) ; err != nil {
t . Fatal ( err )
}
if err := checkStateList ( b , [ ] string { "default" , "env-1" } ) ; err != nil {
t . Fatal ( err )
}
}
2017-12-16 03:04:15 +01:00
func TestKeyEnv ( t * testing . T ) {
testACC ( t )
2017-12-19 15:31:53 +01:00
keyName := "some/paths/tfstate"
2017-12-16 03:04:15 +01:00
2017-12-19 15:31:53 +01:00
bucket0Name := fmt . Sprintf ( "terraform-remote-s3-test-%x-0" , time . Now ( ) . Unix ( ) )
2018-03-21 02:43:02 +01:00
b0 := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
2017-12-19 15:31:53 +01:00
"bucket" : bucket0Name ,
2017-12-18 22:24:34 +01:00
"key" : keyName ,
2017-12-16 03:04:15 +01:00
"encrypt" : true ,
"workspace_key_prefix" : "" ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2017-12-16 03:04:15 +01:00
2017-12-19 15:31:53 +01:00
createS3Bucket ( t , b0 . s3Client , bucket0Name )
defer deleteS3Bucket ( t , b0 . s3Client , bucket0Name )
bucket1Name := fmt . Sprintf ( "terraform-remote-s3-test-%x-1" , time . Now ( ) . Unix ( ) )
2018-03-21 02:43:02 +01:00
b1 := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
2017-12-19 15:31:53 +01:00
"bucket" : bucket1Name ,
2017-12-18 22:24:34 +01:00
"key" : keyName ,
2017-12-16 03:04:15 +01:00
"encrypt" : true ,
2017-12-18 22:24:34 +01:00
"workspace_key_prefix" : "project/env:" ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2017-12-16 03:04:15 +01:00
2017-12-19 15:31:53 +01:00
createS3Bucket ( t , b1 . s3Client , bucket1Name )
defer deleteS3Bucket ( t , b1 . s3Client , bucket1Name )
bucket2Name := fmt . Sprintf ( "terraform-remote-s3-test-%x-2" , time . Now ( ) . Unix ( ) )
2018-03-21 02:43:02 +01:00
b2 := backend . TestBackendConfig ( t , New ( ) , backend . TestWrapConfig ( map [ string ] interface { } {
2017-12-19 15:31:53 +01:00
"bucket" : bucket2Name ,
2017-12-18 22:24:34 +01:00
"key" : keyName ,
"encrypt" : true ,
2018-03-21 02:43:02 +01:00
} ) ) . ( * Backend )
2017-12-16 03:04:15 +01:00
2017-12-19 15:31:53 +01:00
createS3Bucket ( t , b2 . s3Client , bucket2Name )
defer deleteS3Bucket ( t , b2 . s3Client , bucket2Name )
if err := testGetWorkspaceForKey ( b0 , "some/paths/tfstate" , "" ) ; err != nil {
2017-12-16 03:04:15 +01:00
t . Fatal ( err )
}
2017-12-19 15:31:53 +01:00
if err := testGetWorkspaceForKey ( b0 , "ws1/some/paths/tfstate" , "ws1" ) ; err != nil {
2017-12-16 03:04:15 +01:00
t . Fatal ( err )
}
2017-12-19 15:31:53 +01:00
if err := testGetWorkspaceForKey ( b1 , "project/env:/ws1/some/paths/tfstate" , "ws1" ) ; err != nil {
2017-12-16 03:04:15 +01:00
t . Fatal ( err )
}
2017-12-19 15:31:53 +01:00
if err := testGetWorkspaceForKey ( b1 , "project/env:/ws2/some/paths/tfstate" , "ws2" ) ; err != nil {
2017-12-16 03:04:15 +01:00
t . Fatal ( err )
}
2017-12-19 15:31:53 +01:00
if err := testGetWorkspaceForKey ( b2 , "env:/ws3/some/paths/tfstate" , "ws3" ) ; err != nil {
2017-12-16 03:04:15 +01:00
t . Fatal ( err )
}
2017-12-18 22:24:34 +01:00
2018-02-21 03:05:58 +01:00
backend . TestBackendStates ( t , b0 )
backend . TestBackendStates ( t , b1 )
backend . TestBackendStates ( t , b2 )
2017-12-16 03:04:15 +01:00
}
func testGetWorkspaceForKey ( b * Backend , key string , expected string ) error {
2017-12-19 19:14:31 +01:00
if actual := b . keyEnv ( key ) ; actual != expected {
2017-12-19 15:31:53 +01:00
return fmt . Errorf ( "incorrect workspace for key[%q]. Expected[%q]: Actual[%q]" , key , expected , actual )
2017-12-16 03:04:15 +01:00
}
return nil
}
2017-04-12 19:30:49 +02:00
func checkStateList ( b backend . Backend , expected [ ] string ) error {
states , err := b . States ( )
if err != nil {
return err
}
if ! reflect . DeepEqual ( states , expected ) {
return fmt . Errorf ( "incorrect states listed: %q" , states )
}
return nil
}
2017-03-22 20:52:55 +01:00
func createS3Bucket ( t * testing . T , s3Client * s3 . S3 , bucketName string ) {
2017-03-21 18:43:31 +01:00
createBucketReq := & s3 . CreateBucketInput {
Bucket : & bucketName ,
}
// Be clear about what we're doing in case the user needs to clean
// this up later.
2017-03-22 20:52:55 +01:00
t . Logf ( "creating S3 bucket %s in %s" , bucketName , * s3Client . Config . Region )
_ , err := s3Client . CreateBucket ( createBucketReq )
2017-03-21 18:43:31 +01:00
if err != nil {
t . Fatal ( "failed to create test S3 bucket:" , err )
}
}
2017-03-22 20:52:55 +01:00
func deleteS3Bucket ( t * testing . T , s3Client * s3 . S3 , bucketName string ) {
2017-03-22 21:33:41 +01:00
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)"
2017-03-21 18:43:31 +01:00
2017-03-22 21:33:41 +01:00
// first we have to get rid of the env objects, or we can't delete the bucket
resp , err := s3Client . ListObjects ( & s3 . ListObjectsInput { Bucket : & bucketName } )
2017-03-21 18:43:31 +01:00
if err != nil {
2017-03-22 21:33:41 +01:00
t . Logf ( warning , err )
return
}
for _ , obj := range resp . Contents {
if _ , err := s3Client . DeleteObject ( & s3 . DeleteObjectInput { Bucket : & bucketName , Key : obj . Key } ) ; err != nil {
// this will need cleanup no matter what, so just warn and exit
t . Logf ( warning , err )
return
}
}
if _ , err := s3Client . DeleteBucket ( & s3 . DeleteBucketInput { Bucket : & bucketName } ) ; err != nil {
t . Logf ( warning , err )
2017-03-21 18:43:31 +01:00
}
}
// create the dynamoDB table, and wait until we can query it.
2017-03-22 20:52:55 +01:00
func createDynamoDBTable ( t * testing . T , dynClient * dynamodb . DynamoDB , tableName string ) {
2017-03-21 18:43:31 +01:00
createInput := & dynamodb . CreateTableInput {
AttributeDefinitions : [ ] * dynamodb . AttributeDefinition {
{
AttributeName : aws . String ( "LockID" ) ,
AttributeType : aws . String ( "S" ) ,
} ,
} ,
KeySchema : [ ] * dynamodb . KeySchemaElement {
{
AttributeName : aws . String ( "LockID" ) ,
KeyType : aws . String ( "HASH" ) ,
} ,
} ,
ProvisionedThroughput : & dynamodb . ProvisionedThroughput {
ReadCapacityUnits : aws . Int64 ( 5 ) ,
WriteCapacityUnits : aws . Int64 ( 5 ) ,
} ,
TableName : aws . String ( tableName ) ,
}
2017-03-22 20:52:55 +01:00
_ , err := dynClient . CreateTable ( createInput )
2017-03-21 18:43:31 +01:00
if err != nil {
t . Fatal ( err )
}
// now wait until it's ACTIVE
start := time . Now ( )
time . Sleep ( time . Second )
describeInput := & dynamodb . DescribeTableInput {
TableName : aws . String ( tableName ) ,
}
for {
2017-03-22 20:52:55 +01:00
resp , err := dynClient . DescribeTable ( describeInput )
2017-03-21 18:43:31 +01:00
if err != nil {
t . Fatal ( err )
}
if * resp . Table . TableStatus == "ACTIVE" {
return
}
if time . Since ( start ) > time . Minute {
t . Fatalf ( "timed out creating DynamoDB table %s" , tableName )
}
time . Sleep ( 3 * time . Second )
}
}
2017-03-22 20:52:55 +01:00
func deleteDynamoDBTable ( t * testing . T , dynClient * dynamodb . DynamoDB , tableName string ) {
2017-03-21 18:43:31 +01:00
params := & dynamodb . DeleteTableInput {
TableName : aws . String ( tableName ) ,
}
2017-03-22 20:52:55 +01:00
_ , err := dynClient . DeleteTable ( params )
2017-03-21 18:43:31 +01:00
if err != nil {
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 )
}
}