Merge pull request #3487 from nathan7/etcd

Etcd remote state backend
This commit is contained in:
Martin Atkins 2015-10-19 18:42:29 -07:00
commit 484d6f8765
4 changed files with 122 additions and 0 deletions

78
state/remote/etcd.go Normal file
View File

@ -0,0 +1,78 @@
package remote
import (
"crypto/md5"
"fmt"
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
etcdapi "github.com/coreos/etcd/client"
)
func etcdFactory(conf map[string]string) (Client, error) {
path, ok := conf["path"]
if !ok {
return nil, fmt.Errorf("missing 'path' configuration")
}
endpoints, ok := conf["endpoints"]
if !ok || endpoints == "" {
return nil, fmt.Errorf("missing 'endpoints' configuration")
}
config := etcdapi.Config{
Endpoints: strings.Split(endpoints, " "),
}
if username, ok := conf["username"]; ok && username != "" {
config.Username = username
}
if password, ok := conf["password"]; ok && password != "" {
config.Password = password
}
client, err := etcdapi.New(config)
if err != nil {
return nil, err
}
return &EtcdClient{
Client: client,
Path: path,
}, nil
}
// EtcdClient is a remote client that stores data in etcd.
type EtcdClient struct {
Client etcdapi.Client
Path string
}
func (c *EtcdClient) Get() (*Payload, error) {
resp, err := etcdapi.NewKeysAPI(c.Client).Get(context.Background(), c.Path, &etcdapi.GetOptions{Quorum: true})
if err != nil {
if err, ok := err.(etcdapi.Error); ok && err.Code == etcdapi.ErrorCodeKeyNotFound {
return nil, nil
}
return nil, err
}
if resp.Node.Dir {
return nil, fmt.Errorf("path is a directory")
}
data := []byte(resp.Node.Value)
md5 := md5.Sum(data)
return &Payload{
Data: data,
MD5: md5[:],
}, nil
}
func (c *EtcdClient) Put(data []byte) error {
_, err := etcdapi.NewKeysAPI(c.Client).Set(context.Background(), c.Path, string(data), nil)
return err
}
func (c *EtcdClient) Delete() error {
_, err := etcdapi.NewKeysAPI(c.Client).Delete(context.Background(), c.Path, nil)
return err
}

38
state/remote/etcd_test.go Normal file
View File

@ -0,0 +1,38 @@
package remote
import (
"fmt"
"os"
"testing"
"time"
)
func TestEtcdClient_impl(t *testing.T) {
var _ Client = new(EtcdClient)
}
func TestEtcdClient(t *testing.T) {
endpoint := os.Getenv("ETCD_ENDPOINT")
if endpoint == "" {
t.Skipf("skipping; ETCD_ENDPOINT must be set")
}
config := map[string]string{
"endpoints": endpoint,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
}
if username := os.Getenv("ETCD_USERNAME"); username != "" {
config["username"] = username
}
if password := os.Getenv("ETCD_PASSWORD"); password != "" {
config["password"] = password
}
client, err := etcdFactory(config)
if err != nil {
t.Fatalf("Error for valid config: %s", err)
}
testClient(t, client)
}

View File

@ -38,6 +38,7 @@ func NewClient(t string, conf map[string]string) (Client, error) {
var BuiltinClients = map[string]Factory{
"atlas": atlasFactory,
"consul": consulFactory,
"etcd": etcdFactory,
"http": httpFactory,
"s3": s3Factory,
"swift": swiftFactory,

View File

@ -58,6 +58,11 @@ The following backends are supported:
`address`, either `http` or `https`. SSL support can also be triggered
by setting then environment variable `CONSUL_HTTP_SSL` to `true`.
* Etcd - Stores the state in etcd at a given path.
Requires the `path` and `endpoints` variables. The `username` and `password`
variables can optionally be provided. `endpoints` is assumed to be a
space-separated list of etcd endpoints.
* S3 - Stores the state as a given key in a given bucket on Amazon S3.
Requires the `bucket` and `key` variables. Supports and honors the standard
AWS environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`