From bb51882f337e80ca85b75069155ed03c52636dd3 Mon Sep 17 00:00:00 2001 From: Nathan Zadoks Date: Mon, 12 Oct 2015 17:04:58 -0400 Subject: [PATCH 1/2] Etcd remote state backend --- state/remote/etcd.go | 78 +++++++++++++++++++++++++++++++++++++++ state/remote/etcd_test.go | 38 +++++++++++++++++++ state/remote/remote.go | 1 + 3 files changed, 117 insertions(+) create mode 100644 state/remote/etcd.go create mode 100644 state/remote/etcd_test.go diff --git a/state/remote/etcd.go b/state/remote/etcd.go new file mode 100644 index 000000000000..f596a8492ccc --- /dev/null +++ b/state/remote/etcd.go @@ -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 +} diff --git a/state/remote/etcd_test.go b/state/remote/etcd_test.go new file mode 100644 index 000000000000..6d06d801b2a4 --- /dev/null +++ b/state/remote/etcd_test.go @@ -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) +} diff --git a/state/remote/remote.go b/state/remote/remote.go index 7ebea322296e..5337ad7b7bbe 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -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, From 362a2035c0bb709162ab75d47ef6db2b23bcef56 Mon Sep 17 00:00:00 2001 From: Nathan Zadoks Date: Thu, 15 Oct 2015 22:32:59 -0400 Subject: [PATCH 2/2] Document the etcd remote state backend --- website/source/docs/commands/remote-config.html.markdown | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/source/docs/commands/remote-config.html.markdown b/website/source/docs/commands/remote-config.html.markdown index 73a06f8211e8..aaa4148fcbce 100644 --- a/website/source/docs/commands/remote-config.html.markdown +++ b/website/source/docs/commands/remote-config.html.markdown @@ -50,6 +50,11 @@ The following backends are supported: variables can optionally be provided. Address is assumed to be the local agent if not provided. +* 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`