Adds consul_prepared_query resource (#7474)

* provider/consul: first stab at adding prepared query support

* provider/consul: flatten pq resource

* provider/consul: implement updates for PQ's

* provider/consul: implement PQ delete

* provider/consul: add acceptance tests for prepared queries

* provider/consul: add template support to PQ's

* provider/consul: use substructures to express optional related components for PQs

* website: first pass at consul prepared query docs

* provider/consul: PQ's support datacenter option and store_token option

* provider/consul: remove store_token on PQ's for now

* provider/consul: allow specifying a separate stored_token

* website: update consul PQ docs

* website: add link to consul_prepared_query resource

* vendor: update github.com/hashicorp/consul/api

* provider/consul: handle 404's when reading prepared queries

* provider/consul: prepared query failover dcs is a list

* website: update consul PQ example usage

* website: re-order arguments for consul prepared queries
This commit is contained in:
Ryan Uber 2016-08-18 00:46:30 -07:00 committed by Paul Stack
parent d7c028d210
commit ec7fc60d5f
7 changed files with 576 additions and 9 deletions

View File

@ -0,0 +1,271 @@
package consul
import (
"strings"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceConsulPreparedQuery() *schema.Resource {
return &schema.Resource{
Create: resourceConsulPreparedQueryCreate,
Update: resourceConsulPreparedQueryUpdate,
Read: resourceConsulPreparedQueryRead,
Delete: resourceConsulPreparedQueryDelete,
SchemaVersion: 0,
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"session": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"stored_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"service": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"tags": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"near": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"only_passing": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"failover": &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"nearest_n": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"datacenters": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"dns": &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"ttl": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
"template": &schema.Schema{
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"regexp": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
},
},
}
}
func resourceConsulPreparedQueryCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
wo := &consulapi.WriteOptions{
Datacenter: d.Get("datacenter").(string),
Token: d.Get("token").(string),
}
pq := preparedQueryDefinitionFromResourceData(d)
id, _, err := client.PreparedQuery().Create(pq, wo)
if err != nil {
return err
}
d.SetId(id)
return resourceConsulPreparedQueryRead(d, meta)
}
func resourceConsulPreparedQueryUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
wo := &consulapi.WriteOptions{
Datacenter: d.Get("datacenter").(string),
Token: d.Get("token").(string),
}
pq := preparedQueryDefinitionFromResourceData(d)
if _, err := client.PreparedQuery().Update(pq, wo); err != nil {
return err
}
return resourceConsulPreparedQueryRead(d, meta)
}
func resourceConsulPreparedQueryRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
qo := &consulapi.QueryOptions{
Datacenter: d.Get("datacenter").(string),
Token: d.Get("token").(string),
}
queries, _, err := client.PreparedQuery().Get(d.Id(), qo)
if err != nil {
// Check for a 404/not found, these are returned as errors.
if strings.Contains(err.Error(), "not found") {
d.SetId("")
return nil
}
return err
}
if len(queries) != 1 {
d.SetId("")
return nil
}
pq := queries[0]
d.Set("name", pq.Name)
d.Set("session", pq.Session)
d.Set("stored_token", pq.Token)
d.Set("service", pq.Service.Service)
d.Set("near", pq.Service.Near)
d.Set("only_passing", pq.Service.OnlyPassing)
d.Set("tags", pq.Service.Tags)
if pq.Service.Failover.NearestN > 0 {
d.Set("failover.0.nearest_n", pq.Service.Failover.NearestN)
}
if len(pq.Service.Failover.Datacenters) > 0 {
d.Set("failover.0.datacenters", pq.Service.Failover.Datacenters)
}
if pq.DNS.TTL != "" {
d.Set("dns.0.ttl", pq.DNS.TTL)
}
if pq.Template.Type != "" {
d.Set("template.0.type", pq.Template.Type)
d.Set("template.0.regexp", pq.Template.Regexp)
}
return nil
}
func resourceConsulPreparedQueryDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
qo := &consulapi.QueryOptions{
Datacenter: d.Get("datacenter").(string),
Token: d.Get("token").(string),
}
if _, err := client.PreparedQuery().Delete(d.Id(), qo); err != nil {
return err
}
d.SetId("")
return nil
}
func preparedQueryDefinitionFromResourceData(d *schema.ResourceData) *consulapi.PreparedQueryDefinition {
pq := &consulapi.PreparedQueryDefinition{
ID: d.Id(),
Name: d.Get("name").(string),
Session: d.Get("session").(string),
Token: d.Get("stored_token").(string),
Service: consulapi.ServiceQuery{
Service: d.Get("service").(string),
Near: d.Get("near").(string),
OnlyPassing: d.Get("only_passing").(bool),
},
}
tags := d.Get("tags").(*schema.Set).List()
pq.Service.Tags = make([]string, len(tags))
for i, v := range tags {
pq.Service.Tags[i] = v.(string)
}
if _, ok := d.GetOk("failover.0"); ok {
failover := consulapi.QueryDatacenterOptions{
NearestN: d.Get("failover.0.nearest_n").(int),
}
dcs := d.Get("failover.0.datacenters").([]interface{})
failover.Datacenters = make([]string, len(dcs))
for i, v := range dcs {
failover.Datacenters[i] = v.(string)
}
pq.Service.Failover = failover
}
if _, ok := d.GetOk("template.0"); ok {
pq.Template = consulapi.QueryTemplate{
Type: d.Get("template.0.type").(string),
Regexp: d.Get("template.0.regexp").(string),
}
}
if _, ok := d.GetOk("dns.0"); ok {
pq.DNS = consulapi.QueryDNSOptions{
TTL: d.Get("dns.0.ttl").(string),
}
}
return pq
}

View File

@ -0,0 +1,171 @@
package consul
import (
"fmt"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccConsulPreparedQuery_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckConsulPreparedQueryDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccConsulPreparedQueryConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulPreparedQueryExists(),
testAccCheckConsulPreparedQueryAttrValue("name", "foo"),
testAccCheckConsulPreparedQueryAttrValue("stored_token", "pq-token"),
testAccCheckConsulPreparedQueryAttrValue("service", "redis"),
testAccCheckConsulPreparedQueryAttrValue("near", "_agent"),
testAccCheckConsulPreparedQueryAttrValue("tags.#", "1"),
testAccCheckConsulPreparedQueryAttrValue("only_passing", "true"),
testAccCheckConsulPreparedQueryAttrValue("failover.0.nearest_n", "3"),
testAccCheckConsulPreparedQueryAttrValue("failover.0.datacenters.#", "2"),
testAccCheckConsulPreparedQueryAttrValue("template.0.type", "name_prefix_match"),
testAccCheckConsulPreparedQueryAttrValue("template.0.regexp", "hello"),
testAccCheckConsulPreparedQueryAttrValue("dns.0.ttl", "8m"),
),
},
resource.TestStep{
Config: testAccConsulPreparedQueryConfigUpdate1,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulPreparedQueryExists(),
testAccCheckConsulPreparedQueryAttrValue("name", "baz"),
testAccCheckConsulPreparedQueryAttrValue("stored_token", "pq-token-updated"),
testAccCheckConsulPreparedQueryAttrValue("service", "memcached"),
testAccCheckConsulPreparedQueryAttrValue("near", "node1"),
testAccCheckConsulPreparedQueryAttrValue("tags.#", "2"),
testAccCheckConsulPreparedQueryAttrValue("only_passing", "false"),
testAccCheckConsulPreparedQueryAttrValue("failover.0.nearest_n", "2"),
testAccCheckConsulPreparedQueryAttrValue("failover.0.datacenters.#", "1"),
testAccCheckConsulPreparedQueryAttrValue("template.0.regexp", "goodbye"),
testAccCheckConsulPreparedQueryAttrValue("dns.0.ttl", "16m"),
),
},
resource.TestStep{
Config: testAccConsulPreparedQueryConfigUpdate2,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulPreparedQueryExists(),
testAccCheckConsulPreparedQueryAttrValue("stored_token", ""),
testAccCheckConsulPreparedQueryAttrValue("near", ""),
testAccCheckConsulPreparedQueryAttrValue("tags.#", "0"),
testAccCheckConsulPreparedQueryAttrValue("failover.#", "0"),
testAccCheckConsulPreparedQueryAttrValue("template.#", "0"),
testAccCheckConsulPreparedQueryAttrValue("dns.#", "0"),
),
},
},
})
}
func checkPreparedQueryExists(s *terraform.State) bool {
rn, ok := s.RootModule().Resources["consul_prepared_query.foo"]
if !ok {
return false
}
id := rn.Primary.ID
client := testAccProvider.Meta().(*consulapi.Client).PreparedQuery()
opts := &consulapi.QueryOptions{Datacenter: "dc1"}
pq, _, err := client.Get(id, opts)
return err == nil && pq != nil
}
func testAccCheckConsulPreparedQueryDestroy(s *terraform.State) error {
if checkPreparedQueryExists(s) {
return fmt.Errorf("Prepared query 'foo' still exists")
}
return nil
}
func testAccCheckConsulPreparedQueryExists() resource.TestCheckFunc {
return func(s *terraform.State) error {
if !checkPreparedQueryExists(s) {
return fmt.Errorf("Prepared query 'foo' does not exist")
}
return nil
}
}
func testAccCheckConsulPreparedQueryAttrValue(attr, val string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rn, ok := s.RootModule().Resources["consul_prepared_query.foo"]
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 out != val {
return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val)
}
return nil
}
}
const testAccConsulPreparedQueryConfig = `
resource "consul_prepared_query" "foo" {
name = "foo"
token = "client-token"
stored_token = "pq-token"
service = "redis"
tags = ["prod"]
near = "_agent"
only_passing = true
failover {
nearest_n = 3
datacenters = ["dc1", "dc2"]
}
template {
type = "name_prefix_match"
regexp = "hello"
}
dns {
ttl = "8m"
}
}
`
const testAccConsulPreparedQueryConfigUpdate1 = `
resource "consul_prepared_query" "foo" {
name = "baz"
token = "client-token"
stored_token = "pq-token-updated"
service = "memcached"
tags = ["prod","sup"]
near = "node1"
only_passing = false
failover {
nearest_n = 2
datacenters = ["dc2"]
}
template {
type = "name_prefix_match"
regexp = "goodbye"
}
dns {
ttl = "16m"
}
}
`
const testAccConsulPreparedQueryConfigUpdate2 = `
resource "consul_prepared_query" "foo" {
name = "baz"
service = "memcached"
token = "client-token"
}
`

View File

@ -66,6 +66,7 @@ func Provider() terraform.ResourceProvider {
"consul_keys": resourceConsulKeys(),
"consul_key_prefix": resourceConsulKeyPrefix(),
"consul_node": resourceConsulNode(),
"consul_prepared_query": resourceConsulPreparedQuery(),
"consul_service": resourceConsulService(),
},

View File

@ -25,6 +25,11 @@ type ServiceQuery struct {
// Service is the service to query.
Service string
// Near allows baking in the name of a node to automatically distance-
// sort from. The magic "_agent" value is supported, which sorts near
// the agent which initiated the request by default.
Near string
// Failover controls what we do if there are no healthy nodes in the
// local datacenter.
Failover QueryDatacenterOptions
@ -40,6 +45,17 @@ type ServiceQuery struct {
Tags []string
}
// QueryTemplate carries the arguments for creating a templated query.
type QueryTemplate struct {
// Type specifies the type of the query template. Currently only
// "name_prefix_match" is supported. This field is required.
Type string
// Regexp allows specifying a regex pattern to match against the name
// of the query being executed.
Regexp string
}
// PrepatedQueryDefinition defines a complete prepared query.
type PreparedQueryDefinition struct {
// ID is this UUID-based ID for the query, always generated by Consul.
@ -67,6 +83,11 @@ type PreparedQueryDefinition struct {
// DNS has options that control how the results of this query are
// served over DNS.
DNS QueryDNSOptions
// Template is used to pass through the arguments for creating a
// prepared query with an attached template. If a template is given,
// interpolations are possible in other struct fields.
Template QueryTemplate
}
// PreparedQueryExecuteResponse has the results of executing a query.

6
vendor/vendor.json vendored
View File

@ -902,11 +902,11 @@
"revisionTime": "2016-07-26T16:33:11Z"
},
{
"checksumSHA1": "glOabn8rkJvz7tjz/xfX4lmt070=",
"checksumSHA1": "ZY6NCrR80zUmtOtPtKffbmFxRWw=",
"comment": "v0.6.3-28-g3215b87",
"path": "github.com/hashicorp/consul/api",
"revision": "d4a8a43d2b600e662a50a75be70daed5fad8dd2d",
"revisionTime": "2016-06-04T06:35:46Z"
"revision": "6e061b2d580d80347b7c5c4dfc8730de7403a145",
"revisionTime": "2016-07-03T02:45:54Z"
},
{
"path": "github.com/hashicorp/errwrap",

View File

@ -0,0 +1,99 @@
---
layout: "consul"
page_title: "Consul: consul_prepared_query"
sidebar_current: "docs-consul-resource-prepared-query"
description: |-
Allows Terraform to manage a Consul prepared query
---
# consul\_prepared\_query
Allows Terraform to manage a Consul prepared query.
Managing prepared queries is done using Consul's REST API. This resource is
useful to provide a consistent and declarative way of managing prepared
queries in your Consul cluster using Terraform.
## Example Usage
```
resource "consul_prepared_query" "service-near-self" {
datacenter = "nyc1"
token = "abcd"
stored_token = "wxyz"
name = ""
only_passing = true
near = "_agent"
template {
type = "name_prefix_match"
regexp = "^(.*)-near-self$"
}
service = "$${match(1)}"
failover {
nearest_n = 3
datacenters = ["dc2", "dc3", "dc4"]
}
dns {
ttl = "5m"
}
}
```
## Argument Reference
The following arguments are supported:
* `datacenter` - (Optional) The datacenter to use. This overrides the
datacenter in the provider setup and the agent's default datacenter.
* `token` - (Optional) The ACL token to use when saving the prepared query.
This overrides the token that the agent provides by default.
* `stored_token` - (Optional) The ACL token to store with the prepared
query. This token will be used by default whenever the query is executed.
* `name` - (Required) The name of the prepared query. Used to identify
the prepared query during requests. Can be specified as an empty string
to configure the query as a catch-all.
* `service` - (Required) The name of the service to query.
* `only_passing` - (Optional) When true, the prepared query will only
return nodes with passing health checks in the result.
* `near` - (Optional) Allows specifying the name of a node to sort results
near using Consul's distance sorting and network coordinates. The magic
`_agent` value can be used to always sort nearest the node servicing the
request.
* `failover` - (Optional) Options for controlling behavior when no healthy
nodes are available in the local DC.
* `nearest_n` - (Optional) Return results from this many datacenters,
sorted in ascending order of estimated RTT.
* `datacenters` - (Optional) Remote datacenters to return results from.
* `dns` - (Optional) Settings for controlling the DNS response details.
* `ttl` - (Optional) The TTL to send when returning DNS results.
* `template` - (Optional) Query templating options. This is used to make a
single prepared query respond to many different requests.
* `type` - (Required) The type of template matching to perform. Currently
only `name_prefix_match` is supported.
* `regexp` - (Required) The regular expression to match with. When using
`name_prefix_match`, this regex is applied against the query name.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the prepared query, generated by Consul.

View File

@ -36,10 +36,14 @@
</li>
<li<%= sidebar_current("docs-consul-resource-node") %>>
<a href="/docs/providers/consul/r/node.html">consul_node</a>
</li>
<li<%= sidebar_current("docs-consul-resource-prepared-query") %>>
<a href="/docs/providers/consul/r/prepared_query.html">consul_prepared_query</a>
</li>
<li<%= sidebar_current("docs-consul-resource-service") %>>
<a href="/docs/providers/consul/r/service.html">consul_service</a>
</li>
</ul>
</li>
</ul>