diff --git a/builtin/providers/consul/resource_consul_prepared_query.go b/builtin/providers/consul/resource_consul_prepared_query.go new file mode 100644 index 0000000000..329bd2f7d8 --- /dev/null +++ b/builtin/providers/consul/resource_consul_prepared_query.go @@ -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 +} diff --git a/builtin/providers/consul/resource_consul_prepared_query_test.go b/builtin/providers/consul/resource_consul_prepared_query_test.go new file mode 100644 index 0000000000..6b08adaa86 --- /dev/null +++ b/builtin/providers/consul/resource_consul_prepared_query_test.go @@ -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" +} +` diff --git a/builtin/providers/consul/resource_provider.go b/builtin/providers/consul/resource_provider.go index c6e3c5b8a5..304f1b91af 100644 --- a/builtin/providers/consul/resource_provider.go +++ b/builtin/providers/consul/resource_provider.go @@ -61,12 +61,13 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "consul_agent_service": resourceConsulAgentService(), - "consul_catalog_entry": resourceConsulCatalogEntry(), - "consul_keys": resourceConsulKeys(), - "consul_key_prefix": resourceConsulKeyPrefix(), - "consul_node": resourceConsulNode(), - "consul_service": resourceConsulService(), + "consul_agent_service": resourceConsulAgentService(), + "consul_catalog_entry": resourceConsulCatalogEntry(), + "consul_keys": resourceConsulKeys(), + "consul_key_prefix": resourceConsulKeyPrefix(), + "consul_node": resourceConsulNode(), + "consul_prepared_query": resourceConsulPreparedQuery(), + "consul_service": resourceConsulService(), }, ConfigureFunc: providerConfigure, diff --git a/vendor/github.com/hashicorp/consul/api/prepared_query.go b/vendor/github.com/hashicorp/consul/api/prepared_query.go index c8141887c4..63e741e050 100644 --- a/vendor/github.com/hashicorp/consul/api/prepared_query.go +++ b/vendor/github.com/hashicorp/consul/api/prepared_query.go @@ -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. diff --git a/vendor/vendor.json b/vendor/vendor.json index 39076c623a..244b958780 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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", diff --git a/website/source/docs/providers/consul/r/prepared_query.markdown b/website/source/docs/providers/consul/r/prepared_query.markdown new file mode 100644 index 0000000000..a8c6cd4124 --- /dev/null +++ b/website/source/docs/providers/consul/r/prepared_query.markdown @@ -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. diff --git a/website/source/layouts/consul.erb b/website/source/layouts/consul.erb index 0af3d742a7..df831b0587 100644 --- a/website/source/layouts/consul.erb +++ b/website/source/layouts/consul.erb @@ -37,9 +37,13 @@ > consul_node + > + consul_prepared_query + > consul_service +