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"
2017-08-05 00:26:19 +02:00
"github.com/hashicorp/terraform/config"
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
}
b := backend . TestBackendConfig ( t , New ( ) , config ) . ( * Backend )
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 )
cfg := map [ string ] interface { } {
"region" : "us-west-1" ,
"bucket" : "tf-test" ,
"key" : "/leading-slash" ,
"encrypt" : true ,
"dynamodb_table" : "dynamoTable" ,
}
rawCfg , err := config . NewRawConfig ( cfg )
if err != nil {
t . Fatal ( err )
}
resCfg := terraform . NewResourceConfig ( rawCfg )
_ , errs := New ( ) . Validate ( resCfg )
if len ( errs ) != 1 {
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"
b := backend . TestBackendConfig ( t , New ( ) , map [ string ] interface { } {
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
} ) . ( * Backend )
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
backend . TestBackend ( t , b , nil )
}
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
b1 := backend . TestBackendConfig ( t , New ( ) , map [ string ] interface { } {
2017-05-26 01:12:20 +02:00
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
"dynamodb_table" : bucketName ,
2017-03-21 18:43:31 +01:00
} ) . ( * Backend )
b2 := backend . TestBackendConfig ( t , New ( ) , map [ string ] interface { } {
2017-05-26 01:12:20 +02:00
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
"dynamodb_table" : bucketName ,
2017-03-21 18:43:31 +01:00
} ) . ( * Backend )
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
backend . TestBackend ( t , b1 , b2 )
}
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"
b := backend . TestBackendConfig ( t , New ( ) , map [ string ] interface { } {
"bucket" : bucketName ,
"key" : keyName ,
"encrypt" : true ,
} ) . ( * Backend )
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 )
}
}
2017-12-16 03:04:15 +01:00
func TestKeyEnv ( t * testing . T ) {
testACC ( t )
bucketName := fmt . Sprintf ( "terraform-remote-s3-test-%x" , time . Now ( ) . Unix ( ) )
keyName0 := "tfstate"
keyName1 := "ws1/tfstate"
keyName2 := "ws1/env1/tfstate"
b0 := backend . TestBackendConfig ( t , New ( ) , map [ string ] interface { } {
"bucket" : bucketName ,
"key" : keyName0 ,
"encrypt" : true ,
"workspace_key_prefix" : "" ,
} ) . ( * Backend )
b1 := backend . TestBackendConfig ( t , New ( ) , map [ string ] interface { } {
"bucket" : bucketName ,
"key" : keyName1 ,
"encrypt" : true ,
"workspace_key_prefix" : "root/userA" ,
} ) . ( * Backend )
b2 := backend . TestBackendConfig ( t , New ( ) , map [ string ] interface { } {
"bucket" : bucketName ,
"key" : keyName2 ,
"encrypt" : true ,
"workspace_key_prefix" : "root/userA" ,
} ) . ( * Backend )
if err := testGetWorkspaceForKey ( b0 , "tfstate" , "" ) ; err != nil {
t . Fatal ( err )
}
if err := testGetWorkspaceForKey ( b0 , "ws1/tfstate" , "ws1" ) ; err != nil {
t . Fatal ( err )
}
if err := testGetWorkspaceForKey ( b1 , "root/userA/ws1/tfstate" , "ws1" ) ; err != nil {
t . Fatal ( err )
}
if err := testGetWorkspaceForKey ( b1 , "root/userA/ws2/tfstate" , "ws2" ) ; err != nil {
t . Fatal ( err )
}
if err := testGetWorkspaceForKey ( b2 , "root/userA/ws2/env1/tfstate" , "ws2" ) ; err != nil {
t . Fatal ( err )
}
}
func testGetWorkspaceForKey ( b * Backend , key string , expected string ) error {
if getWorkspaceForKey ( key , b ) != expected {
return fmt . Errorf ( "incorrect workspace for key[%q]: %q" , expected , key )
}
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 )
}
}