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/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go new file mode 100644 index 000000000..6636060a8 --- /dev/null +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -0,0 +1,139 @@ +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, + 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 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 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 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 { + d.Set("id", "") + } 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 resourceConsulAgentServiceDelete(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_agent_service_test.go b/builtin/providers/consul/resource_consul_agent_service_test.go new file mode 100644 index 000000000..5150c4e85 --- /dev/null +++ b/builtin/providers/consul/resource_consul_agent_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 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"), + 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"), + ), + }, + }, + }) +} + +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" { + 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 new file mode 100644 index 000000000..cf7d9acd9 --- /dev/null +++ b/builtin/providers/consul/resource_consul_catalog_entry.go @@ -0,0 +1,270 @@ +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.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: resourceConsulCatalogEntryServiceTagsHash, + }, + }, + }, + Set: resourceConsulCatalogEntryServicesHash, + }, + + "token": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceConsulCatalogEntryServiceTagsHash(v interface{}) int { + return hashcode.String(v.(string)) +} + +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.(*schema.Set).List() + 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(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) + 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"].(*schema.Set).List(); 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(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) + 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_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_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..9cb62a4f6 --- /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.foo", "address", "127.0.0.1"), + testAccCheckConsulNodeValue("consul_catalog_entry.foo", "node", "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" + node = "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 cd1583428..9b15ecdab 100644 --- a/builtin/providers/consul/resource_provider.go +++ b/builtin/providers/consul/resource_provider.go @@ -26,6 +26,11 @@ func Provider() terraform.ResourceProvider { Type: schema.TypeString, Optional: true, }, + + "token": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, }, DataSourcesMap: map[string]*schema.Resource{ @@ -33,8 +38,12 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "consul_keys": resourceConsulKeys(), - "consul_key_prefix": resourceConsulKeyPrefix(), + "consul_agent_service": resourceConsulAgentService(), + "consul_catalog_entry": resourceConsulCatalogEntry(), + "consul_keys": resourceConsulKeys(), + "consul_key_prefix": resourceConsulKeyPrefix(), + "consul_node": resourceConsulNode(), + "consul_service": resourceConsulService(), }, 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..354301a33 --- /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`. 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.