Skip to content

Commit

Permalink
Merge pull request #5988 from apparentlymart/consul-key-subtree
Browse files Browse the repository at this point in the history
provider/consul: consul_key_prefix resource
  • Loading branch information
jen20 committed Apr 5, 2016
2 parents 317ce31 + d706130 commit fe4ddba
Show file tree
Hide file tree
Showing 6 changed files with 489 additions and 1 deletion.
30 changes: 30 additions & 0 deletions builtin/providers/consul/key_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ func (c *keyClient) Get(path string) (string, error) {
return value, nil
}

func (c *keyClient) GetUnderPrefix(pathPrefix string) (map[string]string, error) {
log.Printf(
"[DEBUG] Listing keys under '%s' in %s",
pathPrefix, c.qOpts.Datacenter,
)
pairs, _, err := c.client.List(pathPrefix, c.qOpts)
if err != nil {
return nil, fmt.Errorf(
"Failed to list Consul keys under prefix '%s': %s", pathPrefix, err,
)
}
value := map[string]string{}
for _, pair := range pairs {
subKey := pair.Key[len(pathPrefix):]
value[subKey] = string(pair.Value)
}
return value, nil
}

func (c *keyClient) Put(path, value string) error {
log.Printf(
"[DEBUG] Setting key '%s' to '%v' in %s",
Expand All @@ -64,3 +83,14 @@ func (c *keyClient) Delete(path string) error {
}
return nil
}

func (c *keyClient) DeleteUnderPrefix(pathPrefix string) error {
log.Printf(
"[DEBUG] Deleting all keys under prefix '%s' in %s",
pathPrefix, c.wOpts.Datacenter,
)
if _, err := c.client.DeleteTree(pathPrefix, c.wOpts); err != nil {
return fmt.Errorf("Failed to delete Consul keys under '%s': %s", pathPrefix, err)
}
return nil
}
221 changes: 221 additions & 0 deletions builtin/providers/consul/resource_consul_key_prefix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package consul

import (
"fmt"

consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceConsulKeyPrefix() *schema.Resource {
return &schema.Resource{
Create: resourceConsulKeyPrefixCreate,
Update: resourceConsulKeyPrefixUpdate,
Read: resourceConsulKeyPrefixRead,
Delete: resourceConsulKeyPrefixDelete,

Schema: map[string]*schema.Schema{
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},

"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},

"path_prefix": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"subkeys": &schema.Schema{
Type: schema.TypeMap,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}

func resourceConsulKeyPrefixCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
kv := client.KV()
token := d.Get("token").(string)
dc, err := getDC(d, client)
if err != nil {
return err
}

keyClient := newKeyClient(kv, dc, token)

pathPrefix := d.Get("path_prefix").(string)
subKeys := map[string]string{}
for k, vI := range d.Get("subkeys").(map[string]interface{}) {
subKeys[k] = vI.(string)
}

// To reduce the impact of mistakes, we will only "create" a prefix that
// is currently empty. This way we are less likely to accidentally
// conflict with other mechanisms managing the same prefix.
currentSubKeys, err := keyClient.GetUnderPrefix(pathPrefix)
if err != nil {
return err
}
if len(currentSubKeys) > 0 {
return fmt.Errorf(
"%d keys already exist under %s; delete them before managing this prefix with Terraform",
len(currentSubKeys), pathPrefix,
)
}

// Ideally we'd use d.Partial(true) here so we can correctly record
// a partial write, but that mechanism doesn't work for individual map
// members, so we record that the resource was created before we
// do anything and that way we can recover from errors by doing an
// Update on subsequent runs, rather than re-attempting Create with
// some keys possibly already present.
d.SetId(pathPrefix)

// Store the datacenter on this resource, which can be helpful for reference
// in case it was read from the provider
d.Set("datacenter", dc)

// Now we can just write in all the initial values, since we can expect
// that nothing should need deleting yet, as long as there isn't some
// other program racing us to write values... which we'll catch on a
// subsequent Read.
for k, v := range subKeys {
fullPath := pathPrefix + k
err := keyClient.Put(fullPath, v)
if err != nil {
return fmt.Errorf("error while writing %s: %s", fullPath, err)
}
}

return nil
}

func resourceConsulKeyPrefixUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
kv := client.KV()
token := d.Get("token").(string)
dc, err := getDC(d, client)
if err != nil {
return err
}

keyClient := newKeyClient(kv, dc, token)

pathPrefix := d.Id()

if d.HasChange("subkeys") {
o, n := d.GetChange("subkeys")
if o == nil {
o = map[string]interface{}{}
}
if n == nil {
n = map[string]interface{}{}
}

om := o.(map[string]interface{})
nm := n.(map[string]interface{})

// First we'll write all of the stuff in the "new map" nm,
// and then we'll delete any keys that appear in the "old map" om
// and do not also appear in nm. This ordering means that if a subkey
// name is changed we will briefly have both the old and new names in
// Consul, as opposed to briefly having neither.

// Again, we'd ideally use d.Partial(true) here but it doesn't work
// for maps and so we'll just rely on a subsequent Read to tidy up
// after a partial write.

// Write new and changed keys
for k, vI := range nm {
v := vI.(string)
fullPath := pathPrefix + k
err := keyClient.Put(fullPath, v)
if err != nil {
return fmt.Errorf("error while writing %s: %s", fullPath, err)
}
}

// Remove deleted keys
for k, _ := range om {
if _, exists := nm[k]; exists {
continue
}
fullPath := pathPrefix + k
err := keyClient.Delete(fullPath)
if err != nil {
return fmt.Errorf("error while deleting %s: %s", fullPath, 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 resourceConsulKeyPrefixRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
kv := client.KV()
token := d.Get("token").(string)
dc, err := getDC(d, client)
if err != nil {
return err
}

keyClient := newKeyClient(kv, dc, token)

pathPrefix := d.Id()

subKeys, err := keyClient.GetUnderPrefix(pathPrefix)
if err != nil {
return err
}

d.Set("subkeys", subKeys)

// 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 resourceConsulKeyPrefixDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
kv := client.KV()
token := d.Get("token").(string)
dc, err := getDC(d, client)
if err != nil {
return err
}

keyClient := newKeyClient(kv, dc, token)

pathPrefix := d.Id()

// Delete everything under our prefix, since the entire set of keys under
// the given prefix is considered to be managed exclusively by Terraform.
err = keyClient.DeleteUnderPrefix(pathPrefix)
if err != nil {
return err
}

d.SetId("")

return nil
}
Loading

0 comments on commit fe4ddba

Please sign in to comment.