From dd58896d0749071457d503c8d1fb1218559c6db9 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 25 Jul 2014 22:14:48 -0400 Subject: [PATCH] provider/consul: first pass --- builtin/providers/consul/config.go | 3 +- .../providers/consul/resource_consul_key.go | 321 ++++++++++++++++-- .../consul/resource_consul_keys_test.go | 95 ++++++ builtin/providers/consul/resource_provider.go | 9 +- .../consul/resource_provider_test.go | 52 +++ builtin/providers/consul/resources.go | 11 +- 6 files changed, 463 insertions(+), 28 deletions(-) create mode 100644 builtin/providers/consul/resource_consul_keys_test.go create mode 100644 builtin/providers/consul/resource_provider_test.go diff --git a/builtin/providers/consul/config.go b/builtin/providers/consul/config.go index c5e94a03d..868907f6f 100644 --- a/builtin/providers/consul/config.go +++ b/builtin/providers/consul/config.go @@ -24,7 +24,8 @@ func (c *Config) Client() (*consulapi.Client, error) { } client, err := consulapi.NewClient(config) - log.Printf("[INFO] Consul Client configured with: %#v", config) + log.Printf("[INFO] Consul Client configured with address: '%s', datacenter: '%s'", + config.Address, config.Datacenter) if err != nil { return nil, err } diff --git a/builtin/providers/consul/resource_consul_key.go b/builtin/providers/consul/resource_consul_key.go index 047aa055c..dff4b53dc 100644 --- a/builtin/providers/consul/resource_consul_key.go +++ b/builtin/providers/consul/resource_consul_key.go @@ -1,35 +1,174 @@ package consul import ( - "github.com/hashicorp/terraform/helper/config" + "fmt" + "log" + + "github.com/armon/consul-api" "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/mapstructure" ) -func resource_consul_keys_validation() *config.Validator { - return &config.Validator{ - Optional: []string{ - "datacenter", - "*.key", - "*.value", - "*.default", - "*.delete", - }, +type consulKeys map[string]*consulKey + +type consulKey struct { + Key string + Value string + Default string + Delete bool + + SetValue bool `mapstructure:"-"` + SetDefault bool `mapstructure:"-"` +} + +func resource_consul_keys_validate(c *terraform.ResourceConfig) (ws []string, es []error) { + conf := c.Raw + for k, v := range conf { + // datacenter is special and can be ignored + if k == "datacenter" { + continue + } + + keyList, ok := v.([]map[string]interface{}) + if !ok { + es = append(es, fmt.Errorf("Field '%s' must be map containing a key", k)) + continue + } + if len(keyList) > 1 { + es = append(es, fmt.Errorf("Field '%s' is defined more than once", k)) + continue + } + key := keyList[0] + + for sub, val := range key { + // Verify the sub-key is supported + switch sub { + case "key": + case "value": + case "default": + case "delete": + default: + es = append(es, fmt.Errorf("Field '%s' has unsupported config '%s'", k, sub)) + continue + } + + // Verify value is of the correct type + _, isStr := val.(string) + _, isBool := val.(bool) + if !isStr && sub != "delete" { + es = append(es, fmt.Errorf("Field '%s' must set '%s' as a string", key, sub)) + } + if !isBool && sub == "delete" { + es = append(es, fmt.Errorf("Field '%s' must set '%s' as a bool", key, sub)) + } + } } + return } func resource_consul_keys_create( s *terraform.ResourceState, d *terraform.ResourceDiff, meta interface{}) (*terraform.ResourceState, error) { - // Merge the diff into the state so that we have all the attributes - // properly. - rs := s.MergeDiff(d) - return rs, nil + p := meta.(*ResourceProvider) + if s.Attributes == nil { + s.Attributes = make(map[string]string) + } + + // Load the configuration + var config map[string]interface{} + for _, attr := range d.Attributes { + if attr.NewExtra != nil { + config = attr.NewExtra.(map[string]interface{}) + break + } + } + if config == nil { + return s, fmt.Errorf("Missing configuration state") + } + dc, keys, err := partsFromConfig(config) + if err != nil { + return s, err + } + + // Check if we are missing a datacenter + if dc == "" { + dc, err = get_dc(p.client) + } + s.Attributes["datacenter"] = dc + + // Handle each of the keys + kv := p.client.KV() + qOpts := consulapi.QueryOptions{Datacenter: dc} + wOpts := consulapi.WriteOptions{Datacenter: dc} + for name, conf := range keys { + if conf.SetValue { + log.Printf("[DEBUG] Setting key '%s' to '%v' in %s", conf.Key, conf.Value, dc) + pair := consulapi.KVPair{Key: conf.Key, Value: []byte(conf.Value)} + if _, err := kv.Put(&pair, &wOpts); err != nil { + return s, fmt.Errorf("Failed to set Consul key '%s': %v", conf.Key, err) + } + s.Attributes[name] = conf.Value + } else { + log.Printf("[DEBUG] Getting key '%s' in %s", conf.Key, dc) + pair, _, err := kv.Get(conf.Key, &qOpts) + if err != nil { + return s, fmt.Errorf("Failed to get Consul key '%s': %v", conf.Key, err) + } + if pair == nil && conf.SetDefault { + s.Attributes[name] = conf.Default + } else if pair == nil { + s.Attributes[name] = "" + } else { + s.Attributes[name] = string(pair.Value) + } + } + } + + // Set an ID, store the config + s.ID = "consul" + s.Extra = config + return s, nil +} + +// get_dc is used to get the datacenter of the local agent +func get_dc(client *consulapi.Client) (string, error) { + info, err := client.Agent().Self() + if err != nil { + return "", fmt.Errorf("Failed to get datacenter from Consul agent: %v", err) + } + dc := info["Config"]["Datacenter"].(string) + return dc, nil } func resource_consul_keys_destroy( s *terraform.ResourceState, meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + kv := client.KV() + + // Restore our configuration + dc, keys, err := partsFromConfig(s.Extra) + if err != nil { + return err + } + + // Load the DC if not given + if dc == "" { + dc = s.Attributes["datacenter"] + } + opts := consulapi.WriteOptions{Datacenter: dc} + for _, key := range keys { + // Skip any non-managed keys + if !key.Delete { + continue + } + log.Printf("[DEBUG] Deleting key '%s' in %s", key.Key, dc) + if _, err := kv.Delete(key.Key, &opts); err != nil { + return fmt.Errorf("Failed to delete Consul key '%s': %v", key.Key, err) + } + } return nil } @@ -37,21 +176,165 @@ func resource_consul_keys_update( s *terraform.ResourceState, d *terraform.ResourceDiff, meta interface{}) (*terraform.ResourceState, error) { - // Merge the diff into the state so that we have all the attributes - // properly. - rs := s.MergeDiff(d) - return rs, nil + // TODO + panic("update not supported") + return s, nil } func resource_consul_keys_diff( s *terraform.ResourceState, c *terraform.ResourceConfig, meta interface{}) (*terraform.ResourceDiff, error) { - return nil, nil + // Parse the configuration + dc, keys, err := partsFromConfig(c.Config) + if err != nil { + return nil, err + } + + // Get the old values + oldValues := s.Attributes + + // Initialize the diff set + attrs := make(map[string]*terraform.ResourceAttrDiff) + diff := &terraform.ResourceDiff{Attributes: attrs} + + // Handle removed attributes + for key, oldVal := range oldValues { + if key == "datacenter" { + continue + } + if _, keep := keys[key]; !keep { + attrs[key] = &terraform.ResourceAttrDiff{ + Old: oldVal, + NewRemoved: true, + } + } + } + + // Handle added or changed attributes + for key, conf := range keys { + aDiff := &terraform.ResourceAttrDiff{ + Type: terraform.DiffAttrInput, + } + oldVal, ok := oldValues[key] + if conf.SetValue { + aDiff.New = conf.Value + } else { + aDiff.NewComputed = true + } + if ok { + aDiff.Old = oldVal + } + + // If this is new or changed we need to refresh + if !ok || (conf.SetValue && oldVal != conf.Value) { + attrs[key] = aDiff + } + } + + // If the DC has changed, require a destroy! + if old := oldValues["datacenter"]; dc != old { + aDiff := &terraform.ResourceAttrDiff{ + Old: old, + New: dc, + RequiresNew: true, + Type: terraform.DiffAttrInput, + } + if aDiff.New == "" { + aDiff.NewComputed = true + } + attrs["datacenter"] = aDiff + } + + // Make sure one of the attributes contains the configuration + if len(attrs) > 0 { + for _, aDiff := range attrs { + aDiff.NewExtra = c.Config + break + } + } + return diff, nil } func resource_consul_keys_refresh( s *terraform.ResourceState, meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + agent := client.Agent() + kv := client.KV() + + // Restore our configuration + dc, keys, err := partsFromConfig(s.Extra) + if err != nil { + return s, err + } + + // Check if we are missing a datacenter + if dc == "" { + info, err := agent.Self() + if err != nil { + return s, fmt.Errorf("Failed to get datacenter from Consul agent: %v", err) + } + dc = info["Config"]["Datacenter"].(string) + } + + // Update the attributes + s.Attributes["datacenter"] = dc + opts := consulapi.QueryOptions{Datacenter: dc} + for name, key := range keys { + pair, _, err := kv.Get(key.Key, &opts) + if err != nil { + return s, fmt.Errorf("Failed to get key '%s' from Consul: %v", key.Key, err) + } + if pair == nil && key.SetDefault { + s.Attributes[name] = key.Default + } else if pair == nil { + s.Attributes[name] = "" + } else { + s.Attributes[name] = string(pair.Value) + } + } return s, nil } + +// partsFromConfig extracts the relevant configuration from the raw format +func partsFromConfig(raw map[string]interface{}) (string, consulKeys, error) { + var dc string + keys := make(map[string]*consulKey) + for k, v := range raw { + // datacenter is special and can be ignored + if k == "datacenter" { + vStr, ok := v.(string) + if !ok { + return "", nil, fmt.Errorf("datacenter must be a string") + } + dc = vStr + continue + } + + confs, ok := v.([]map[string]interface{}) + if !ok { + return "", nil, fmt.Errorf("Field '%s' must be map containing a key", k) + } + if len(confs) > 1 { + return "", nil, fmt.Errorf("Field '%s' has duplicate definitions", k) + } + conf := confs[0] + + key := &consulKey{} + if err := mapstructure.WeakDecode(conf, key); err != nil { + return "", nil, fmt.Errorf("Field '%s' failed to decode: %v", k, err) + } + for sub := range conf { + switch sub { + case "value": + key.SetValue = true + case "default": + key.SetDefault = true + } + } + keys[k] = key + } + return dc, keys, nil +} diff --git a/builtin/providers/consul/resource_consul_keys_test.go b/builtin/providers/consul/resource_consul_keys_test.go new file mode 100644 index 000000000..f7bef6163 --- /dev/null +++ b/builtin/providers/consul/resource_consul_keys_test.go @@ -0,0 +1,95 @@ +package consul + +import ( + "fmt" + "testing" + + "github.com/armon/consul-api" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccConsulKeys(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() {}, + Providers: testAccProviders, + CheckDestroy: testAccCheckConsulKeysDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccConsulKeysConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckConsulKeysExists(), + testAccCheckConsulKeysValue("consul_keys.app", "time", ""), + testAccCheckConsulKeysValue("consul_keys.app", "enabled", "true"), + testAccCheckConsulKeysValue("consul_keys.app", "set", "acceptance"), + ), + }, + }, + }) +} + +func testAccCheckConsulKeysDestroy(s *terraform.State) error { + kv := testAccProvider.client.KV() + opts := &consulapi.QueryOptions{Datacenter: "nyc1"} + pair, _, err := kv.Get("test/set", opts) + if err != nil { + return err + } + if pair != nil { + return fmt.Errorf("Key still exists: %#v", pair) + } + return nil +} + +func testAccCheckConsulKeysExists() resource.TestCheckFunc { + return func(s *terraform.State) error { + kv := testAccProvider.client.KV() + opts := &consulapi.QueryOptions{Datacenter: "nyc1"} + pair, _, err := kv.Get("test/set", opts) + if err != nil { + return err + } + if pair == nil { + return fmt.Errorf("Key 'test/set' does not exist") + } + return nil + } +} + +func testAccCheckConsulKeysValue(n, attr, val string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rn, ok := s.Resources[n] + if !ok { + return fmt.Errorf("Resource not found") + } + out, ok := rn.Attributes[attr] + if !ok { + return fmt.Errorf("Attribute '%s' not found: %#v", attr, rn.Attributes) + } + if val != "" && out != val { + return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val) + } + if val == "" && out == "" { + return fmt.Errorf("Attribute '%s' value '%s'", attr, out) + } + return nil + } +} + +const testAccConsulKeysConfig = ` +resource "consul_keys" "app" { + datacenter = "nyc1" + time { + key = "global/time" + } + enabled { + key = "test/enabled" + default = "true" + } + set { + key = "test/set" + value = "acceptance" + delete = true + } +} +` diff --git a/builtin/providers/consul/resource_provider.go b/builtin/providers/consul/resource_provider.go index 410b8e6d6..2ca299abf 100644 --- a/builtin/providers/consul/resource_provider.go +++ b/builtin/providers/consul/resource_provider.go @@ -25,7 +25,12 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er func (p *ResourceProvider) ValidateResource( t string, c *terraform.ResourceConfig) ([]string, []error) { - return resourceMap.Validate(t, c) + switch t { + case "consul_keys": + return resource_consul_keys_validate(c) + default: + return resourceMap.Validate(t, c) + } } func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error { @@ -33,7 +38,7 @@ func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error { return err } - log.Println("[INFO] Initializing Consul client") + log.Printf("[INFO] Initializing Consul client") var err error p.client, err = p.Config.Client() if err != nil { diff --git a/builtin/providers/consul/resource_provider_test.go b/builtin/providers/consul/resource_provider_test.go new file mode 100644 index 000000000..ca1adcc5b --- /dev/null +++ b/builtin/providers/consul/resource_provider_test.go @@ -0,0 +1,52 @@ +package consul + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *ResourceProvider + +func init() { + testAccProvider = new(ResourceProvider) + testAccProvider.Config.Address = "demo.consul.io:80" + testAccProviders = map[string]terraform.ResourceProvider{ + "consul": testAccProvider, + } +} + +func TestResourceProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = new(ResourceProvider) +} + +func TestResourceProvider_Configure(t *testing.T) { + rp := new(ResourceProvider) + + raw := map[string]interface{}{ + "address": "demo.consul.io:80", + "datacenter": "nyc1", + } + + rawConfig, err := config.NewRawConfig(raw) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = rp.Configure(terraform.NewResourceConfig(rawConfig)) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := Config{ + Address: "demo.consul.io:80", + Datacenter: "nyc1", + } + + if !reflect.DeepEqual(rp.Config, expected) { + t.Fatalf("bad: %#v", rp.Config) + } +} diff --git a/builtin/providers/consul/resources.go b/builtin/providers/consul/resources.go index 5829d059a..1de2c6b2b 100644 --- a/builtin/providers/consul/resources.go +++ b/builtin/providers/consul/resources.go @@ -12,12 +12,11 @@ func init() { resourceMap = &resource.Map{ Mapping: map[string]resource.Resource{ "consul_keys": resource.Resource{ - ConfigValidator: resource_consul_keys_validation(), - Create: resource_consul_keys_create, - Destroy: resource_consul_keys_destroy, - Update: resource_consul_keys_update, - Diff: resource_consul_keys_diff, - Refresh: resource_consul_keys_refresh, + Create: resource_consul_keys_create, + Destroy: resource_consul_keys_destroy, + Update: resource_consul_keys_update, + Diff: resource_consul_keys_diff, + Refresh: resource_consul_keys_refresh, }, }, }