2017-07-19 11:07:24 +02:00
|
|
|
package gcloud
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2017-09-07 13:10:56 +02:00
|
|
|
"regexp"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"cloud.google.com/go/storage"
|
2017-07-19 11:07:24 +02:00
|
|
|
"github.com/hashicorp/terraform/backend"
|
|
|
|
"github.com/hashicorp/terraform/state"
|
|
|
|
"github.com/hashicorp/terraform/state/remote"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
"google.golang.org/api/iterator"
|
|
|
|
)
|
|
|
|
|
|
|
|
func (b *Backend) States() ([]string, error) {
|
|
|
|
workspaces := []string{backend.DefaultStateName}
|
|
|
|
var stateRegex *regexp.Regexp
|
|
|
|
var err error
|
|
|
|
if b.stateDir == "" {
|
|
|
|
stateRegex = regexp.MustCompile(`^(.+)\.tfstate$`)
|
|
|
|
} else {
|
|
|
|
stateRegex, err = regexp.Compile(fmt.Sprintf("^%v/(.+)\\.tfstate$", regexp.QuoteMeta(b.stateDir)))
|
|
|
|
if err != nil {
|
|
|
|
return []string{}, fmt.Errorf("Failed to compile regex for querying states: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket := b.storageClient.Bucket(b.bucketName)
|
|
|
|
query := &storage.Query{
|
|
|
|
Prefix: b.stateDir,
|
|
|
|
}
|
|
|
|
|
|
|
|
files := bucket.Objects(b.storageContext, query)
|
|
|
|
for {
|
|
|
|
attrs, err := files.Next()
|
|
|
|
if err == iterator.Done {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return []string{}, fmt.Errorf("Failed to query remote states: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
matches := stateRegex.FindStringSubmatch(attrs.Name)
|
|
|
|
if len(matches) == 2 && matches[1] != backend.DefaultStateName {
|
|
|
|
workspaces = append(workspaces, matches[1])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(workspaces[1:])
|
|
|
|
return workspaces, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Backend) DeleteState(name string) error {
|
|
|
|
if name == backend.DefaultStateName || name == "" {
|
|
|
|
return fmt.Errorf("Can't delete default state")
|
|
|
|
}
|
|
|
|
|
|
|
|
client, err := b.remoteClient(name)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to create Google Storage client: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = client.Delete()
|
|
|
|
if err != nil {
|
2017-09-07 12:56:17 +02:00
|
|
|
return fmt.Errorf("Failed to delete state file %v: %v", client.stateFileURL(), err)
|
2017-07-19 11:07:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// get a remote client configured for this state
|
|
|
|
func (b *Backend) remoteClient(name string) (*RemoteClient, error) {
|
|
|
|
if name == "" {
|
|
|
|
return nil, errors.New("Missing state name")
|
|
|
|
}
|
|
|
|
|
|
|
|
client := &RemoteClient{
|
|
|
|
storageContext: b.storageContext,
|
|
|
|
storageClient: b.storageClient,
|
|
|
|
bucketName: b.bucketName,
|
|
|
|
stateFilePath: b.stateFileName(name),
|
|
|
|
lockFilePath: b.lockFileName(name),
|
|
|
|
}
|
|
|
|
|
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Backend) State(name string) (state.State, error) {
|
|
|
|
client, err := b.remoteClient(name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to create Google Storage client: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
stateMgr := &remote.State{Client: client}
|
|
|
|
lockInfo := state.NewLockInfo()
|
|
|
|
lockInfo.Operation = "init"
|
|
|
|
lockId, err := stateMgr.Lock(lockInfo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to lock state in Google Cloud Storage: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Local helper function so we can call it multiple places
|
|
|
|
lockUnlock := func(parent error) error {
|
|
|
|
if err := stateMgr.Unlock(lockId); err != nil {
|
2017-09-07 12:56:17 +02:00
|
|
|
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, client.lockFileURL(), err)
|
2017-07-19 11:07:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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) stateFileName(stateName string) string {
|
|
|
|
if b.stateDir == "" {
|
|
|
|
return fmt.Sprintf("%v.tfstate", stateName)
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf("%v/%v.tfstate", b.stateDir, stateName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Backend) lockFileName(stateName string) string {
|
|
|
|
if b.stateDir == "" {
|
|
|
|
return fmt.Sprintf("%v.tflock", stateName)
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf("%v/%v.tflock", b.stateDir, stateName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const errStateUnlock = `
|
|
|
|
Error unlocking Google Cloud Storage state.
|
|
|
|
|
|
|
|
Lock ID: %v
|
|
|
|
Lock file URL: %v
|
|
|
|
Error: %v
|
|
|
|
|
|
|
|
You may have to force-unlock this state in order to use it again.
|
|
|
|
The GCloud backend acquires a lock during initialization to ensure
|
|
|
|
the initial state file is created.
|
|
|
|
`
|