Skip to content

Commit

Permalink
First steps towards an 'etcdv3' backend.
Browse files Browse the repository at this point in the history
  • Loading branch information
bmcustodio committed Aug 2, 2017
1 parent 6a869f8 commit cb3fbfd
Show file tree
Hide file tree
Showing 44 changed files with 29,994 additions and 0 deletions.
2 changes: 2 additions & 0 deletions backend/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
backendlegacy "github.com/hashicorp/terraform/backend/legacy"
backendlocal "github.com/hashicorp/terraform/backend/local"
backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul"
backendetcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
Expand Down Expand Up @@ -40,6 +41,7 @@ func init() {
"inmem": func() backend.Backend { return backendinmem.New() },
"swift": func() backend.Backend { return backendSwift.New() },
"s3": func() backend.Backend { return backendS3.New() },
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
}

// Add the legacy remote backends that haven't yet been convertd to
Expand Down
91 changes: 91 additions & 0 deletions backend/remote-state/etcdv3/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package etcd

import (
"context"

etcdv3 "github.com/coreos/etcd/clientv3"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
"strings"
)

func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"endpoints": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Comma-separated list of endpoints for the etcd cluster.",
},

"username": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Username used to connect to the etcd cluster.",
Default: "", // To prevent input.
},

"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Password used to connect to the etcd cluster.",
Default: "", // To prevent input.
},

"prefix": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The prefix to use when storing state in etcd.",
},

"lock": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Lock state access.",
Default: true,
},
},
}

result := &Backend{Backend: s}
result.Backend.ConfigureFunc = result.configure
return result
}

type Backend struct {
*schema.Backend

// The fields below are set from configure.
data *schema.ResourceData
lock bool
prefix string
}

func (b *Backend) configure(ctx context.Context) error {
// Grab the resource data.
b.data = schema.FromContextBackendConfig(ctx)
// Store the lock information.
b.lock = b.data.Get("lock").(bool)
// Store the prefix information.
b.prefix = b.data.Get("prefix").(string)
// Initialize a client to test config.
_, err := b.rawClient()
// Return err, if any.
return err
}

func (b *Backend) rawClient() (*etcdv3.Client, error) {
config := etcdv3.Config{}

if v, ok := b.data.GetOk("endpoints"); ok && v.(string) != "" {
config.Endpoints = strings.Split(v.(string), ",")
}
if v, ok := b.data.GetOk("username"); ok && v.(string) != "" {
config.Username = v.(string)
}
if v, ok := b.data.GetOk("password"); ok && v.(string) != "" {
config.Password = v.(string)
}

return etcdv3.New(config)
}
125 changes: 125 additions & 0 deletions backend/remote-state/etcdv3/backend_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package etcd

import (
"context"
"fmt"
"strings"

etcdv3 "github.com/coreos/etcd/clientv3"
"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) {
client, err := b.rawClient()
if err != nil {
return nil, err
}

prefix := b.determineKey("")
res, err := client.Get(context.TODO(), prefix, etcdv3.WithPrefix(), etcdv3.WithKeysOnly())
if err != nil {
return nil, err
}

result := make([]string, 1, len(res.Kvs)+1)
result[0] = backend.DefaultStateName
for _, kv := range res.Kvs {
result = append(result, strings.TrimPrefix(string(kv.Key), prefix))
}

return result, nil
}

func (b *Backend) DeleteState(name string) error {
if name == backend.DefaultStateName || name == "" {
return fmt.Errorf("Can't delete default state.")
}

client, err := b.rawClient()
if err != nil {
return err
}

path := b.determineKey(name)

_, err = client.Delete(context.TODO(), path)
return err
}

func (b *Backend) State(name string) (state.State, error) {
client, err := b.rawClient()
if err != nil {
return nil, err
}

var stateMgr state.State = &remote.State{
Client: &RemoteClient{
Client: client,
Key: b.determineKey(name),
lockState: b.lock,
},
}

if !b.lock {
stateMgr = &state.LockDisabled{Inner: stateMgr}
}

lockInfo := state.NewLockInfo()
lockInfo.Operation = "init"
lockId, err := stateMgr.Lock(lockInfo)
if err != nil {
return nil, fmt.Errorf("Failed to lock state in etcd: %s", err)
}

lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
}
return parent
}

if err := stateMgr.RefreshState(); err != nil {
err = lockUnlock(err)
return nil, err
}

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
}
}

if err := lockUnlock(nil); err != nil {
return nil, err
}

return stateMgr, nil
}

func (b *Backend) determineKey(name string) string {
prefix := b.prefix
if name != backend.DefaultStateName {
prefix += fmt.Sprintf("%s%s", keyEnvPrefix, name)
}
return prefix
}

const errStateUnlock = `
Error unlocking etcd state. Lock ID: %s
Error: %s
You may have to force-unlock this state in order to use it again.
`
65 changes: 65 additions & 0 deletions backend/remote-state/etcdv3/backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package etcd

import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"

etcdv3 "github.com/coreos/etcd/clientv3"
"github.com/hashicorp/terraform/backend"
)

const (
keyPrefix = "tf-unit"
)

func TestBackend_impl(t *testing.T) {
var _ backend.Backend = new(Backend)
}

func prepareEtcdv3(t *testing.T) {
skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_ETCDV3_TEST") == ""
if skip {
t.Log("etcd server tests require setting TF_ACC or TF_ETCDV3_TEST")
t.Skip()
}

client, err := etcdv3.New(etcdv3.Config{
Endpoints: strings.Split(os.Getenv("TF_ETCDV3_ENDPOINTS"), ","),
})
if err != nil {
t.Fatal(err)
}

res, err := client.KV.Delete(context.TODO(), keyPrefix, etcdv3.WithPrefix())
if err != nil {
t.Fatal(err)
}

t.Logf("Cleaned up %d keys.", res.Deleted)
}

func TestBackend_lockDisabled(t *testing.T) {
prepareEtcdv3(t)

key := fmt.Sprintf("%s/%s", keyPrefix, time.Now().String())

// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"endpoints": strings.Split(os.Getenv("TF_ETCDV3_ENDPOINTS"), ","),
"key": key,
"lock": false,
})

b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"endpoints": strings.Split(os.Getenv("TF_ETCDV3_ENDPOINTS"), ","),
"key": key + "/" + "different", // Diff so locking test would fail if it was locking
"lock": false,
})

// Test
backend.TestBackend(t, b1, b2)
}
Loading

0 comments on commit cb3fbfd

Please sign in to comment.