Skip to content

Commit

Permalink
Merge pull request #15680 from brunomcustodio/etcdv3-backend
Browse files Browse the repository at this point in the history
[WIP] etcd v3 backend with lock support.
  • Loading branch information
jbardin authored Oct 3, 2017
2 parents 7d5c320 + 4d23195 commit 91442b7
Show file tree
Hide file tree
Showing 62 changed files with 31,974 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ go:

# add TF_CONSUL_TEST=1 to run consul tests
# they were causing timouts in travis
# add TF_ETCDV3_TEST=1 to run etcdv3 tests
# if added, TF_ETCDV3_ENDPOINTS must be set to a comma-separated list of (insecure) etcd endpoints against which to test
env:
- CONSUL_VERSION=0.7.5 GOMAXPROCS=4

Expand Down
2 changes: 2 additions & 0 deletions backend/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
backendlocal "github.com/hashicorp/terraform/backend/local"
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
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 @@ -45,6 +46,7 @@ func init() {
"azure": deprecateBackend(backendAzure.New(),
`Warning: "azure" name is deprecated, please use "azurerm"`),
"azurerm": func() backend.Backend { return backendAzure.New() },
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
}

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

import (
"context"

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

const (
endpointsKey = "endpoints"
usernameKey = "username"
usernameEnvVarName = "ETCDV3_USERNAME"
passwordKey = "password"
passwordEnvVarName = "ETCDV3_PASSWORD"
prefixKey = "prefix"
lockKey = "lock"
cacertPathKey = "cacert_path"
certPathKey = "cert_path"
keyPathKey = "key_path"
)

func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
endpointsKey: &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
MinItems: 1,
Required: true,
Description: "Endpoints for the etcd cluster.",
},

usernameKey: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Username used to connect to the etcd cluster.",
DefaultFunc: schema.EnvDefaultFunc(usernameEnvVarName, ""),
},

passwordKey: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Password used to connect to the etcd cluster.",
DefaultFunc: schema.EnvDefaultFunc(passwordEnvVarName, ""),
},

prefixKey: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "An optional prefix to be added to keys when to storing state in etcd.",
Default: "",
},

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

cacertPathKey: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The path to a PEM-encoded CA bundle with which to verify certificates of TLS-enabled etcd servers.",
Default: "",
},

certPathKey: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The path to a PEM-encoded certificate to provide to etcd for secure client identification.",
Default: "",
},

keyPathKey: &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The path to a PEM-encoded key to provide to etcd for secure client identification.",
Default: "",
},
},
}

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

type Backend struct {
*schema.Backend

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

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

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

if v, ok := b.data.GetOk(endpointsKey); ok {
config.Endpoints = retrieveEndpoints(v)
}
if v, ok := b.data.GetOk(usernameKey); ok && v.(string) != "" {
config.Username = v.(string)
}
if v, ok := b.data.GetOk(passwordKey); ok && v.(string) != "" {
config.Password = v.(string)
}
if v, ok := b.data.GetOk(cacertPathKey); ok && v.(string) != "" {
tlsInfo.TrustedCAFile = v.(string)
}
if v, ok := b.data.GetOk(certPathKey); ok && v.(string) != "" {
tlsInfo.CertFile = v.(string)
}
if v, ok := b.data.GetOk(keyPathKey); ok && v.(string) != "" {
tlsInfo.KeyFile = v.(string)
}

if tlsCfg, err := tlsInfo.ClientConfig(); err != nil {
return nil, err
} else if !tlsInfo.Empty() {
config.TLS = tlsCfg // Assign TLS configuration only if it valid and non-empty.
}

return etcdv3.New(config)
}

func retrieveEndpoints(v interface{}) []string {
var endpoints []string
list := v.([]interface{})
for _, ep := range list {
endpoints = append(endpoints, ep.(string))
}
return endpoints
}
103 changes: 103 additions & 0 deletions backend/remote-state/etcdv3/backend_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package etcd

import (
"context"
"fmt"
"sort"
"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"
)

func (b *Backend) States() ([]string, error) {
res, err := b.client.Get(context.TODO(), b.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), b.prefix))
}
sort.Strings(result[1:])

return result, nil
}

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

key := b.determineKey(name)

_, err := b.client.Delete(context.TODO(), key)
return err
}

func (b *Backend) State(name string) (state.State, error) {
var stateMgr state.State = &remote.State{
Client: &RemoteClient{
Client: b.client,
DoLock: b.lock,
Key: b.determineKey(name),
},
}

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 {
return b.prefix + name
}

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.
`
93 changes: 93 additions & 0 deletions backend/remote-state/etcdv3/backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package etcd

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

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

var (
etcdv3Endpoints = strings.Split(os.Getenv("TF_ETCDV3_ENDPOINTS"), ",")
)

const (
keyPrefix = "tf-unit"
)

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

func cleanupEtcdv3(t *testing.T) {
client, err := etcdv3.New(etcdv3.Config{
Endpoints: etcdv3Endpoints,
})
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 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()
}
cleanupEtcdv3(t)
}

func TestBackend(t *testing.T) {
prepareEtcdv3(t)
defer cleanupEtcdv3(t)

prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339))

// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
})

b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
})

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

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

prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339))

// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
"lock": false,
})

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

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

0 comments on commit 91442b7

Please sign in to comment.