diff --git a/backend/remote-state/s3/backend_state.go b/backend/remote-state/s3/backend_state.go index 6e9252861..3166cbfb9 100644 --- a/backend/remote-state/s3/backend_state.go +++ b/backend/remote-state/s3/backend_state.go @@ -2,46 +2,81 @@ package s3 import ( "fmt" + "sort" "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/state" "github.com/hashicorp/terraform/state/remote" ) const ( - // This will be used a directory name, the odd looking colon is to reduce - // the chance of name conflicts with existing deployments. + // This will be used as directory name, the odd looking colon is simply to + // reduce the chance of name conflicts with existing objects. keyEnvPrefix = "env:" ) 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 { - return backend.ErrNamedStatesNotSupported if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } - //params := &s3.ListObjectsInput{ - // Bucket: &b.client.bucketName, - // Delimiter: aws.String("Delimiter"), - // EncodingType: aws.String("EncodingType"), - // Marker: aws.String("Marker"), - // MaxKeys: aws.Int64(1), - // Prefix: aws.String("env"), - // RequestPayer: aws.String("RequestPayer"), - //} + params := &s3.DeleteObjectInput{ + Bucket: &b.bucketName, + Key: aws.String(b.path(name)), + } + + _, err := b.s3Client.DeleteObject(params) + if err != nil { + return err + } + return nil } func (b *Backend) State(name string) (state.State, error) { - if name != backend.DefaultStateName { - return nil, backend.ErrNamedStatesNotSupported - } - client := &RemoteClient{ s3Client: b.s3Client, dynClient: b.dynClient, @@ -53,7 +88,13 @@ func (b *Backend) State(name string) (state.State, error) { 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 } diff --git a/backend/remote-state/s3/backend_test.go b/backend/remote-state/s3/backend_test.go index 0838fa159..f8b664b80 100644 --- a/backend/remote-state/s3/backend_test.go +++ b/backend/remote-state/s3/backend_test.go @@ -127,13 +127,24 @@ func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { } func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { - deleteBucketReq := &s3.DeleteBucketInput{ - Bucket: &bucketName, + 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 + } } - _, err := s3Client.DeleteBucket(deleteBucketReq) - if err != nil { - 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) + if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil { + t.Logf(warning, err) } }