mirror of https://github.com/hashicorp/terraform
Merge pull request #12367 from hashicorp/f-consul-state
backend/consul: support named statespull/12401/head
commit
45ca69e09f
@ -0,0 +1,158 @@
|
||||
package consul
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/state/remote"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
const (
|
||||
keyEnvPrefix = "-env:"
|
||||
)
|
||||
|
||||
func (b *Backend) States() ([]string, error) {
|
||||
// Get the Consul client
|
||||
client, err := b.clientRaw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// List our raw path
|
||||
prefix := b.configData.Get("path").(string) + keyEnvPrefix
|
||||
keys, _, err := client.KV().Keys(prefix, "/", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find the envs, we use a map since we can get duplicates with
|
||||
// path suffixes.
|
||||
envs := map[string]struct{}{}
|
||||
for _, key := range keys {
|
||||
// Consul should ensure this but it doesn't hurt to check again
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
key = strings.TrimPrefix(key, prefix)
|
||||
|
||||
// Ignore anything with a "/" in it since we store the state
|
||||
// directly in a key not a directory.
|
||||
if idx := strings.IndexRune(key, '/'); idx >= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
envs[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 1, len(envs)+1)
|
||||
result[0] = backend.DefaultStateName
|
||||
for k, _ := range envs {
|
||||
result = append(result, k)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *Backend) DeleteState(name string) error {
|
||||
if name == backend.DefaultStateName {
|
||||
return fmt.Errorf("can't delete default state")
|
||||
}
|
||||
|
||||
// Get the Consul API client
|
||||
client, err := b.clientRaw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine the path of the data
|
||||
path := b.path(name)
|
||||
|
||||
// Delete it. We just delete it without any locking since
|
||||
// the DeleteState API is documented as such.
|
||||
_, err = client.KV().Delete(path, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Backend) State(name string) (state.State, error) {
|
||||
// Get the Consul API client
|
||||
client, err := b.clientRaw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine the path of the data
|
||||
path := b.path(name)
|
||||
|
||||
// Build the state client
|
||||
stateMgr := &remote.State{
|
||||
Client: &RemoteClient{
|
||||
Client: client,
|
||||
Path: path,
|
||||
},
|
||||
}
|
||||
|
||||
// Grab a lock, we use this to write an empty state if one doesn't
|
||||
// exist already. We have to write an empty state as a sentinel value
|
||||
// so States() knows it exists.
|
||||
lockInfo := state.NewLockInfo()
|
||||
lockInfo.Operation = "init"
|
||||
lockId, err := stateMgr.Lock(lockInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to lock state in Consul: %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) path(name string) string {
|
||||
path := b.configData.Get("path").(string)
|
||||
if name != backend.DefaultStateName {
|
||||
path += fmt.Sprintf("%s%s", keyEnvPrefix, name)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
const errStateUnlock = `
|
||||
Error unlocking Consul state. Lock ID: %s
|
||||
|
||||
Error: %s
|
||||
|
||||
You may have to force-unlock this state in order to use it again.
|
||||
The Consul backend acquires a lock during initialization to ensure
|
||||
the minimum required key/values are prepared.
|
||||
`
|
||||
@ -0,0 +1,31 @@
|
||||
package consul
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
)
|
||||
|
||||
func TestBackend_impl(t *testing.T) {
|
||||
var _ backend.Backend = new(Backend)
|
||||
}
|
||||
|
||||
func TestBackend(t *testing.T) {
|
||||
addr := os.Getenv("CONSUL_HTTP_ADDR")
|
||||
if addr == "" {
|
||||
t.Log("consul tests require CONSUL_HTTP_ADDR")
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
// Get the backend
|
||||
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||
"address": addr,
|
||||
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
|
||||
})
|
||||
|
||||
// Test
|
||||
backend.TestBackend(t, b)
|
||||
}
|
||||
Loading…
Reference in new issue