diff --git a/builtin/providers/consul/resource_consul_keys.go b/builtin/providers/consul/resource_consul_keys.go index 58000d7f7..a2d965785 100644 --- a/builtin/providers/consul/resource_consul_keys.go +++ b/builtin/providers/consul/resource_consul_keys.go @@ -1,13 +1,11 @@ package consul import ( - "bytes" "fmt" "log" "strconv" consulapi "github.com/hashicorp/consul/api" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" ) @@ -18,6 +16,9 @@ func resourceConsulKeys() *schema.Resource { Read: resourceConsulKeysRead, Delete: resourceConsulKeysDelete, + SchemaVersion: 1, + MigrateState: resourceConsulKeysMigrateState, + Schema: map[string]*schema.Schema{ "datacenter": &schema.Schema{ Type: schema.TypeString, @@ -64,7 +65,6 @@ func resourceConsulKeys() *schema.Resource { }, }, }, - Set: resourceConsulKeysHash, }, "var": &schema.Schema{ @@ -75,35 +75,13 @@ func resourceConsulKeys() *schema.Resource { } } -func resourceConsulKeysHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["path"].(string))) - return hashcode.String(buf.String()) -} - func resourceConsulKeysCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*consulapi.Client) kv := client.KV() - - // Resolve the datacenter first, all the other keys are dependent - // on this. - var dc string - if v, ok := d.GetOk("datacenter"); ok { - dc = v.(string) - log.Printf("[DEBUG] Consul datacenter: %s", dc) - } else { - log.Printf("[DEBUG] Resolving Consul datacenter...") - var err error - dc, err = getDC(client) - if err != nil { - return err - } - } - var token string - if v, ok := d.GetOk("token"); ok { - token = v.(string) + token := d.Get("token").(string) + dc, err := getDC(d, client) + if err != nil { + return err } // Setup the operations using the datacenter @@ -129,8 +107,6 @@ func resourceConsulKeysCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Failed to set Consul key '%s': %v", path, err) } vars[key] = value - sub["value"] = value - } else { log.Printf("[DEBUG] Getting key '%s' in %s", path, dc) pair, _, err := kv.Get(path, &qOpts) @@ -142,29 +118,29 @@ func resourceConsulKeysCreate(d *schema.ResourceData, meta interface{}) error { } } - // Update the resource + // The ID doesn't matter, since we use provider config, datacenter, + // and key paths to address consul properly. So we just need to fill it in + // with some value to indicate the resource has been created. d.SetId("consul") + + // Set the vars we collected above + if err := d.Set("var", vars); err != nil { + return err + } + // Store the datacenter on this resource, which can be helpful for reference + // in case it was read from the provider d.Set("datacenter", dc) - d.Set("key", keys) - d.Set("var", vars) + return nil } func resourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*consulapi.Client) kv := client.KV() - - // Get the DC, error if not available. - var dc string - if v, ok := d.GetOk("datacenter"); ok { - dc = v.(string) - log.Printf("[DEBUG] Consul datacenter: %s", dc) - } else { - return fmt.Errorf("Missing datacenter configuration") - } - var token string - if v, ok := d.GetOk("token"); ok { - token = v.(string) + token := d.Get("token").(string) + dc, err := getDC(d, client) + if err != nil { + return err } // Setup the operations using the datacenter @@ -189,30 +165,26 @@ func resourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error { value := attributeValue(sub, key, pair) vars[key] = value - sub["value"] = value } // Update the resource - d.Set("key", keys) - d.Set("var", vars) + if err := d.Set("var", vars); err != nil { + return err + } + // Store the datacenter on this resource, which can be helpful for reference + // in case it was read from the provider + d.Set("datacenter", dc) + return nil } func resourceConsulKeysDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*consulapi.Client) kv := client.KV() - - // Get the DC, error if not available. - var dc string - if v, ok := d.GetOk("datacenter"); ok { - dc = v.(string) - log.Printf("[DEBUG] Consul datacenter: %s", dc) - } else { - return fmt.Errorf("Missing datacenter configuration") - } - var token string - if v, ok := d.GetOk("token"); ok { - token = v.(string) + token := d.Get("token").(string) + dc, err := getDC(d, client) + if err != nil { + return err } // Setup the operations using the datacenter @@ -285,11 +257,13 @@ func attributeValue(sub map[string]interface{}, key string, pair *consulapi.KVPa } // getDC is used to get the datacenter of the local agent -func getDC(client *consulapi.Client) (string, error) { +func getDC(d *schema.ResourceData, client *consulapi.Client) (string, error) { + if v, ok := d.GetOk("datacenter"); ok { + return v.(string), nil + } 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 + return info["Config"]["Datacenter"].(string), nil } diff --git a/builtin/providers/consul/resource_consul_keys_migrate.go b/builtin/providers/consul/resource_consul_keys_migrate.go new file mode 100644 index 000000000..2aa62f890 --- /dev/null +++ b/builtin/providers/consul/resource_consul_keys_migrate.go @@ -0,0 +1,92 @@ +package consul + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func resourceConsulKeysMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found consul_keys State v0; migrating to v1") + return resourceConsulKeysMigrateStateV0toV1(is) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +func resourceConsulKeysMigrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { + if is.Empty() || is.Attributes == nil { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) + + res := resourceConsulKeys() + keys, err := readV0Keys(is, res) + if err != nil { + return is, err + } + if err := clearV0Keys(is); err != nil { + return is, err + } + if err := writeV1Keys(is, res, keys); err != nil { + return is, err + } + + log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) + return is, nil +} + +func readV0Keys( + is *terraform.InstanceState, + res *schema.Resource, +) (*schema.Set, error) { + reader := &schema.MapFieldReader{ + Schema: res.Schema, + Map: schema.BasicMapReader(is.Attributes), + } + result, err := reader.ReadField([]string{"key"}) + if err != nil { + return nil, err + } + + oldKeys, ok := result.Value.(*schema.Set) + if !ok { + return nil, fmt.Errorf("Got unexpected value from state: %#v", result.Value) + } + return oldKeys, nil +} + +func clearV0Keys(is *terraform.InstanceState) error { + for k := range is.Attributes { + if strings.HasPrefix(k, "key.") { + delete(is.Attributes, k) + } + } + return nil +} + +func writeV1Keys( + is *terraform.InstanceState, + res *schema.Resource, + keys *schema.Set, +) error { + writer := schema.MapFieldWriter{ + Schema: res.Schema, + } + if err := writer.WriteField([]string{"key"}, keys); err != nil { + return err + } + for k, v := range writer.Map() { + is.Attributes[k] = v + } + + return nil +} diff --git a/builtin/providers/consul/resource_consul_keys_migrate_test.go b/builtin/providers/consul/resource_consul_keys_migrate_test.go new file mode 100644 index 000000000..e1935b524 --- /dev/null +++ b/builtin/providers/consul/resource_consul_keys_migrate_test.go @@ -0,0 +1,90 @@ +package consul + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestConsulKeysMigrateState(t *testing.T) { + cases := map[string]struct { + StateVersion int + Attributes map[string]string + Expected map[string]string + Meta interface{} + }{ + "v0.6.9 and earlier, with old values hash function": { + StateVersion: 0, + Attributes: map[string]string{ + "key.#": "2", + "key.12345.name": "hello", + "key.12345.path": "foo/bar", + "key.12345.value": "world", + "key.12345.default": "", + "key.12345.delete": "false", + "key.6789.name": "temp", + "key.6789.path": "foo/foo", + "key.6789.value": "", + "key.6789.default": "", + "key.6789.delete": "true", + }, + Expected: map[string]string{ + "key.#": "2", + "key.2401383718.default": "", + "key.2401383718.delete": "true", + "key.2401383718.name": "temp", + "key.2401383718.path": "foo/foo", + "key.2401383718.value": "", + "key.3116955509.path": "foo/bar", + "key.3116955509.default": "", + "key.3116955509.delete": "false", + "key.3116955509.name": "hello", + "key.3116955509.value": "world", + }, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: "consul", + Attributes: tc.Attributes, + } + is, err := resourceConsulKeys().MigrateState( + tc.StateVersion, is, tc.Meta) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.Expected { + if is.Attributes[k] != v { + t.Fatalf( + "bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v", + tn, k, v, k, is.Attributes[k], is.Attributes) + } + } + } +} + +func TestConsulKeysMigrateState_empty(t *testing.T) { + var is *terraform.InstanceState + var meta interface{} + + // should handle nil + is, err := resourceConsulKeys().MigrateState(0, is, meta) + + if err != nil { + t.Fatalf("err: %#v", err) + } + if is != nil { + t.Fatalf("expected nil instancestate, got: %#v", is) + } + + // should handle non-nil but empty + is = &terraform.InstanceState{} + is, err = resourceConsulKeys().MigrateState(0, is, meta) + + if err != nil { + t.Fatalf("err: %#v", err) + } +} diff --git a/builtin/providers/consul/resource_consul_keys_test.go b/builtin/providers/consul/resource_consul_keys_test.go index 97890335c..a820ff331 100644 --- a/builtin/providers/consul/resource_consul_keys_test.go +++ b/builtin/providers/consul/resource_consul_keys_test.go @@ -11,7 +11,7 @@ import ( func TestAccConsulKeys_basic(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() {}, + PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckConsulKeysDestroy, Steps: []resource.TestStep{ @@ -19,18 +19,25 @@ func TestAccConsulKeys_basic(t *testing.T) { Config: testAccConsulKeysConfig, Check: resource.ComposeTestCheckFunc( testAccCheckConsulKeysExists(), - testAccCheckConsulKeysValue("consul_keys.app", "time", ""), testAccCheckConsulKeysValue("consul_keys.app", "enabled", "true"), testAccCheckConsulKeysValue("consul_keys.app", "set", "acceptance"), ), }, + resource.TestStep{ + Config: testAccConsulKeysConfig_Update, + Check: resource.ComposeTestCheckFunc( + testAccCheckConsulKeysExists(), + testAccCheckConsulKeysValue("consul_keys.app", "enabled", "true"), + testAccCheckConsulKeysValue("consul_keys.app", "set", "acceptanceUpdated"), + ), + }, }, }) } func testAccCheckConsulKeysDestroy(s *terraform.State) error { kv := testAccProvider.Meta().(*consulapi.Client).KV() - opts := &consulapi.QueryOptions{Datacenter: "nyc3"} + opts := &consulapi.QueryOptions{Datacenter: "dc1"} pair, _, err := kv.Get("test/set", opts) if err != nil { return err @@ -44,7 +51,7 @@ func testAccCheckConsulKeysDestroy(s *terraform.State) error { func testAccCheckConsulKeysExists() resource.TestCheckFunc { return func(s *terraform.State) error { kv := testAccProvider.Meta().(*consulapi.Client).KV() - opts := &consulapi.QueryOptions{Datacenter: "nyc3"} + opts := &consulapi.QueryOptions{Datacenter: "dc1"} pair, _, err := kv.Get("test/set", opts) if err != nil { return err @@ -78,11 +85,7 @@ func testAccCheckConsulKeysValue(n, attr, val string) resource.TestCheckFunc { const testAccConsulKeysConfig = ` resource "consul_keys" "app" { - datacenter = "nyc3" - key { - name = "time" - path = "global/time" - } + datacenter = "dc1" key { name = "enabled" path = "test/enabled" @@ -96,3 +99,20 @@ resource "consul_keys" "app" { } } ` + +const testAccConsulKeysConfig_Update = ` +resource "consul_keys" "app" { + datacenter = "dc1" + key { + name = "enabled" + path = "test/enabled" + default = "true" + } + key { + name = "set" + path = "test/set" + value = "acceptanceUpdated" + delete = true + } +} +` diff --git a/builtin/providers/consul/resource_provider_test.go b/builtin/providers/consul/resource_provider_test.go index 15ee38397..eb7f73ba0 100644 --- a/builtin/providers/consul/resource_provider_test.go +++ b/builtin/providers/consul/resource_provider_test.go @@ -1,6 +1,7 @@ package consul import ( + "os" "testing" consulapi "github.com/hashicorp/consul/api" @@ -21,7 +22,6 @@ func init() { // Use the demo address for the acceptance tests testAccProvider.ConfigureFunc = func(d *schema.ResourceData) (interface{}, error) { conf := consulapi.DefaultConfig() - conf.Address = "demo.consul.io:80" return consulapi.NewClient(conf) } } @@ -55,3 +55,9 @@ func TestResourceProvider_Configure(t *testing.T) { t.Fatalf("err: %s", err) } } + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("CONSUL_HTTP_ADDR"); v == "" { + t.Fatal("CONSUL_HTTP_ADDR must be set for acceptance tests") + } +} diff --git a/helper/schema/field_writer_map.go b/helper/schema/field_writer_map.go index 433ff7df6..bc8fcd896 100644 --- a/helper/schema/field_writer_map.go +++ b/helper/schema/field_writer_map.go @@ -277,7 +277,7 @@ func (w *MapFieldWriter) setSet( // not the `value` directly is because this forces all types // to become []interface{} (generic) instead of []string, which // most hash functions are expecting. - s := &Set{F: schema.Set} + s := schema.ZeroValue().(*Set) tempR := &MapFieldReader{ Map: BasicMapReader(tempW.Map()), Schema: tempSchemaMap,