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 }