diff --git a/backend/remote-state/backend.go b/backend/remote-state/backend.go new file mode 100644 index 000000000..bcb6912bc --- /dev/null +++ b/backend/remote-state/backend.go @@ -0,0 +1,57 @@ +// Package remotestate implements a Backend for remote state implementations +// from the state/remote package that also implement a backend schema for +// configuration. +package remotestate + +import ( + "context" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" + "github.com/hashicorp/terraform/terraform" +) + +// Backend implements backend.Backend for remote state backends. +// +// All exported fields should be set. This struct should only be used +// by implementers of backends, not by consumers. If you're consuming, please +// use a higher level package such as Consul backends. +type Backend struct { + // Backend should be set to the configuration schema. ConfigureFunc + // should not be set on the schema. + *schema.Backend + + // ConfigureFunc takes the ctx from a schema.Backend and returns a + // fully configured remote client to use for state operations. + ConfigureFunc func(ctx context.Context) (remote.Client, error) + + client remote.Client +} + +func (b *Backend) Configure(rc *terraform.ResourceConfig) error { + // Set our configureFunc manually + b.Backend.ConfigureFunc = func(ctx context.Context) error { + c, err := b.ConfigureFunc(ctx) + if err != nil { + return err + } + + // Set the client for later + b.client = c + return nil + } + + // Run the normal configuration + return b.Backend.Configure(rc) +} + +func (b *Backend) State() (state.State, error) { + // This shouldn't happen + if b.client == nil { + panic("nil remote client") + } + + s := &remote.State{Client: b.client} + return s, nil +} diff --git a/backend/remote-state/backend_test.go b/backend/remote-state/backend_test.go new file mode 100644 index 000000000..d1f7adcc1 --- /dev/null +++ b/backend/remote-state/backend_test.go @@ -0,0 +1,11 @@ +package remotestate + +import ( + "testing" + + "github.com/hashicorp/terraform/backend" +) + +func TestBackend_impl(t *testing.T) { + var _ backend.Backend = new(Backend) +} diff --git a/backend/remote-state/consul/backend.go b/backend/remote-state/consul/backend.go new file mode 100644 index 000000000..a1f75eb85 --- /dev/null +++ b/backend/remote-state/consul/backend.go @@ -0,0 +1,111 @@ +package consul + +import ( + "context" + "strings" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/backend/remote-state" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/state/remote" +) + +// New creates a new backend for Consul remote state. +func New() backend.Backend { + return &remotestate.Backend{ + ConfigureFunc: configure, + + // Set the schema + Backend: &schema.Backend{ + Schema: map[string]*schema.Schema{ + "path": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Path to store state in Consul", + }, + + "access_token": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Access token for a Consul ACL", + Default: "", // To prevent input + }, + + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Address to the Consul Cluster", + }, + + "scheme": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Scheme to communicate to Consul with", + Default: "", // To prevent input + }, + + "datacenter": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Datacenter to communicate with", + Default: "", // To prevent input + }, + + "http_auth": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "HTTP Auth in the format of 'username:password'", + Default: "", // To prevent input + }, + }, + }, + } +} + +func configure(ctx context.Context) (remote.Client, error) { + // Grab the resource data + data := schema.FromContextBackendConfig(ctx) + + // Configure the client + config := consulapi.DefaultConfig() + if v, ok := data.GetOk("access_token"); ok && v.(string) != "" { + config.Token = v.(string) + } + if v, ok := data.GetOk("address"); ok && v.(string) != "" { + config.Address = v.(string) + } + if v, ok := data.GetOk("scheme"); ok && v.(string) != "" { + config.Scheme = v.(string) + } + if v, ok := data.GetOk("datacenter"); ok && v.(string) != "" { + config.Datacenter = v.(string) + } + if v, ok := data.GetOk("http_auth"); ok && v.(string) != "" { + auth := v.(string) + + var username, password string + if strings.Contains(auth, ":") { + split := strings.SplitN(auth, ":", 2) + username = split[0] + password = split[1] + } else { + username = auth + } + + config.HttpAuth = &consulapi.HttpBasicAuth{ + Username: username, + Password: password, + } + } + + client, err := consulapi.NewClient(config) + if err != nil { + return nil, err + } + + return &RemoteClient{ + Client: client, + Path: data.Get("path").(string), + }, nil +} diff --git a/backend/remote-state/consul/client.go b/backend/remote-state/consul/client.go new file mode 100644 index 000000000..51df7a31d --- /dev/null +++ b/backend/remote-state/consul/client.go @@ -0,0 +1,45 @@ +package consul + +import ( + "crypto/md5" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/state/remote" +) + +// RemoteClient is a remote client that stores data in Consul. +type RemoteClient struct { + Client *consulapi.Client + Path string +} + +func (c *RemoteClient) Get() (*remote.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 &remote.Payload{ + Data: pair.Value, + MD5: md5[:], + }, nil +} + +func (c *RemoteClient) Put(data []byte) error { + kv := c.Client.KV() + _, err := kv.Put(&consulapi.KVPair{ + Key: c.Path, + Value: data, + }, nil) + return err +} + +func (c *RemoteClient) Delete() error { + kv := c.Client.KV() + _, err := kv.Delete(c.Path, nil) + return err +} diff --git a/backend/remote-state/consul/client_test.go b/backend/remote-state/consul/client_test.go new file mode 100644 index 000000000..ce726fd5d --- /dev/null +++ b/backend/remote-state/consul/client_test.go @@ -0,0 +1,29 @@ +package consul + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/backend/remote-state" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/state/remote" +) + +func TestRemoteClient_impl(t *testing.T) { + var _ remote.Client = new(RemoteClient) +} + +func TestRemoteClient(t *testing.T) { + acctest.RemoteTestPrecheck(t) + + // Get the backend + b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + "address": "demo.consul.io:80", + "path": fmt.Sprintf("tf-unit/%s", time.Now().String()), + }) + + // Test + remotestate.TestClient(t, b) +} diff --git a/backend/remote-state/testing.go b/backend/remote-state/testing.go new file mode 100644 index 000000000..17d9b80f5 --- /dev/null +++ b/backend/remote-state/testing.go @@ -0,0 +1,17 @@ +package remotestate + +import ( + "testing" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state/remote" +) + +func TestClient(t *testing.T, raw backend.Backend) { + b, ok := raw.(*Backend) + if !ok { + t.Fatalf("not Backend: %T", raw) + } + + remote.TestClient(t, b.client) +}