170 lines
3.7 KiB
Go
170 lines
3.7 KiB
Go
package s3
|
|
|
|
import (
|
|
"errors"
|
|
"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"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
const (
|
|
// 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) {
|
|
params := &s3.ListObjectsInput{
|
|
Bucket: &b.bucketName,
|
|
Prefix: aws.String(keyEnvPrefix + "/"),
|
|
}
|
|
|
|
resp, err := b.s3Client.ListObjects(params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
envs := []string{backend.DefaultStateName}
|
|
for _, obj := range resp.Contents {
|
|
env := b.keyEnv(*obj.Key)
|
|
if env != "" {
|
|
envs = append(envs, env)
|
|
}
|
|
}
|
|
|
|
sort.Strings(envs[1:])
|
|
return envs, nil
|
|
}
|
|
|
|
// extract the env name from the S3 key
|
|
func (b *Backend) keyEnv(key string) string {
|
|
// we have 3 parts, the prefix, the env name, and the key name
|
|
parts := strings.SplitN(key, "/", 3)
|
|
if len(parts) < 3 {
|
|
// no env here
|
|
return ""
|
|
}
|
|
|
|
// shouldn't happen since we listed by prefix
|
|
if parts[0] != keyEnvPrefix {
|
|
return ""
|
|
}
|
|
|
|
// not our key, so don't include it in our listing
|
|
if parts[2] != b.keyName {
|
|
return ""
|
|
}
|
|
|
|
return parts[1]
|
|
}
|
|
|
|
func (b *Backend) DeleteState(name string) error {
|
|
if name == backend.DefaultStateName || name == "" {
|
|
return fmt.Errorf("can't delete default state")
|
|
}
|
|
|
|
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 == "" {
|
|
return nil, errors.New("missing state name")
|
|
}
|
|
|
|
client := &RemoteClient{
|
|
s3Client: b.s3Client,
|
|
dynClient: b.dynClient,
|
|
bucketName: b.bucketName,
|
|
path: b.path(name),
|
|
serverSideEncryption: b.serverSideEncryption,
|
|
acl: b.acl,
|
|
kmsKeyID: b.kmsKeyID,
|
|
lockTable: b.lockTable,
|
|
}
|
|
|
|
stateMgr := &remote.State{Client: client}
|
|
|
|
//if this isn't the default state name, we need to create the object so
|
|
//it's listed by States.
|
|
if name != backend.DefaultStateName {
|
|
// take a lock on this state while we write it
|
|
lockInfo := state.NewLockInfo()
|
|
lockInfo.Operation = "init"
|
|
lockId, err := client.Lock(lockInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to lock s3 state: %s", err)
|
|
}
|
|
|
|
// Local helper function so we can call it multiple places
|
|
lockUnlock := func(parent error) error {
|
|
if err := stateMgr.Unlock(lockId); err != nil {
|
|
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
|
|
}
|
|
return parent
|
|
}
|
|
|
|
// Grab the value
|
|
if err := stateMgr.RefreshState(); err != nil {
|
|
err = lockUnlock(err)
|
|
return nil, err
|
|
}
|
|
|
|
// If we have no state, we have to create an empty state
|
|
if v := stateMgr.State(); v == nil {
|
|
if err := stateMgr.WriteState(terraform.NewState()); err != nil {
|
|
err = lockUnlock(err)
|
|
return nil, err
|
|
}
|
|
if err := stateMgr.PersistState(); err != nil {
|
|
err = lockUnlock(err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Unlock, the state should now be initialized
|
|
if err := lockUnlock(nil); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
}
|
|
|
|
return stateMgr, nil
|
|
}
|
|
|
|
func (b *Backend) client() *RemoteClient {
|
|
return &RemoteClient{}
|
|
}
|
|
|
|
func (b *Backend) path(name string) string {
|
|
if name == backend.DefaultStateName {
|
|
return b.keyName
|
|
}
|
|
|
|
return strings.Join([]string{keyEnvPrefix, name, b.keyName}, "/")
|
|
}
|
|
|
|
const errStateUnlock = `
|
|
Error unlocking S3 state. Lock ID: %s
|
|
|
|
Error: %s
|
|
|
|
You may have to force-unlock this state in order to use it again.
|
|
`
|