From 63a045f4ff09b1d5097c84ecac325998795de734 Mon Sep 17 00:00:00 2001 From: Max Englander Date: Wed, 23 Sep 2015 03:43:48 -0400 Subject: [PATCH 01/12] #2087: add consul agent_service and catalog_entry resources; create resource works; TODO support more fields and add tests --- builtin/providers/consul/config.go | 4 + builtin/providers/consul/get_dc.go | 17 ++ .../consul/resource_consul_agent_service.go | 79 +++++++ .../consul/resource_consul_catalog_entry.go | 199 ++++++++++++++++++ .../providers/consul/resource_consul_keys.go | 10 - builtin/providers/consul/resource_provider.go | 7 + 6 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 builtin/providers/consul/get_dc.go create mode 100644 builtin/providers/consul/resource_consul_agent_service.go create mode 100644 builtin/providers/consul/resource_consul_catalog_entry.go diff --git a/builtin/providers/consul/config.go b/builtin/providers/consul/config.go index 7983018c6..8465bd9f6 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..6584770c0 --- /dev/null +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -0,0 +1,79 @@ +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, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: 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 err := agent.ServiceRegister(®istration); err != nil { + return fmt.Errorf("Failed to register service '%s' with Consul agent: %v", name, err) + } + + // Update the resource + d.SetId(fmt.Sprintf("consul-agent-service-%s", name)) + 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 _, ok := services[name]; !ok { + return fmt.Errorf("Failed to get service '%s' from Consul agent: %v", name, err) + } + } + + 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_catalog_entry.go b/builtin/providers/consul/resource_consul_catalog_entry.go new file mode 100644 index 000000000..86ab6874c --- /dev/null +++ b/builtin/providers/consul/resource_consul_catalog_entry.go @@ -0,0 +1,199 @@ +package consul + +import ( + "bytes" + "fmt" + "log" + + 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, + }, + + "datacenter": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "node": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "service": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource { + Schema: map[string]*schema.Schema{ + "service": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + 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["service"].(string))) + return hashcode.String(buf.String()) +} + +func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + catalog := client.Catalog() + + // 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) + } + + // Setup the operations using the datacenter + wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token} + + address := d.Get("address").(string) + node := d.Get("node").(string) + + if rawServiceDefinition, ok := d.GetOk("service"); ok { + rawServiceList := rawServiceDefinition.(*schema.Set).List() + for _, rawService := range rawServiceList { + service, ok := rawService.(map[string]interface{}) + + if !ok { + return fmt.Errorf("Failed to unroll: %#v", rawService) + } + + serviceName := service["service"].(string) + + registration := consulapi.CatalogRegistration{ + Node: node, Address: address, Datacenter: dc, + Service: &consulapi.AgentService{Service: serviceName}, + } + + if _, err := catalog.Register(®istration, &wOpts); err != nil { + return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' with service %s in %s: %v", + node, address, serviceName, dc, err) + } + } + } else { + registration := consulapi.CatalogRegistration{ + Node: node, Address: address, Datacenter: dc, + } + + if _, err := catalog.Register(®istration, &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 + d.SetId(fmt.Sprintf("consul-catalog-node-%s-%s", node, address)) + d.Set("datacenter", dc) + 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) + 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) + } + + node := d.Get("node").(string) + + // Setup the operations using the datacenter + qOpts := consulapi.QueryOptions{Datacenter: dc, Token: token} + + 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() + + // 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) + } + + // 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{ + Node: node, Address: address, Datacenter: dc, + } + + 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(), }, From 65ebbda7767ab33eee9d0caa156a6f5b4006615a Mon Sep 17 00:00:00 2001 From: Max Englander Date: Thu, 24 Sep 2015 01:30:06 -0400 Subject: [PATCH 02/12] #2087 agent_service resource supports address, port, and tag keys, can be read (refreshed) and deleted --- .../consul/resource_consul_agent_service.go | 66 +++++++++++++++++-- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/builtin/providers/consul/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go index 6584770c0..bbd83aab8 100644 --- a/builtin/providers/consul/resource_consul_agent_service.go +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -16,15 +16,32 @@ func resourceConsulAgentService() *schema.Resource { Schema: map[string]*schema.Schema{ "address": &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, Optional: true, Computed: true, }, "name": &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, Required: true, }, + + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, } } @@ -34,15 +51,46 @@ func resourceConsulAgentServiceCreate(d *schema.ResourceData, meta interface{}) 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 - d.SetId(fmt.Sprintf("consul-agent-service-%s", name)) + 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 } @@ -54,10 +102,14 @@ func resourceConsulAgentServiceRead(d *schema.ResourceData, meta interface{}) er 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: %v", name, err) } else { - if _, ok := services[name]; !ok { - return fmt.Errorf("Failed to get service '%s' from Consul agent: %v", name, err) - } + 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 From 929e3cdbe25ea453d00451f5f7d68de898573640 Mon Sep 17 00:00:00 2001 From: Max Englander Date: Mon, 28 Sep 2015 01:12:20 -0400 Subject: [PATCH 03/12] #2087: add consul_agent_service acceptance test --- .../consul/resource_consul_agent_service.go | 2 +- .../resource_consul_agent_service_test.go | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 builtin/providers/consul/resource_consul_agent_service_test.go diff --git a/builtin/providers/consul/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go index bbd83aab8..c4cf0f272 100644 --- a/builtin/providers/consul/resource_consul_agent_service.go +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -103,7 +103,7 @@ func resourceConsulAgentServiceRead(d *schema.ResourceData, meta interface{}) er 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: %v", name, err) + return fmt.Errorf("Failed to get service '%s' from Consul agent", name) } else { d.Set("address", service.Address) d.Set("name", service.Service) 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 +} +` From 7b9ec5925801239236d2d1c8ceccd79633b06690 Mon Sep 17 00:00:00 2001 From: Max Englander Date: Thu, 15 Oct 2015 04:06:52 -0400 Subject: [PATCH 04/12] #2087: consul_catalog_entry can set a service --- builtin/providers/consul/config.go | 2 +- .../consul/resource_consul_catalog_entry.go | 104 +++++++++++------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/builtin/providers/consul/config.go b/builtin/providers/consul/config.go index 8465bd9f6..cb6d7af79 100644 --- a/builtin/providers/consul/config.go +++ b/builtin/providers/consul/config.go @@ -27,7 +27,7 @@ func (c *Config) Client() (*consulapi.Client, error) { config.Scheme = c.Scheme } if c.Token != "" { - config.Token = c.Token + config.Token = c.Token } client, err := consulapi.NewClient(config) diff --git a/builtin/providers/consul/resource_consul_catalog_entry.go b/builtin/providers/consul/resource_consul_catalog_entry.go index 86ab6874c..09d9e62ff 100644 --- a/builtin/providers/consul/resource_consul_catalog_entry.go +++ b/builtin/providers/consul/resource_consul_catalog_entry.go @@ -3,7 +3,6 @@ package consul import ( "bytes" "fmt" - "log" consulapi "github.com/hashicorp/consul/api" "github.com/hashicorp/terraform/helper/hashcode" @@ -19,38 +18,60 @@ func resourceConsulCatalogEntry() *schema.Resource { Schema: map[string]*schema.Schema{ "address": &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, Required: true, }, "datacenter": &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, }, "node": &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, Required: true, }, "service": &schema.Schema{ - Type: schema.TypeSet, + Type: schema.TypeSet, Optional: true, - Elem: &schema.Resource { + Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "service": &schema.Schema{ - Type: schema.TypeString, + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, Required: true, }, + + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, }, Set: resourceConsulCatalogEntryServicesHash, }, "token": &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, Optional: true, }, }, @@ -60,7 +81,7 @@ func resourceConsulCatalogEntry() *schema.Resource { func resourceConsulCatalogEntryServicesHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["service"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["id"].(string))) return hashcode.String(buf.String()) } @@ -68,16 +89,12 @@ func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) client := meta.(*consulapi.Client) catalog := client.Catalog() - // 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 { + if dc, err = getDC(client); err != nil { return err } } @@ -93,30 +110,39 @@ func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) address := d.Get("address").(string) node := d.Get("node").(string) - if rawServiceDefinition, ok := d.GetOk("service"); ok { - rawServiceList := rawServiceDefinition.(*schema.Set).List() - for _, rawService := range rawServiceList { - service, ok := rawService.(map[string]interface{}) + if services, ok := d.GetOk("service"); ok { + for _, rawService := range services.(*schema.Set).List() { + serviceData := rawService.(map[string]interface{}) - if !ok { - return fmt.Errorf("Failed to unroll: %#v", rawService) + rawTags := serviceData["tags"].([]interface{}) + tags := make([]string, len(rawTags)) + for i, v := range rawTags { + tags[i] = v.(string) } - serviceName := service["service"].(string) - registration := consulapi.CatalogRegistration{ - Node: node, Address: address, Datacenter: dc, - Service: &consulapi.AgentService{Service: serviceName}, + Address: address, + Datacenter: dc, + Node: node, + Service: &consulapi.AgentService{ + Address: serviceData["address"].(string), + ID: serviceData["id"].(string), + Service: serviceData["name"].(string), + Port: serviceData["port"].(int), + Tags: tags, + }, } if _, err := catalog.Register(®istration, &wOpts); err != nil { - return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' with service %s in %s: %v", - node, address, serviceName, dc, err) + 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{ - Node: node, Address: address, Datacenter: dc, + Address: address, + Datacenter: dc, + Node: node, } if _, err := catalog.Register(®istration, &wOpts); err != nil { @@ -126,8 +152,14 @@ func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) } // 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) + } d.SetId(fmt.Sprintf("consul-catalog-node-%s-%s", node, address)) - d.Set("datacenter", dc) return nil } @@ -139,19 +171,12 @@ func resourceConsulCatalogEntryRead(d *schema.ResourceData, meta interface{}) er 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) } node := d.Get("node").(string) // Setup the operations using the datacenter - qOpts := consulapi.QueryOptions{Datacenter: dc, Token: token} + 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) @@ -168,10 +193,8 @@ func resourceConsulCatalogEntryDelete(d *schema.ResourceData, meta interface{}) 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) @@ -196,4 +219,3 @@ func resourceConsulCatalogEntryDelete(d *schema.ResourceData, meta interface{}) d.SetId("") return nil } - From 8d6b71e2ae4626ead4e5e7d3e9bf72262c89b583 Mon Sep 17 00:00:00 2001 From: Max Englander Date: Thu, 29 Oct 2015 05:04:25 -0400 Subject: [PATCH 05/12] use ForceNew on just about all catalog entry attributes; struggle in the hell of 'diffs mismatch' --- .../consul/resource_consul_catalog_entry.go | 72 +++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/builtin/providers/consul/resource_consul_catalog_entry.go b/builtin/providers/consul/resource_consul_catalog_entry.go index 09d9e62ff..73ed90307 100644 --- a/builtin/providers/consul/resource_consul_catalog_entry.go +++ b/builtin/providers/consul/resource_consul_catalog_entry.go @@ -3,6 +3,8 @@ package consul import ( "bytes" "fmt" + "sort" + "strings" consulapi "github.com/hashicorp/consul/api" "github.com/hashicorp/terraform/helper/hashcode" @@ -20,6 +22,7 @@ func resourceConsulCatalogEntry() *schema.Resource { "address": &schema.Schema{ Type: schema.TypeString, Required: true, + ForceNew: true, }, "datacenter": &schema.Schema{ @@ -32,37 +35,44 @@ func resourceConsulCatalogEntry() *schema.Resource { "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}, }, }, @@ -82,6 +92,21 @@ 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()) } @@ -110,42 +135,50 @@ func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) address := d.Get("address").(string) node := d.Get("node").(string) - if services, ok := d.GetOk("service"); ok { - for _, rawService := range services.(*schema.Set).List() { + 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{}) - rawTags := serviceData["tags"].([]interface{}) - tags := make([]string, len(rawTags)) - for i, v := range rawTags { - tags[i] = v.(string) + 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{ + registration := &consulapi.CatalogRegistration{ Address: address, Datacenter: dc, Node: node, Service: &consulapi.AgentService{ Address: serviceData["address"].(string), - ID: serviceData["id"].(string), + ID: serviceID, Service: serviceData["name"].(string), Port: serviceData["port"].(int), Tags: tags, }, } - if _, err := catalog.Register(®istration, &wOpts); err != nil { + 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{ + registration := &consulapi.CatalogRegistration{ Address: address, Datacenter: dc, Node: node, } - if _, err := catalog.Register(®istration, &wOpts); err != nil { + 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) } @@ -159,7 +192,12 @@ func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) } else { d.Set("datacenter", dc) } - d.SetId(fmt.Sprintf("consul-catalog-node-%s-%s", node, address)) + + sort.Strings(serviceIDs) + serviceIDsJoined := strings.Join(serviceIDs, ",") + + d.SetId(fmt.Sprintf("%s-%s-[%s]", node, address, serviceIDsJoined)) + return nil } @@ -189,10 +227,14 @@ func resourceConsulCatalogEntryDelete(d *schema.ResourceData, meta interface{}) 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) + } else { + var err error + if dc, err = getDC(client); err != nil { + return err + } } var token string @@ -207,7 +249,9 @@ func resourceConsulCatalogEntryDelete(d *schema.ResourceData, meta interface{}) node := d.Get("node").(string) deregistration := consulapi.CatalogDeregistration{ - Node: node, Address: address, Datacenter: dc, + Address: address, + Datacenter: dc, + Node: node, } if _, err := catalog.Deregister(&deregistration, &wOpts); err != nil { From 5f5fd7c6e61ef8195cd363b350be0305a0a2c9ff Mon Sep 17 00:00:00 2001 From: Max Englander Date: Thu, 29 Oct 2015 11:39:27 -0400 Subject: [PATCH 06/12] make good use of ForceNew in consul_agent_service --- builtin/providers/consul/resource_consul_agent_service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/builtin/providers/consul/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go index c4cf0f272..1a15bf6d8 100644 --- a/builtin/providers/consul/resource_consul_agent_service.go +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -19,12 +19,14 @@ func resourceConsulAgentService() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, + ForceNew: true, }, "id": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, + ForceNew: true, }, "name": &schema.Schema{ @@ -35,12 +37,14 @@ func resourceConsulAgentService() *schema.Resource { "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, }, }, } From adda285caecb524e12f6a4c0842591b34f0d5a0a Mon Sep 17 00:00:00 2001 From: Max Englander Date: Mon, 25 Jan 2016 00:15:09 +0000 Subject: [PATCH 07/12] finish tests and add docs for consul_{agent_service,catalog_entry} --- .../consul/resource_consul_agent_service.go | 30 +++--- .../resource_consul_agent_service_test.go | 7 +- .../consul/resource_consul_catalog_entry.go | 11 +- .../resource_consul_catalog_entry_test.go | 100 ++++++++++++++++++ builtin/providers/consul/resource_provider.go | 2 +- .../consul/r/agent_service.html.markdown | 47 ++++++++ .../consul/r/catalog_entry.html.markdown | 58 ++++++++++ 7 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 builtin/providers/consul/resource_consul_catalog_entry_test.go create mode 100644 website/source/docs/providers/consul/r/agent_service.html.markdown create mode 100644 website/source/docs/providers/consul/r/catalog_entry.html.markdown diff --git a/builtin/providers/consul/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go index 1a15bf6d8..9ede63bf3 100644 --- a/builtin/providers/consul/resource_consul_agent_service.go +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -24,9 +24,7 @@ func resourceConsulAgentService() *schema.Resource { "id": &schema.Schema{ Type: schema.TypeString, - Optional: true, Computed: true, - ForceNew: true, }, "name": &schema.Schema{ @@ -61,10 +59,6 @@ func resourceConsulAgentServiceCreate(d *schema.ResourceData, meta interface{}) 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) } @@ -89,10 +83,15 @@ func resourceConsulAgentServiceCreate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Failed to read service '%s' from Consul agent: %v", name, err) } else { d.Set("address", service.Address) + d.Set("id", service.ID) + d.SetId(service.ID) d.Set("name", service.Service) d.Set("port", service.Port) - d.Set("tags", service.Tags) - d.SetId(service.ID) + tags := make([]string, 0, len(service.Tags)) + for _, tag := range service.Tags { + tags = append(tags, tag) + } + d.Set("tags", tags) } return nil @@ -110,10 +109,15 @@ func resourceConsulAgentServiceRead(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("Failed to get service '%s' from Consul agent", name) } else { d.Set("address", service.Address) + d.Set("id", service.ID) + d.SetId(service.ID) d.Set("name", service.Service) d.Set("port", service.Port) - d.Set("tags", service.Tags) - d.SetId(service.ID) + tags := make([]string, 0, len(service.Tags)) + for _, tag := range service.Tags { + tags = append(tags, tag) + } + d.Set("tags", tags) } return nil @@ -123,10 +127,10 @@ func resourceConsulAgentServiceDelete(d *schema.ResourceData, meta interface{}) client := meta.(*consulapi.Client) catalog := client.Agent() - name := d.Get("name").(string) + id := d.Get("id").(string) - if err := catalog.ServiceDeregister(name); err != nil { - return fmt.Errorf("Failed to deregister service '%s' from Consul agent: %v", name, err) + if err := catalog.ServiceDeregister(id); err != nil { + return fmt.Errorf("Failed to deregister service '%s' from Consul agent: %v", id, err) } // Clear the ID diff --git a/builtin/providers/consul/resource_consul_agent_service_test.go b/builtin/providers/consul/resource_consul_agent_service_test.go index 5d5b022d8..5150c4e85 100644 --- a/builtin/providers/consul/resource_consul_agent_service_test.go +++ b/builtin/providers/consul/resource_consul_agent_service_test.go @@ -22,6 +22,10 @@ func TestAccConsulAgentService_basic(t *testing.T) { testAccCheckConsulAgentServiceValue("consul_agent_service.app", "address", "www.google.com"), testAccCheckConsulAgentServiceValue("consul_agent_service.app", "id", "google"), testAccCheckConsulAgentServiceValue("consul_agent_service.app", "name", "google"), + testAccCheckConsulAgentServiceValue("consul_agent_service.app", "port", "80"), + testAccCheckConsulAgentServiceValue("consul_agent_service.app", "tags.#", "2"), + testAccCheckConsulAgentServiceValue("consul_agent_service.app", "tags.0", "tag0"), + testAccCheckConsulAgentServiceValue("consul_agent_service.app", "tags.1", "tag1"), ), }, }, @@ -78,8 +82,9 @@ func testAccCheckConsulAgentServiceValue(n, attr, val string) resource.TestCheck const testAccConsulAgentServiceConfig = ` resource "consul_agent_service" "app" { - name = "google" address = "www.google.com" + name = "google" port = 80 + tags = ["tag0", "tag1"] } ` diff --git a/builtin/providers/consul/resource_consul_catalog_entry.go b/builtin/providers/consul/resource_consul_catalog_entry.go index 73ed90307..263c21f71 100644 --- a/builtin/providers/consul/resource_consul_catalog_entry.go +++ b/builtin/providers/consul/resource_consul_catalog_entry.go @@ -70,10 +70,11 @@ func resourceConsulCatalogEntry() *schema.Resource { }, "tags": &schema.Schema{ - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, + Set: resourceConsulCatalogEntryServiceTagsHash, }, }, }, @@ -88,6 +89,10 @@ func resourceConsulCatalogEntry() *schema.Resource { } } +func resourceConsulCatalogEntryServiceTagsHash(v interface{}) int { + return hashcode.String(v.(string)) +} + func resourceConsulCatalogEntryServicesHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) @@ -96,7 +101,7 @@ func resourceConsulCatalogEntryServicesHash(v interface{}) int { 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{}) + vs := v.(*schema.Set).List() s := make([]string, len(vs)) for i, raw := range vs { s[i] = raw.(string) @@ -146,7 +151,7 @@ func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) serviceIDs[i] = serviceID var tags []string - if v := serviceData["tags"].([]interface{}); len(v) > 0 { + if v := serviceData["tags"].(*schema.Set).List(); len(v) > 0 { tags = make([]string, len(v)) for i, raw := range v { tags[i] = raw.(string) diff --git a/builtin/providers/consul/resource_consul_catalog_entry_test.go b/builtin/providers/consul/resource_consul_catalog_entry_test.go new file mode 100644 index 000000000..0a28b675c --- /dev/null +++ b/builtin/providers/consul/resource_consul_catalog_entry_test.go @@ -0,0 +1,100 @@ +package consul + +import ( + "fmt" + "testing" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccConsulCatalogEntry_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() {}, + Providers: testAccProviders, + CheckDestroy: testAccCheckConsulCatalogEntryDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccConsulCatalogEntryConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckConsulCatalogEntryExists(), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "address", "127.0.0.1"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "node", "bastion"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.#", "1"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.address", "www.google.com"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.id", "google1"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.name", "google"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.port", "80"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.tags.#", "2"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.tags.2154398732", "tag0"), + testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.tags.4151227546", "tag1"), + ), + }, + }, + }) +} + +func testAccCheckConsulCatalogEntryDestroy(s *terraform.State) error { + catalog := testAccProvider.Meta().(*consulapi.Client).Catalog() + qOpts := consulapi.QueryOptions{} + services, _, err := catalog.Services(&qOpts) + 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 testAccCheckConsulCatalogEntryExists() resource.TestCheckFunc { + return func(s *terraform.State) error { + catalog := testAccProvider.Meta().(*consulapi.Client).Catalog() + qOpts := consulapi.QueryOptions{} + services, _, err := catalog.Services(&qOpts) + if err != nil { + return err + } + _, ok := services["google"] + if !ok { + return fmt.Errorf("Service does not exist: %#v", "google") + } + return nil + } +} + +func testAccCheckConsulCatalogEntryValue(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 testAccConsulCatalogEntryConfig = ` +resource "consul_catalog_entry" "app" { + address = "127.0.0.1" + node = "bastion" + service = { + address = "www.google.com" + id = "google1" + name = "google" + port = 80 + tags = ["tag0", "tag1"] + } +} +` diff --git a/builtin/providers/consul/resource_provider.go b/builtin/providers/consul/resource_provider.go index b109b9fe8..08a153046 100644 --- a/builtin/providers/consul/resource_provider.go +++ b/builtin/providers/consul/resource_provider.go @@ -36,7 +36,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "consul_agent_service": resourceConsulAgentService(), "consul_catalog_entry": resourceConsulCatalogEntry(), - "consul_keys": resourceConsulKeys(), + "consul_keys": resourceConsulKeys(), }, ConfigureFunc: providerConfigure, diff --git a/website/source/docs/providers/consul/r/agent_service.html.markdown b/website/source/docs/providers/consul/r/agent_service.html.markdown new file mode 100644 index 000000000..edf7524dd --- /dev/null +++ b/website/source/docs/providers/consul/r/agent_service.html.markdown @@ -0,0 +1,47 @@ +--- +layout: "consul" +page_title: "Consul: consul_agent_service" +sidebar_current: "docs-consul-resource-agent-service" +description: |- + Provides access to Agent Service data in Consul. This can be used to define a service associated with a particular agent. Currently, defining health checks for an agent service is not supported. +--- + +# consul\_agent\_service + +Provides access to Agent Service data in Consul. This can be used to define a service associated with a particular agent. Currently, defining health checks for an agent service is not supported. + +## Example Usage + +``` +resource "consul_agent_service" "app" { + address = "www.google.com" + name = "google" + port = 80 + tags = ["tag0", "tag1"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `address` - (Optional) The address of the service. Defaults to the + address of the agent. + +* `name` - (Required) The name of the service. + +* `port` - (Optional) The port of the service. + +* `tags` - (Optional) A list of values that are opaque to Consul, + but can be used to distinguish between services or nodes. + + +## Attributes Reference + +The following attributes are exported: + +* `address` - The address of the service. +* `id` - The id of the service, defaults to the value of `name`. +* `name` - The name of the service. +* `port` - The port of the service. +* `tags` - The tags of the service. diff --git a/website/source/docs/providers/consul/r/catalog_entry.html.markdown b/website/source/docs/providers/consul/r/catalog_entry.html.markdown new file mode 100644 index 000000000..d750642c5 --- /dev/null +++ b/website/source/docs/providers/consul/r/catalog_entry.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "consul" +page_title: "Consul: consul_catalog_entry" +sidebar_current: "docs-consul-resource-catalog-entry" +description: |- + Provides access to Catalog data in Consul. This can be used to define a node or a service. Currently, defining health checks is not supported. +--- + +# consul\_catalog\_entry_ + +Provides access to Catalog data in Consul. This can be used to define a node or a service. Currently, defining health checks is not supported. + +## Example Usage + +``` +resource "consul_catalog_entry" "app" { + address = "192.168.10.10" + name = "foobar" + service = { + address = "127.0.0.1" + id = "redis1" + name = "redis" + port = 8000 + tags = ["master", "v1"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `address` - (Required) The address of the node being added to + or referenced in the catalog. + +* `node` - (Required) The name of the node being added to or + referenced in the catalog. + +* `service` - (Optional) A service to optionally associated with + the node. Supported values documented below. + +The `service` block supports the following: + +* `address` - (Optional) The address of the service. Defaults to the + node address. +* `id` - (Optional) The ID of the service. Defaults to the `name`. +* `name` - (Required) The name of the service +* `port` - (Optional) The port of the service. +* `tags` - (Optional) A list of values that are opaque to Consul, + but can be used to distinguish between services or nodes. + + +## Attributes Reference + +The following attributes are exported: + +* `address` - The address of the service. +* `node` - The id of the service, defaults to the value of `name`. From 8c8b58400ceb37cace0aef553bf6ab0d0bde77fa Mon Sep 17 00:00:00 2001 From: Max Englander Date: Mon, 25 Jan 2016 00:18:13 +0000 Subject: [PATCH 08/12] consul_catalog_entry docs: remove errant underscore --- .../source/docs/providers/consul/r/catalog_entry.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/providers/consul/r/catalog_entry.html.markdown b/website/source/docs/providers/consul/r/catalog_entry.html.markdown index d750642c5..354301a33 100644 --- a/website/source/docs/providers/consul/r/catalog_entry.html.markdown +++ b/website/source/docs/providers/consul/r/catalog_entry.html.markdown @@ -6,7 +6,7 @@ description: |- Provides access to Catalog data in Consul. This can be used to define a node or a service. Currently, defining health checks is not supported. --- -# consul\_catalog\_entry_ +# consul\_catalog\_entry Provides access to Catalog data in Consul. This can be used to define a node or a service. Currently, defining health checks is not supported. From 296f8be10a4c50bcc2d5926e46748bc3b4167c0e Mon Sep 17 00:00:00 2001 From: Max Englander Date: Sun, 27 Mar 2016 03:58:12 +0000 Subject: [PATCH 09/12] #2087 add consul_node and consul_service resources --- .../providers/consul/resource_consul_node.go | 156 ++++++++++++++++++ .../consul/resource_consul_node_test.go | 87 ++++++++++ .../consul/resource_consul_service.go | 139 ++++++++++++++++ .../consul/resource_consul_service_test.go | 90 ++++++++++ builtin/providers/consul/resource_provider.go | 2 + 5 files changed, 474 insertions(+) create mode 100644 builtin/providers/consul/resource_consul_node.go create mode 100644 builtin/providers/consul/resource_consul_node_test.go create mode 100644 builtin/providers/consul/resource_consul_service.go create mode 100644 builtin/providers/consul/resource_consul_service_test.go diff --git a/builtin/providers/consul/resource_consul_node.go b/builtin/providers/consul/resource_consul_node.go new file mode 100644 index 000000000..c81544ccb --- /dev/null +++ b/builtin/providers/consul/resource_consul_node.go @@ -0,0 +1,156 @@ +package consul + +import ( + "fmt" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceConsulNode() *schema.Resource { + return &schema.Resource{ + Create: resourceConsulNodeCreate, + Update: resourceConsulNodeCreate, + Read: resourceConsulNodeRead, + Delete: resourceConsulNodeDelete, + + 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, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "token": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceConsulNodeCreate(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(d, 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) + name := d.Get("name").(string) + + registration := &consulapi.CatalogRegistration{ + Address: address, + Datacenter: dc, + Node: name, + } + + if _, err := catalog.Register(registration, &wOpts); err != nil { + return fmt.Errorf("Failed to register Consul catalog node with name '%s' at address '%s' in %s: %v", + name, address, dc, err) + } + + // Update the resource + qOpts := consulapi.QueryOptions{Datacenter: dc} + if _, _, err := catalog.Node(name, &qOpts); err != nil { + return fmt.Errorf("Failed to read Consul catalog node with name '%s' at address '%s' in %s: %v", + name, address, dc, err) + } else { + d.Set("datacenter", dc) + } + + d.SetId(fmt.Sprintf("%s-%s", name, address)) + + return nil +} + +func resourceConsulNodeRead(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) + } + + name := d.Get("name").(string) + + // Setup the operations using the datacenter + qOpts := consulapi.QueryOptions{Datacenter: dc} + + if _, _, err := catalog.Node(name, &qOpts); err != nil { + return fmt.Errorf("Failed to get name '%s' from Consul catalog: %v", name, err) + } + + return nil +} + +func resourceConsulNodeDelete(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(d, 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) + name := d.Get("name").(string) + + deregistration := consulapi.CatalogDeregistration{ + Address: address, + Datacenter: dc, + Node: name, + } + + if _, err := catalog.Deregister(&deregistration, &wOpts); err != nil { + return fmt.Errorf("Failed to deregister Consul catalog node with name '%s' at address '%s' in %s: %v", + name, address, dc, err) + } + + // Clear the ID + d.SetId("") + return nil +} diff --git a/builtin/providers/consul/resource_consul_node_test.go b/builtin/providers/consul/resource_consul_node_test.go new file mode 100644 index 000000000..a24991d45 --- /dev/null +++ b/builtin/providers/consul/resource_consul_node_test.go @@ -0,0 +1,87 @@ +package consul + +import ( + "fmt" + "testing" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccConsulNode_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() {}, + Providers: testAccProviders, + CheckDestroy: testAccCheckConsulNodeDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccConsulNodeConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckConsulNodeExists(), + testAccCheckConsulNodeValue("consul_catalog_entry.app", "address", "127.0.0.1"), + testAccCheckConsulNodeValue("consul_catalog_entry.app", "name", "foo"), + ), + }, + }, + }) +} + +func testAccCheckConsulNodeDestroy(s *terraform.State) error { + catalog := testAccProvider.Meta().(*consulapi.Client).Catalog() + qOpts := consulapi.QueryOptions{} + nodes, _, err := catalog.Nodes(&qOpts) + if err != nil { + return fmt.Errorf("Could not retrieve services: %#v", err) + } + for i := range nodes { + if nodes[i].Node == "foo" { + return fmt.Errorf("Node still exists: %#v", "foo") + } + } + return nil +} + +func testAccCheckConsulNodeExists() resource.TestCheckFunc { + return func(s *terraform.State) error { + catalog := testAccProvider.Meta().(*consulapi.Client).Catalog() + qOpts := consulapi.QueryOptions{} + nodes, _, err := catalog.Nodes(&qOpts) + if err != nil { + return err + } + for i := range nodes { + if nodes[i].Node == "foo" { + return nil + } + } + return fmt.Errorf("Service does not exist: %#v", "google") + } +} + +func testAccCheckConsulNodeValue(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 testAccConsulNodeConfig = ` +resource "consul_catalog_entry" "foo" { + address = "127.0.0.1" + name = "foo" +} +` diff --git a/builtin/providers/consul/resource_consul_service.go b/builtin/providers/consul/resource_consul_service.go new file mode 100644 index 000000000..57f95a856 --- /dev/null +++ b/builtin/providers/consul/resource_consul_service.go @@ -0,0 +1,139 @@ +package consul + +import ( + "fmt" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceConsulService() *schema.Resource { + return &schema.Resource{ + Create: resourceConsulServiceCreate, + Update: resourceConsulServiceCreate, + Read: resourceConsulServiceRead, + Delete: resourceConsulServiceDelete, + + Schema: map[string]*schema.Schema{ + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: 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 resourceConsulServiceCreate(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 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("id", service.ID) + d.SetId(service.ID) + d.Set("name", service.Service) + d.Set("port", service.Port) + tags := make([]string, 0, len(service.Tags)) + for _, tag := range service.Tags { + tags = append(tags, tag) + } + d.Set("tags", tags) + } + + return nil +} + +func resourceConsulServiceRead(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("id", service.ID) + d.SetId(service.ID) + d.Set("name", service.Service) + d.Set("port", service.Port) + tags := make([]string, 0, len(service.Tags)) + for _, tag := range service.Tags { + tags = append(tags, tag) + } + d.Set("tags", tags) + } + + return nil +} + +func resourceConsulServiceDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*consulapi.Client) + catalog := client.Agent() + + id := d.Get("id").(string) + + if err := catalog.ServiceDeregister(id); err != nil { + return fmt.Errorf("Failed to deregister service '%s' from Consul agent: %v", id, err) + } + + // Clear the ID + d.SetId("") + return nil +} diff --git a/builtin/providers/consul/resource_consul_service_test.go b/builtin/providers/consul/resource_consul_service_test.go new file mode 100644 index 000000000..f4df71542 --- /dev/null +++ b/builtin/providers/consul/resource_consul_service_test.go @@ -0,0 +1,90 @@ +package consul + +import ( + "fmt" + "testing" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccConsulService_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() {}, + Providers: testAccProviders, + CheckDestroy: testAccCheckConsulServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccConsulServiceConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckConsulServiceExists(), + testAccCheckConsulServiceValue("consul_service.app", "address", "www.google.com"), + testAccCheckConsulServiceValue("consul_service.app", "id", "google"), + testAccCheckConsulServiceValue("consul_service.app", "name", "google"), + testAccCheckConsulServiceValue("consul_service.app", "port", "80"), + testAccCheckConsulServiceValue("consul_service.app", "tags.#", "2"), + testAccCheckConsulServiceValue("consul_service.app", "tags.0", "tag0"), + testAccCheckConsulServiceValue("consul_service.app", "tags.1", "tag1"), + ), + }, + }, + }) +} + +func testAccCheckConsulServiceDestroy(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 testAccCheckConsulServiceExists() 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 testAccCheckConsulServiceValue(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 testAccConsulServiceConfig = ` +resource "consul_service" "app" { + address = "www.google.com" + name = "google" + port = 80 + tags = ["tag0", "tag1"] +} +` diff --git a/builtin/providers/consul/resource_provider.go b/builtin/providers/consul/resource_provider.go index 08a153046..b1d090f11 100644 --- a/builtin/providers/consul/resource_provider.go +++ b/builtin/providers/consul/resource_provider.go @@ -37,6 +37,8 @@ func Provider() terraform.ResourceProvider { "consul_agent_service": resourceConsulAgentService(), "consul_catalog_entry": resourceConsulCatalogEntry(), "consul_keys": resourceConsulKeys(), + "consul_node": resourceConsulNode(), + "consul_service": resourceConsulService(), }, ConfigureFunc: providerConfigure, From 4fe7db9441f5d9638667587af57124842f4906ca Mon Sep 17 00:00:00 2001 From: Max Englander Date: Sun, 27 Mar 2016 04:12:38 +0000 Subject: [PATCH 10/12] #2087 add docs for consul_node and consul_service --- .../providers/consul/r/node.html.markdown | 37 +++++++++++++++ .../providers/consul/r/service.html.markdown | 47 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 website/source/docs/providers/consul/r/node.html.markdown create mode 100644 website/source/docs/providers/consul/r/service.html.markdown diff --git a/website/source/docs/providers/consul/r/node.html.markdown b/website/source/docs/providers/consul/r/node.html.markdown new file mode 100644 index 000000000..d8cc322bb --- /dev/null +++ b/website/source/docs/providers/consul/r/node.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "consul" +page_title: "Consul: consul_node" +sidebar_current: "docs-consul-resource-node" +description: |- + Provides access to Node data in Consul. This can be used to define a node. +--- + +# consul\_node + +Provides access to Node data in Consul. This can be used to define a node. Currently, defining health checks is not supported. + +## Example Usage + +``` +resource "consul_node" "foobar" { + address = "192.168.10.10" + name = "foobar" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `address` - (Required) The address of the node being added to + or referenced in the catalog. + +* `name` - (Required) The name of the node being added to or + referenced in the catalog. + +## Attributes Reference + +The following attributes are exported: + +* `address` - The address of the service. +* `name` - The name of the service. diff --git a/website/source/docs/providers/consul/r/service.html.markdown b/website/source/docs/providers/consul/r/service.html.markdown new file mode 100644 index 000000000..a91370c2c --- /dev/null +++ b/website/source/docs/providers/consul/r/service.html.markdown @@ -0,0 +1,47 @@ +--- +layout: "consul" +page_title: "Consul: consul_service" +sidebar_current: "docs-consul-resource-service" +description: |- + A high-level resource for creating a Service in Consul. Since Consul requires clients to register services with either the catalog or an agent, `consul_service` may register with either the catalog or an agent, depending on the configuration of `consul_service`. For now, `consul_service` always registers services with the agent running at the address defined in the `consul` resource. Health checks are not currently supported. +--- + +# consul\_service + +A high-level resource for creating a Service in Consul. Currently, defining health checks for a service is not supported. + +## Example Usage + +``` +resource "consul_service" "google" { + address = "www.google.com" + name = "google" + port = 80 + tags = ["tag0", "tag1"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `address` - (Optional) The address of the service. Defaults to the + address of the agent. + +* `name` - (Required) The name of the service. + +* `port` - (Optional) The port of the service. + +* `tags` - (Optional) A list of values that are opaque to Consul, + but can be used to distinguish between services or nodes. + + +## Attributes Reference + +The following attributes are exported: + +* `address` - The address of the service. +* `id` - The id of the service, defaults to the value of `name`. +* `name` - The name of the service. +* `port` - The port of the service. +* `tags` - The tags of the service. From 6e402102272d64b428fa7cd61f61aafa93f665e8 Mon Sep 17 00:00:00 2001 From: Max Englander Date: Wed, 6 Jul 2016 08:29:47 -0400 Subject: [PATCH 11/12] #2087 gracefully handle non-presence of service on remote consul agent --- builtin/providers/consul/resource_consul_agent_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/consul/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go index 9ede63bf3..6636060a8 100644 --- a/builtin/providers/consul/resource_consul_agent_service.go +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -106,7 +106,7 @@ func resourceConsulAgentServiceRead(d *schema.ResourceData, meta interface{}) er 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) + d.Set("id", "") } else { d.Set("address", service.Address) d.Set("id", service.ID) From 47c274f699f459c8d220572d181efec95e5950ab Mon Sep 17 00:00:00 2001 From: stack72 Date: Tue, 26 Jul 2016 18:31:38 +0100 Subject: [PATCH 12/12] provider/consul: Fixing the ConsulNode_basic tests to have the correct references --- builtin/providers/consul/resource_consul_node_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/consul/resource_consul_node_test.go b/builtin/providers/consul/resource_consul_node_test.go index a24991d45..9cb62a4f6 100644 --- a/builtin/providers/consul/resource_consul_node_test.go +++ b/builtin/providers/consul/resource_consul_node_test.go @@ -19,8 +19,8 @@ func TestAccConsulNode_basic(t *testing.T) { Config: testAccConsulNodeConfig, Check: resource.ComposeTestCheckFunc( testAccCheckConsulNodeExists(), - testAccCheckConsulNodeValue("consul_catalog_entry.app", "address", "127.0.0.1"), - testAccCheckConsulNodeValue("consul_catalog_entry.app", "name", "foo"), + testAccCheckConsulNodeValue("consul_catalog_entry.foo", "address", "127.0.0.1"), + testAccCheckConsulNodeValue("consul_catalog_entry.foo", "node", "foo"), ), }, }, @@ -82,6 +82,6 @@ func testAccCheckConsulNodeValue(n, attr, val string) resource.TestCheckFunc { const testAccConsulNodeConfig = ` resource "consul_catalog_entry" "foo" { address = "127.0.0.1" - name = "foo" + node = "foo" } `