add named state support to the s3 backend

This adds named state (environment) support to the S3 backend.

A state NAME will prepend the configured s3 key with `env:/NAME/`.
The default state will remain rooted in the bucket for backwards
compatibility.

Locks in DynamoDB use the S3 key as the as the primary key value, so
locking will work as expected for multiple states.
This commit is contained in:
James Bardin 2017-03-22 16:33:41 -04:00
parent 4980fa20e7
commit fa4dc01cf4
2 changed files with 75 additions and 23 deletions

View File

@ -2,46 +2,81 @@ package s3
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/state/remote"
) )
const ( const (
// This will be used a directory name, the odd looking colon is to reduce // This will be used as directory name, the odd looking colon is simply to
// the chance of name conflicts with existing deployments. // reduce the chance of name conflicts with existing objects.
keyEnvPrefix = "env:" keyEnvPrefix = "env:"
) )
func (b *Backend) States() ([]string, error) { func (b *Backend) States() ([]string, error) {
return nil, backend.ErrNamedStatesNotSupported params := &s3.ListObjectsInput{
Bucket: &b.bucketName,
Prefix: aws.String(keyEnvPrefix + "/"),
}
resp, err := b.s3Client.ListObjects(params)
if err != nil {
return nil, err
}
var envs []string
for _, obj := range resp.Contents {
env := keyEnv(*obj.Key)
if env != "" {
envs = append(envs, env)
}
}
sort.Strings(envs)
envs = append([]string{backend.DefaultStateName}, envs...)
return envs, nil
}
// extract the env name from the S3 key
func keyEnv(key string) string {
parts := strings.Split(key, "/")
if len(parts) < 3 {
// no env here
return ""
}
if parts[0] != keyEnvPrefix {
// not our key, so ignore
return ""
}
return parts[1]
} }
func (b *Backend) DeleteState(name string) error { func (b *Backend) DeleteState(name string) error {
return backend.ErrNamedStatesNotSupported
if name == backend.DefaultStateName || name == "" { if name == backend.DefaultStateName || name == "" {
return fmt.Errorf("can't delete default state") return fmt.Errorf("can't delete default state")
} }
//params := &s3.ListObjectsInput{ params := &s3.DeleteObjectInput{
// Bucket: &b.client.bucketName, Bucket: &b.bucketName,
// Delimiter: aws.String("Delimiter"), Key: aws.String(b.path(name)),
// EncodingType: aws.String("EncodingType"), }
// Marker: aws.String("Marker"),
// MaxKeys: aws.Int64(1), _, err := b.s3Client.DeleteObject(params)
// Prefix: aws.String("env"), if err != nil {
// RequestPayer: aws.String("RequestPayer"), return err
//} }
return nil return nil
} }
func (b *Backend) State(name string) (state.State, error) { func (b *Backend) State(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
}
client := &RemoteClient{ client := &RemoteClient{
s3Client: b.s3Client, s3Client: b.s3Client,
dynClient: b.dynClient, dynClient: b.dynClient,
@ -53,7 +88,13 @@ func (b *Backend) State(name string) (state.State, error) {
lockTable: b.lockTable, lockTable: b.lockTable,
} }
// TODO: create new state if it doesn't exist // if this isn't the default state name, we need to create the object so
// it's listed by States.
if name != backend.DefaultStateName {
if err := client.Put([]byte{}); err != nil {
return nil, err
}
}
return &remote.State{Client: client}, nil return &remote.State{Client: client}, nil
} }

View File

@ -127,13 +127,24 @@ func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
} }
func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) {
deleteBucketReq := &s3.DeleteBucketInput{ 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)"
Bucket: &bucketName,
// 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
}
} }
_, err := s3Client.DeleteBucket(deleteBucketReq) if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil {
if err != nil { t.Logf(warning, err)
t.Logf("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)", err)
} }
} }