diff --git a/state/remote/consul.go b/state/remote/consul.go new file mode 100644 index 000000000..ca98633d6 --- /dev/null +++ b/state/remote/consul.go @@ -0,0 +1,69 @@ +package remote + +import ( + "crypto/md5" + "fmt" + + consulapi "github.com/hashicorp/consul/api" +) + +func consulFactory(conf map[string]string) (Client, error) { + path, ok := conf["path"] + if !ok { + return nil, fmt.Errorf("missing 'path' configuration") + } + + config := consulapi.DefaultConfig() + if token, ok := conf["access_token"]; ok && token != "" { + config.Token = token + } + if addr, ok := conf["address"]; ok && addr != "" { + config.Address = addr + } + + client, err := consulapi.NewClient(config) + if err != nil { + return nil, err + } + + return &ConsulClient{ + Client: client, + Path: path, + }, nil +} + +type ConsulClient struct { + Client *consulapi.Client + Path string +} + +func (c *ConsulClient) Get() (*Payload, error) { + pair, _, err := c.Client.KV().Get(c.Path, nil) + if err != nil { + return nil, err + } + if pair == nil { + return nil, nil + } + + md5 := md5.Sum(pair.Value) + return &Payload{ + Data: pair.Value, + MD5: md5[:], + }, nil +} + +func (c *ConsulClient) Put(data []byte) error { + kv := c.Client.KV() + _, err := kv.Put(&consulapi.KVPair{ + Key: c.Path, + Value: data, + }, nil) + return err +} + +func (c *ConsulClient) Delete() error { + kv := c.Client.KV() + _, err := kv.Delete(c.Path, nil) + return err +} diff --git a/state/remote/consul_test.go b/state/remote/consul_test.go new file mode 100644 index 000000000..d478c1264 --- /dev/null +++ b/state/remote/consul_test.go @@ -0,0 +1,28 @@ +package remote + +import ( + "fmt" + "net/http" + "testing" + "time" +) + +func TestConsulClient_impl(t *testing.T) { + var _ Client = new(ConsulClient) +} + +func TestConsulClient(t *testing.T) { + if _, err := http.Get("http://google.com"); err != nil { + t.Skipf("skipping, internet seems to not be available: %s", err) + } + + client, err := consulFactory(map[string]string{ + "address": "demo.consul.io:80", + "path": fmt.Sprintf("tf-unit/%s", time.Now().String()), + }) + if err != nil { + t.Fatalf("bad: %s", err) + } + + testClient(t, client) +} diff --git a/state/remote/remote.go b/state/remote/remote.go index 28cba741b..73020c66c 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -1,5 +1,9 @@ package remote +import ( + "fmt" +) + // Client is the interface that must be implemented for a remote state // driver. It supports dumb put/get/delete, and the higher level structs // handle persisting the state properly here. @@ -17,3 +21,20 @@ type Payload struct { // Factory is the factory function to create a remote client. type Factory func(map[string]string) (Client, error) + +// NewClient returns a new Client with the given type and configuration. +// The client is looked up in the BuiltinClients variable. +func NewClient(t string, conf map[string]string) (Client, error) { + f, ok := BuiltinClients[t] + if !ok { + return nil, fmt.Errorf("unknown remote client type: %s", t) + } + + return f(conf) +} + +// BuiltinClients is the list of built-in clients that can be used with +// NewClient. +var BuiltinClients = map[string]Factory{ + "consul": consulFactory, +} diff --git a/state/remote/remote_test.go b/state/remote/remote_test.go new file mode 100644 index 000000000..fa25c8aed --- /dev/null +++ b/state/remote/remote_test.go @@ -0,0 +1,35 @@ +package remote + +import ( + "bytes" + "testing" +) + +// testClient is a generic function to test any client. +func testClient(t *testing.T, c Client) { + data := []byte("foo") + + if err := c.Put(data); err != nil { + t.Fatalf("put: %s", err) + } + + p, err := c.Get() + if err != nil { + t.Fatalf("get: %s", err) + } + if !bytes.Equal(p.Data, data) { + t.Fatalf("bad: %#v", p) + } + + if err := c.Delete(); err != nil { + t.Fatalf("delete: %s", err) + } + + p, err = c.Get() + if err != nil { + t.Fatalf("get: %s", err) + } + if p != nil { + t.Fatalf("bad: %#v", p) + } +}