Add a `consul_catalog_service` resource to obtain detailed information about a specific Consul service.

This commit is contained in:
Sean Chittenden 2017-02-14 17:39:25 -08:00
parent 41e4208777
commit 59925478c3
No known key found for this signature in database
GPG Key ID: 4EBC9DC16C2E5E16
4 changed files with 296 additions and 11 deletions

View File

@ -0,0 +1,209 @@
package consul
import (
"fmt"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"github.com/y0ssar1an/q"
)
const (
queryOptServicesAttr = "services"
catalogServiceCreateIndex = "create_index"
catalogServiceDatacenter = "datacenter"
catalogServiceModifyIndex = "modify_index"
catalogServiceNodeAddress = "node_address"
catalogServiceNodeID = "node_id"
catalogServiceNodeMeta = "node_meta"
catalogServiceNodeName = "node_name"
catalogServiceServiceAddress = "address"
catalogServiceServiceEnableTagOverride = "enable_tag_override"
catalogServiceServiceID = "id"
catalogServiceServiceName = "name"
catalogServiceServicePort = "port"
catalogServiceServiceTags = "tags"
catalogServiceTaggedAddresses = "tagged_addresses"
// Filters
catalogServiceName = "name"
catalogServiceTag = "tag"
)
func dataSourceConsulCatalogService() *schema.Resource {
return &schema.Resource{
Read: dataSourceConsulCatalogServiceRead,
Schema: map[string]*schema.Schema{
// Data Source Predicate(s)
catalogServiceDatacenter: &schema.Schema{
// Used in the query, must be stored and force a refresh if the value changes.
Computed: true,
Type: schema.TypeString,
ForceNew: true,
},
catalogServiceTag: &schema.Schema{
// Used in the query, must be stored and force a refresh if the value changes.
Computed: true,
Type: schema.TypeString,
ForceNew: true,
},
catalogServiceName: &schema.Schema{
Required: true,
Type: schema.TypeString,
},
queryOpts: schemaQueryOpts,
// Out parameters
queryOptServicesAttr: &schema.Schema{
Computed: true,
Type: schema.TypeList,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
catalogServiceCreateIndex: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceNodeAddress: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceNodeID: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceModifyIndex: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceNodeName: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceNodeMeta: &schema.Schema{
Type: schema.TypeMap,
Computed: true,
},
catalogServiceServiceAddress: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceServiceEnableTagOverride: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceServiceID: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceServiceName: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceServicePort: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
catalogServiceServiceTags: &schema.Schema{
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
catalogServiceTaggedAddresses: &schema.Schema{
Type: schema.TypeMap,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
schemaTaggedLAN: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
schemaTaggedWAN: &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
},
},
},
}
}
func dataSourceConsulCatalogServiceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
// Parse out data source filters to populate Consul's query options
queryOpts, err := getQueryOpts(d, client)
if err != nil {
return errwrap.Wrapf("unable to get query options for fetching catalog services: {{err}}", err)
}
var serviceName string
if v, ok := d.GetOk(catalogServiceName); ok {
serviceName = v.(string)
}
var serviceTag string
if v, ok := d.GetOk(catalogServiceTag); ok {
serviceTag = v.(string)
}
// services, meta, err := client.Catalog().Services(queryOpts)
services, meta, err := client.Catalog().Service(serviceName, serviceTag, queryOpts)
if err != nil {
return err
}
l := make([]interface{}, 0, len(services))
q.Q(services)
for _, service := range services {
q.Q(service)
const defaultServiceAttrs = 13
m := make(map[string]interface{}, defaultServiceAttrs)
m[catalogServiceCreateIndex] = fmt.Sprintf("%d", service.CreateIndex)
m[catalogServiceModifyIndex] = fmt.Sprintf("%d", service.ModifyIndex)
m[catalogServiceNodeAddress] = service.Address
m[catalogServiceNodeID] = service.ID
m[catalogServiceNodeMeta] = service.NodeMeta
m[catalogServiceNodeName] = service.Node
m[catalogServiceServiceAddress] = service.ServiceAddress
m[catalogServiceServiceEnableTagOverride] = fmt.Sprintf("%t", service.ServiceEnableTagOverride)
m[catalogServiceServiceID] = service.ServiceID
m[catalogServiceServiceName] = service.ServiceName
m[catalogServiceServicePort] = fmt.Sprintf("%d", service.ServicePort)
m[catalogServiceServiceTags] = service.ServiceTags
m[catalogServiceTaggedAddresses] = service.TaggedAddresses
{
const initNumTaggedAddrs = 2
taggedAddrs := make(map[string]interface{}, initNumTaggedAddrs)
if addr, found := service.TaggedAddresses[apiTaggedLAN]; found {
taggedAddrs[schemaTaggedLAN] = addr
}
if addr, found := service.TaggedAddresses[apiTaggedWAN]; found {
taggedAddrs[schemaTaggedWAN] = addr
}
m[catalogServiceTaggedAddresses] = taggedAddrs
}
l = append(l, m)
}
const idKeyFmt = "catalog-service-%s-%q-%q"
d.SetId(fmt.Sprintf(idKeyFmt, queryOpts.Datacenter, serviceName, serviceTag))
d.Set(catalogServiceDatacenter, queryOpts.Datacenter)
d.Set(catalogServiceTag, serviceTag)
if err := d.Set(queryOptServicesAttr, l); err != nil {
return errwrap.Wrapf("Unable to store services: {{err}}", err)
}
return nil
}

View File

@ -0,0 +1,50 @@
package consul
import (
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccDataConsulCatalogService_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataConsulCatalogServiceConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataSourceValue("data.consul_catalog_service.read", "datacenter", "dc1"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.#", "1"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.address", "<all>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.create_index", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.enable_tag_override", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.id", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.modify_index", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.name", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.node_address", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.node_id", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.node_meta.%", "0"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.node_name", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.port", "<any>"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.tagged_addresses.%", "2"),
testAccCheckDataSourceValue("data.consul_catalog_service.read", "services.0.tags.#", "0"),
),
},
},
})
}
const testAccDataConsulCatalogServiceConfig = `
data "consul_catalog_service" "read" {
query_options {
allow_stale = true
require_consistent = false
token = ""
wait_index = 0
wait_time = "1m"
}
name = "consul"
}
`

View File

@ -9,6 +9,8 @@ import (
const (
queryOptAllowStale = "allow_stale"
queryOptDatacenter = "datacenter"
queryOptNear = "near"
queryOptNodeMeta = "node_meta"
queryOptRequireConsistent = "require_consistent"
queryOptToken = "token"
@ -26,6 +28,20 @@ var schemaQueryOpts = &schema.Schema{
Default: true,
Type: schema.TypeBool,
},
queryOptDatacenter: &schema.Schema{
// Optional because we'll pull the default from the local agent if it's
// not specified, but we can query remote data centers as a result.
Optional: true,
Type: schema.TypeString,
},
queryOptNear: &schema.Schema{
Optional: true,
Type: schema.TypeString,
},
queryOptNodeMeta: &schema.Schema{
Optional: true,
Type: schema.TypeMap,
},
queryOptRequireConsistent: &schema.Schema{
Optional: true,
Default: false,
@ -57,19 +73,28 @@ var schemaQueryOpts = &schema.Schema{
}
func getQueryOpts(d *schema.ResourceData, client *consulapi.Client) (*consulapi.QueryOptions, error) {
dc, err := getDC(d, client)
if err != nil {
return nil, err
}
queryOpts := &consulapi.QueryOptions{
Datacenter: dc,
}
queryOpts := &consulapi.QueryOptions{}
if v, ok := d.GetOk(queryOptAllowStale); ok {
queryOpts.AllowStale = v.(bool)
}
if v, ok := d.GetOk(queryOptDatacenter); ok {
queryOpts.Datacenter = v.(string)
}
if queryOpts.Datacenter == "" {
dc, err := getDC(d, client)
if err != nil {
return nil, err
}
queryOpts.Datacenter = dc
}
if v, ok := d.GetOk(queryOptNear); ok {
queryOpts.Near = v.(string)
}
if v, ok := d.GetOk(queryOptRequireConsistent); ok {
queryOpts.RequireConsistent = v.(bool)
}

View File

@ -64,9 +64,10 @@ func Provider() terraform.ResourceProvider {
},
DataSourcesMap: map[string]*schema.Resource{
"consul_agent_self": dataSourceConsulAgentSelf(),
"consul_catalog_nodes": dataSourceConsulCatalogNodes(),
"consul_keys": dataSourceConsulKeys(),
"consul_agent_self": dataSourceConsulAgentSelf(),
"consul_catalog_nodes": dataSourceConsulCatalogNodes(),
"consul_catalog_service": dataSourceConsulCatalogService(),
"consul_keys": dataSourceConsulKeys(),
},
ResourcesMap: map[string]*schema.Resource{