Merge pull request #1 from maxenglander/2087-consul-service-resource

Feature request: consul_service resource
This commit is contained in:
Max Englander 2015-10-29 11:47:54 -04:00
commit cd0b46f1eb
7 changed files with 513 additions and 10 deletions

View File

@ -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'",

View File

@ -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
}

View File

@ -0,0 +1,135 @@
package consul
import (
"fmt"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceConsulAgentService() *schema.Resource {
return &schema.Resource{
Create: resourceConsulAgentServiceCreate,
Update: resourceConsulAgentServiceCreate,
Read: resourceConsulAgentServiceRead,
Delete: resourceConsulAgentServiceDelete,
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
ForceNew: true,
},
},
}
}
func resourceConsulAgentServiceCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
agent := client.Agent()
name := d.Get("name").(string)
registration := consulapi.AgentServiceRegistration{Name: name}
if address, ok := d.GetOk("address"); ok {
registration.Address = address.(string)
}
if id, ok := d.GetOk("id"); ok {
registration.ID = id.(string)
}
if port, ok := d.GetOk("port"); ok {
registration.Port = port.(int)
}
if v, ok := d.GetOk("tags"); ok {
vs := v.([]interface{})
s := make([]string, len(vs))
for i, raw := range vs {
s[i] = raw.(string)
}
registration.Tags = s
}
if err := agent.ServiceRegister(&registration); err != nil {
return fmt.Errorf("Failed to register service '%s' with Consul agent: %v", name, err)
}
// Update the resource
if serviceMap, err := agent.Services(); err != nil {
return fmt.Errorf("Failed to read services from Consul agent: %v", err)
} else if service, ok := serviceMap[name]; !ok {
return fmt.Errorf("Failed to read service '%s' from Consul agent: %v", name, err)
} else {
d.Set("address", service.Address)
d.Set("name", service.Service)
d.Set("port", service.Port)
d.Set("tags", service.Tags)
d.SetId(service.ID)
}
return nil
}
func resourceConsulAgentServiceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
agent := client.Agent()
name := d.Get("name").(string)
if services, err := agent.Services(); err != nil {
return fmt.Errorf("Failed to get services from Consul agent: %v", err)
} else if service, ok := services[name]; !ok {
return fmt.Errorf("Failed to get service '%s' from Consul agent", name)
} else {
d.Set("address", service.Address)
d.Set("name", service.Service)
d.Set("port", service.Port)
d.Set("tags", service.Tags)
d.SetId(service.ID)
}
return nil
}
func resourceConsulAgentServiceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Agent()
name := d.Get("name").(string)
if err := catalog.ServiceDeregister(name); err != nil {
return fmt.Errorf("Failed to deregister service '%s' from Consul agent: %v", name, err)
}
// Clear the ID
d.SetId("")
return nil
}

View File

@ -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 != "<any>" && out != val {
return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val)
}
if val == "<any>" && 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
}
`

View File

@ -0,0 +1,265 @@
package consul
import (
"bytes"
"fmt"
"sort"
"strings"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceConsulCatalogEntry() *schema.Resource {
return &schema.Resource{
Create: resourceConsulCatalogEntryCreate,
Update: resourceConsulCatalogEntryCreate,
Read: resourceConsulCatalogEntryRead,
Delete: resourceConsulCatalogEntryDelete,
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"node": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"service": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
Set: resourceConsulCatalogEntryServicesHash,
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
}
}
func resourceConsulCatalogEntryServicesHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["id"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["address"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["port"].(int)))
if v, ok := m["tags"]; ok {
vs := v.([]interface{})
s := make([]string, len(vs))
for i, raw := range vs {
s[i] = raw.(string)
}
sort.Strings(s)
for _, v := range s {
buf.WriteString(fmt.Sprintf("%s-", v))
}
}
return hashcode.String(buf.String())
}
func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
} else {
var err error
if dc, err = getDC(client); err != nil {
return err
}
}
var token string
if v, ok := d.GetOk("token"); ok {
token = v.(string)
}
// Setup the operations using the datacenter
wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token}
address := d.Get("address").(string)
node := d.Get("node").(string)
var serviceIDs []string
if service, ok := d.GetOk("service"); ok {
serviceList := service.(*schema.Set).List()
serviceIDs = make([]string, len(serviceList))
for i, rawService := range serviceList {
serviceData := rawService.(map[string]interface{})
serviceID := serviceData["id"].(string)
serviceIDs[i] = serviceID
var tags []string
if v := serviceData["tags"].([]interface{}); len(v) > 0 {
tags = make([]string, len(v))
for i, raw := range v {
tags[i] = raw.(string)
}
}
registration := &consulapi.CatalogRegistration{
Address: address,
Datacenter: dc,
Node: node,
Service: &consulapi.AgentService{
Address: serviceData["address"].(string),
ID: serviceID,
Service: serviceData["name"].(string),
Port: serviceData["port"].(int),
Tags: tags,
},
}
if _, err := catalog.Register(registration, &wOpts); err != nil {
return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' in %s: %v",
node, address, dc, err)
}
}
} else {
registration := &consulapi.CatalogRegistration{
Address: address,
Datacenter: dc,
Node: node,
}
if _, err := catalog.Register(registration, &wOpts); err != nil {
return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' in %s: %v",
node, address, dc, err)
}
}
// Update the resource
qOpts := consulapi.QueryOptions{Datacenter: dc}
if _, _, err := catalog.Node(node, &qOpts); err != nil {
return fmt.Errorf("Failed to read Consul catalog entry for node '%s' at address '%s' in %s: %v",
node, address, dc, err)
} else {
d.Set("datacenter", dc)
}
sort.Strings(serviceIDs)
serviceIDsJoined := strings.Join(serviceIDs, ",")
d.SetId(fmt.Sprintf("%s-%s-[%s]", node, address, serviceIDsJoined))
return nil
}
func resourceConsulCatalogEntryRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
// Get the DC, error if not available.
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
}
node := d.Get("node").(string)
// Setup the operations using the datacenter
qOpts := consulapi.QueryOptions{Datacenter: dc}
if _, _, err := catalog.Node(node, &qOpts); err != nil {
return fmt.Errorf("Failed to get node '%s' from Consul catalog: %v", node, err)
}
return nil
}
func resourceConsulCatalogEntryDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
} else {
var err error
if dc, err = getDC(client); err != nil {
return err
}
}
var token string
if v, ok := d.GetOk("token"); ok {
token = v.(string)
}
// Setup the operations using the datacenter
wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token}
address := d.Get("address").(string)
node := d.Get("node").(string)
deregistration := consulapi.CatalogDeregistration{
Address: address,
Datacenter: dc,
Node: node,
}
if _, err := catalog.Deregister(&deregistration, &wOpts); err != nil {
return fmt.Errorf("Failed to deregister Consul catalog entry with node '%s' at address '%s' in %s: %v",
node, address, dc, err)
}
// Clear the ID
d.SetId("")
return nil
}

View File

@ -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
}

View File

@ -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(),
},