diff --git a/builtin/providers/consul/config.go b/builtin/providers/consul/config.go index 7983018c6..cb6d7af79 100644 --- a/builtin/providers/consul/config.go +++ b/builtin/providers/consul/config.go @@ -9,6 +9,7 @@ import ( type Config struct { Datacenter string `mapstructure:"datacenter"` Address string `mapstructure:"address"` + Token string `mapstructure:"token"` Scheme string `mapstructure:"scheme"` } @@ -25,6 +26,9 @@ func (c *Config) Client() (*consulapi.Client, error) { if c.Scheme != "" { config.Scheme = c.Scheme } + if c.Token != "" { + config.Token = c.Token + } client, err := consulapi.NewClient(config) log.Printf("[INFO] Consul Client configured with address: '%s', scheme: '%s', datacenter: '%s'", diff --git a/builtin/providers/consul/get_dc.go b/builtin/providers/consul/get_dc.go new file mode 100644 index 000000000..0909276aa --- /dev/null +++ b/builtin/providers/consul/get_dc.go @@ -0,0 +1,17 @@ +package consul + +import ( + "fmt" + + consulapi "github.com/hashicorp/consul/api" +) + +// getDC is used to get the datacenter of the local agent +func getDC(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 +} diff --git a/builtin/providers/consul/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go new file mode 100644 index 000000000..1a15bf6d8 --- /dev/null +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -0,0 +1,135 @@ +package consul + +import ( + "fmt" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceConsulAgentService() *schema.Resource { + return &schema.Resource{ + Create: resourceConsulAgentServiceCreate, + Update: resourceConsulAgentServiceCreate, + Read: resourceConsulAgentServiceRead, + Delete: resourceConsulAgentServiceDelete, + + Schema: map[string]*schema.Schema{ + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + }, + }, + } +} + +func resourceConsulAgentServiceCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + agent := client.Agent() + + name := d.Get("name").(string) + registration := consulapi.AgentServiceRegistration{Name: name} + + if address, ok := d.GetOk("address"); ok { + registration.Address = address.(string) + } + + if id, ok := d.GetOk("id"); ok { + registration.ID = id.(string) + } + + if port, ok := d.GetOk("port"); ok { + registration.Port = port.(int) + } + + if v, ok := d.GetOk("tags"); ok { + vs := v.([]interface{}) + s := make([]string, len(vs)) + for i, raw := range vs { + s[i] = raw.(string) + } + registration.Tags = s + } + + if err := agent.ServiceRegister(®istration); err != nil { + return fmt.Errorf("Failed to register service '%s' with Consul agent: %v", name, err) + } + + // Update the resource + if serviceMap, err := agent.Services(); err != nil { + return fmt.Errorf("Failed to read services from Consul agent: %v", err) + } else if service, ok := serviceMap[name]; !ok { + return fmt.Errorf("Failed to read service '%s' from Consul agent: %v", name, err) + } else { + d.Set("address", service.Address) + d.Set("name", service.Service) + d.Set("port", service.Port) + d.Set("tags", service.Tags) + d.SetId(service.ID) + } + + return nil +} + +func resourceConsulAgentServiceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + agent := client.Agent() + + name := d.Get("name").(string) + + if services, err := agent.Services(); err != nil { + return fmt.Errorf("Failed to get services from Consul agent: %v", err) + } else if service, ok := services[name]; !ok { + return fmt.Errorf("Failed to get service '%s' from Consul agent", name) + } else { + d.Set("address", service.Address) + d.Set("name", service.Service) + d.Set("port", service.Port) + d.Set("tags", service.Tags) + d.SetId(service.ID) + } + + return nil +} + +func resourceConsulAgentServiceDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + catalog := client.Agent() + + name := d.Get("name").(string) + + if err := catalog.ServiceDeregister(name); err != nil { + return fmt.Errorf("Failed to deregister service '%s' from Consul agent: %v", name, err) + } + + // Clear the ID + d.SetId("") + return nil +} diff --git a/builtin/providers/consul/resource_consul_agent_service_test.go b/builtin/providers/consul/resource_consul_agent_service_test.go new file mode 100644 index 000000000..5d5b022d8 --- /dev/null +++ b/builtin/providers/consul/resource_consul_agent_service_test.go @@ -0,0 +1,85 @@ +package consul + +import ( + "fmt" + "testing" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccConsulAgentService_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() {}, + Providers: testAccProviders, + CheckDestroy: testAccCheckConsulAgentServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccConsulAgentServiceConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckConsulAgentServiceExists(), + testAccCheckConsulAgentServiceValue("consul_agent_service.app", "address", "www.google.com"), + testAccCheckConsulAgentServiceValue("consul_agent_service.app", "id", "google"), + testAccCheckConsulAgentServiceValue("consul_agent_service.app", "name", "google"), + ), + }, + }, + }) +} + +func testAccCheckConsulAgentServiceDestroy(s *terraform.State) error { + agent := testAccProvider.Meta().(*consulapi.Client).Agent() + services, err := agent.Services() + if err != nil { + return fmt.Errorf("Could not retrieve services: %#v", err) + } + _, ok := services["google"] + if ok { + return fmt.Errorf("Service still exists: %#v", "google") + } + return nil +} + +func testAccCheckConsulAgentServiceExists() resource.TestCheckFunc { + return func(s *terraform.State) error { + agent := testAccProvider.Meta().(*consulapi.Client).Agent() + services, err := agent.Services() + if err != nil { + return err + } + _, ok := services["google"] + if !ok { + return fmt.Errorf("Service does not exist: %#v", "google") + } + return nil + } +} + +func testAccCheckConsulAgentServiceValue(n, attr, val string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rn, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Resource not found") + } + out, ok := rn.Primary.Attributes[attr] + if !ok { + return fmt.Errorf("Attribute '%s' not found: %#v", attr, rn.Primary.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 testAccConsulAgentServiceConfig = ` +resource "consul_agent_service" "app" { + name = "google" + address = "www.google.com" + port = 80 +} +` diff --git a/builtin/providers/consul/resource_consul_catalog_entry.go b/builtin/providers/consul/resource_consul_catalog_entry.go new file mode 100644 index 000000000..73ed90307 --- /dev/null +++ b/builtin/providers/consul/resource_consul_catalog_entry.go @@ -0,0 +1,265 @@ +package consul + +import ( + "bytes" + "fmt" + "sort" + "strings" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceConsulCatalogEntry() *schema.Resource { + return &schema.Resource{ + Create: resourceConsulCatalogEntryCreate, + Update: resourceConsulCatalogEntryCreate, + Read: resourceConsulCatalogEntryRead, + Delete: resourceConsulCatalogEntryDelete, + + Schema: map[string]*schema.Schema{ + "address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "datacenter": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "node": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "service": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + Set: resourceConsulCatalogEntryServicesHash, + }, + + "token": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceConsulCatalogEntryServicesHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["id"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["address"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["port"].(int))) + if v, ok := m["tags"]; ok { + vs := v.([]interface{}) + s := make([]string, len(vs)) + for i, raw := range vs { + s[i] = raw.(string) + } + sort.Strings(s) + + for _, v := range s { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + } + return hashcode.String(buf.String()) +} + +func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + catalog := client.Catalog() + + var dc string + if v, ok := d.GetOk("datacenter"); ok { + dc = v.(string) + } else { + var err error + if dc, err = getDC(client); err != nil { + return err + } + } + + var token string + if v, ok := d.GetOk("token"); ok { + token = v.(string) + } + + // Setup the operations using the datacenter + wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token} + + address := d.Get("address").(string) + node := d.Get("node").(string) + + var serviceIDs []string + if service, ok := d.GetOk("service"); ok { + serviceList := service.(*schema.Set).List() + serviceIDs = make([]string, len(serviceList)) + for i, rawService := range serviceList { + serviceData := rawService.(map[string]interface{}) + + serviceID := serviceData["id"].(string) + serviceIDs[i] = serviceID + + var tags []string + if v := serviceData["tags"].([]interface{}); len(v) > 0 { + tags = make([]string, len(v)) + for i, raw := range v { + tags[i] = raw.(string) + } + } + + registration := &consulapi.CatalogRegistration{ + Address: address, + Datacenter: dc, + Node: node, + Service: &consulapi.AgentService{ + Address: serviceData["address"].(string), + ID: serviceID, + Service: serviceData["name"].(string), + Port: serviceData["port"].(int), + Tags: tags, + }, + } + + if _, err := catalog.Register(registration, &wOpts); err != nil { + return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' in %s: %v", + node, address, dc, err) + } + } + } else { + registration := &consulapi.CatalogRegistration{ + Address: address, + Datacenter: dc, + Node: node, + } + + if _, err := catalog.Register(registration, &wOpts); err != nil { + return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' in %s: %v", + node, address, dc, err) + } + } + + // Update the resource + qOpts := consulapi.QueryOptions{Datacenter: dc} + if _, _, err := catalog.Node(node, &qOpts); err != nil { + return fmt.Errorf("Failed to read Consul catalog entry for node '%s' at address '%s' in %s: %v", + node, address, dc, err) + } else { + d.Set("datacenter", dc) + } + + sort.Strings(serviceIDs) + serviceIDsJoined := strings.Join(serviceIDs, ",") + + d.SetId(fmt.Sprintf("%s-%s-[%s]", node, address, serviceIDsJoined)) + + return nil +} + +func resourceConsulCatalogEntryRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + catalog := client.Catalog() + + // Get the DC, error if not available. + var dc string + if v, ok := d.GetOk("datacenter"); ok { + dc = v.(string) + } + + node := d.Get("node").(string) + + // Setup the operations using the datacenter + qOpts := consulapi.QueryOptions{Datacenter: dc} + + if _, _, err := catalog.Node(node, &qOpts); err != nil { + return fmt.Errorf("Failed to get node '%s' from Consul catalog: %v", node, err) + } + + return nil +} + +func resourceConsulCatalogEntryDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + catalog := client.Catalog() + + var dc string + if v, ok := d.GetOk("datacenter"); ok { + dc = v.(string) + } else { + var err error + if dc, err = getDC(client); err != nil { + return err + } + } + + var token string + if v, ok := d.GetOk("token"); ok { + token = v.(string) + } + + // Setup the operations using the datacenter + wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token} + + address := d.Get("address").(string) + node := d.Get("node").(string) + + deregistration := consulapi.CatalogDeregistration{ + Address: address, + Datacenter: dc, + Node: node, + } + + if _, err := catalog.Deregister(&deregistration, &wOpts); err != nil { + return fmt.Errorf("Failed to deregister Consul catalog entry with node '%s' at address '%s' in %s: %v", + node, address, dc, err) + } + + // Clear the ID + d.SetId("") + return nil +} diff --git a/builtin/providers/consul/resource_consul_keys.go b/builtin/providers/consul/resource_consul_keys.go index 58000d7f7..7ad6d8d69 100644 --- a/builtin/providers/consul/resource_consul_keys.go +++ b/builtin/providers/consul/resource_consul_keys.go @@ -283,13 +283,3 @@ func attributeValue(sub map[string]interface{}, key string, pair *consulapi.KVPa // No value return "" } - -// getDC is used to get the datacenter of the local agent -func getDC(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 -} diff --git a/builtin/providers/consul/resource_provider.go b/builtin/providers/consul/resource_provider.go index ec5cd43a5..b109b9fe8 100644 --- a/builtin/providers/consul/resource_provider.go +++ b/builtin/providers/consul/resource_provider.go @@ -26,9 +26,16 @@ func Provider() terraform.ResourceProvider { Type: schema.TypeString, Optional: true, }, + + "token": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, }, ResourcesMap: map[string]*schema.Resource{ + "consul_agent_service": resourceConsulAgentService(), + "consul_catalog_entry": resourceConsulCatalogEntry(), "consul_keys": resourceConsulKeys(), },