mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-13 10:01:08 -04:00
Prevent extra keys in the s3 envPrefix path from showing up as listed environments. Better handle keys containing slashes Add tests for unexpected keys in s3.
340 lines
8.6 KiB
Go
340 lines
8.6 KiB
Go
package s3
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"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"
|
|
"github.com/hashicorp/terraform/state/remote"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
// 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) {
|
|
testACC(t)
|
|
config := map[string]interface{}{
|
|
"region": "us-west-1",
|
|
"bucket": "tf-test",
|
|
"key": "state",
|
|
"encrypt": true,
|
|
"lock_table": "dynamoTable",
|
|
}
|
|
|
|
b := backend.TestBackendConfig(t, New(), config).(*Backend)
|
|
|
|
if *b.s3Client.Config.Region != "us-west-1" {
|
|
t.Fatalf("Incorrect region was populated")
|
|
}
|
|
if b.bucketName != "tf-test" {
|
|
t.Fatalf("Incorrect bucketName was populated")
|
|
}
|
|
if b.keyName != "state" {
|
|
t.Fatalf("Incorrect keyName was populated")
|
|
}
|
|
|
|
credentials, err := b.s3Client.Config.Credentials.Get()
|
|
if err != nil {
|
|
t.Fatalf("Error when requesting credentials")
|
|
}
|
|
if credentials.AccessKeyID == "" {
|
|
t.Fatalf("No Access Key Id was populated")
|
|
}
|
|
if credentials.SecretAccessKey == "" {
|
|
t.Fatalf("No Secret Access Key was populated")
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
createS3Bucket(t, b.s3Client, bucketName)
|
|
defer deleteS3Bucket(t, b.s3Client, bucketName)
|
|
|
|
backend.TestBackend(t, b, nil)
|
|
}
|
|
|
|
func TestBackendLocked(t *testing.T) {
|
|
testACC(t)
|
|
|
|
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
|
|
keyName := "test/state"
|
|
|
|
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
|
"bucket": bucketName,
|
|
"key": keyName,
|
|
"encrypt": true,
|
|
"lock_table": bucketName,
|
|
}).(*Backend)
|
|
|
|
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
|
"bucket": bucketName,
|
|
"key": keyName,
|
|
"encrypt": true,
|
|
"lock_table": bucketName,
|
|
}).(*Backend)
|
|
|
|
createS3Bucket(t, b1.s3Client, bucketName)
|
|
defer deleteS3Bucket(t, b1.s3Client, bucketName)
|
|
createDynamoDBTable(t, b1.dynClient, bucketName)
|
|
defer deleteDynamoDBTable(t, b1.dynClient, bucketName)
|
|
|
|
backend.TestBackend(t, b1, b2)
|
|
}
|
|
|
|
// 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,
|
|
lockTable: b.lockTable,
|
|
}
|
|
|
|
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
|
|
client.path = keyEnvPrefix + "/error"
|
|
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
|
|
client.path = keyEnvPrefix + "/s2/notTestState"
|
|
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
|
|
client.path = keyEnvPrefix + "/s2/"
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
|
|
createBucketReq := &s3.CreateBucketInput{
|
|
Bucket: &bucketName,
|
|
}
|
|
|
|
// Be clear about what we're doing in case the user needs to clean
|
|
// this up later.
|
|
t.Logf("creating S3 bucket %s in %s", bucketName, *s3Client.Config.Region)
|
|
_, err := s3Client.CreateBucket(createBucketReq)
|
|
if err != nil {
|
|
t.Fatal("failed to create test S3 bucket:", err)
|
|
}
|
|
}
|
|
|
|
func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
|
|
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)"
|
|
|
|
// 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})
|
|
if err != nil {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// create the dynamoDB table, and wait until we can query it.
|
|
func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
|
|
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),
|
|
}
|
|
|
|
_, err := dynClient.CreateTable(createInput)
|
|
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 {
|
|
resp, err := dynClient.DescribeTable(describeInput)
|
|
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)
|
|
}
|
|
|
|
}
|
|
|
|
func deleteDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) {
|
|
params := &dynamodb.DeleteTableInput{
|
|
TableName: aws.String(tableName),
|
|
}
|
|
_, err := dynClient.DeleteTable(params)
|
|
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)
|
|
}
|
|
}
|