From 3e9868731d22de4bba88c178366d1fbea2177acb Mon Sep 17 00:00:00 2001 From: Ashesh Vidyut <134911583+absolutelightning@users.noreply.github.com> Date: Wed, 25 Oct 2023 18:10:15 +0530 Subject: [PATCH] Add the consul_config_entry_service_router resource (#369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ashesh Vidyut Co-authored-by: RĂ©mi Lapeyre --- ...urce_consul_config_entry_service_router.go | 617 ++++++++++++++++++ ...sul_config_entry_service_router_ce_test.go | 255 ++++++++ ...sul_config_entry_service_router_ee_test.go | 261 ++++++++ consul/resource_provider.go | 1 + .../config_entry_service_defaults.md | 31 +- .../config_entry_service_intentions.md | 47 +- .../config_entry_service_resolver.md | 36 +- docs/resources/config_entry_service_router.md | 151 +++++ .../config_entry_service_splitter.md | 27 +- .../resource.tf | 26 + .../resource.tf | 42 ++ .../resource.tf | 31 + .../resource.tf | 26 + .../resource.tf | 27 +- 14 files changed, 1545 insertions(+), 33 deletions(-) create mode 100644 consul/resource_consul_config_entry_service_router.go create mode 100644 consul/resource_consul_config_entry_service_router_ce_test.go create mode 100644 consul/resource_consul_config_entry_service_router_ee_test.go create mode 100644 docs/resources/config_entry_service_router.md create mode 100644 examples/resources/consul_config_entry_service_defaults/resource.tf create mode 100644 examples/resources/consul_config_entry_service_intentions/resource.tf create mode 100644 examples/resources/consul_config_entry_service_resolver/resource.tf create mode 100644 examples/resources/consul_config_entry_service_router/resource.tf diff --git a/consul/resource_consul_config_entry_service_router.go b/consul/resource_consul_config_entry_service_router.go new file mode 100644 index 00000000..ed0ff2df --- /dev/null +++ b/consul/resource_consul_config_entry_service_router.go @@ -0,0 +1,617 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "fmt" + "time" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +type serviceRouter struct{} + +func (s *serviceRouter) GetKind() string { + return consulapi.ServiceRouter +} + +func (s *serviceRouter) GetDescription() string { + return "The `consul_config_entry_service_router` resource configures a [service router](https://developer.hashicorp.com/consul/docs/connect/config-entries/service-router) to redirect a traffic request for a service to one or more specific service instances." +} + +func (s *serviceRouter) GetSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "Specifies a name for the configuration entry.", + Required: true, + ForceNew: true, + }, + "partition": { + Type: schema.TypeString, + Description: "Specifies the admin partition to apply the configuration entry.", + Optional: true, + ForceNew: true, + }, + "namespace": { + Type: schema.TypeString, + Description: "Specifies the namespace to apply the configuration entry.", + Optional: true, + ForceNew: true, + }, + "meta": { + Type: schema.TypeMap, + Description: "Specifies key-value pairs to add to the KV store.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "routes": { + Type: schema.TypeList, + Description: "Defines the possible routes for L7 requests.", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "match": { + Type: schema.TypeList, + MaxItems: 1, + Description: "Describes a set of criteria that Consul compares incoming L7 traffic with.", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "http": { + Type: schema.TypeList, + MaxItems: 1, + Description: "Specifies a set of HTTP criteria used to evaluate incoming L7 traffic for matches.", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "path_exact": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the exact path to match on the HTTP request path.", + }, + "path_prefix": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the path prefix to match on the HTTP request path.", + }, + "path_regex": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a regular expression to match on the HTTP request path.", + }, + "methods": { + Type: schema.TypeList, + Description: "Specifies HTTP methods that the match applies to.", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "header": { + Type: schema.TypeList, + Optional: true, + Description: "Specifies information in the HTTP request header to match with.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "Specifies the name of the HTTP header to match.", + Optional: true, + }, + "present": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies that a request matches when the value in the `name` argument is present anywhere in the HTTP header.", + }, + "exact": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies that a request matches when the header with the given name is this exact value.", + }, + "prefix": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies that a request matches when the header with the given name has this prefix.", + }, + "suffix": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies that a request matches when the header with the given name has this suffix.", + }, + "regex": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies that a request matches when the header with the given name matches this regular expression.", + }, + "invert": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies that the logic for the HTTP header match should be inverted.", + }, + }, + }, + }, + "query_param": { + Type: schema.TypeList, + Optional: true, + Description: "Specifies information to match to on HTTP query parameters.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "Specifies the name of the HTTP query parameter to match.", + Optional: true, + }, + "present": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies that a request matches when the value in the `name` argument is present anywhere in the HTTP query parameter.", + }, + "exact": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies that a request matches when the query parameter with the given name is this exact value.", + }, + "regex": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies that a request matches when the query parameter with the given name matches this regular expression.", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "destination": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Specifies the target service to route matching requests to, as well as behavior for the request to follow when routed.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "service": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the name of the service to resolve.", + }, + "service_subset": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a named subset of the given service to resolve instead of the one defined as that service's `default_subset` in the service resolver configuration entry.", + }, + "namespace": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the Consul namespace to resolve the service from instead of the current namespace.", + }, + "partition": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the Consul admin partition to resolve the service from instead of the current partition.", + }, + "prefix_rewrite": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies rewrites to the HTTP request path before proxying it to its final destination.", + }, + "request_timeout": { + Type: schema.TypeString, + Optional: true, + Default: "0s", + Description: "Specifies the total amount of time permitted for the entire downstream request to be processed, including retry attempts.", + }, + "idle_timeout": { + Type: schema.TypeString, + Optional: true, + Default: "0s", + Description: "Specifies the total amount of time permitted for the request stream to be idle.", + }, + "num_retries": { + Type: schema.TypeInt, + Optional: true, + Description: "Specifies the number of times to retry the request when a retry condition occurs.", + }, + "retry_on_connect_failure": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies that connection failure errors that trigger a retry request.", + }, + "retry_on": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "Specifies a list of conditions for Consul to retry requests based on the response from an upstream service.", + }, + "retry_on_status_codes": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeInt}, + Optional: true, + Description: "Specifies a list of integers for HTTP response status codes that trigger a retry request.", + }, + "request_headers": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Specifies a set of HTTP-specific header modification rules applied to requests routed with the service router.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "add": { + Type: schema.TypeMap, + Description: "Defines a set of key-value pairs to add to the header. Use header names as the keys.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "set": { + Type: schema.TypeMap, + Optional: true, + Description: "Defines a set of key-value pairs to add to the request header or to replace existing header values with.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "remove": { + Type: schema.TypeList, + Description: "Defines a list of headers to remove.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "response_headers": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Specifies a set of HTTP-specific header modification rules applied to responses routed with the service router.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "add": { + Type: schema.TypeMap, + Optional: true, + Description: "Defines a set of key-value pairs to add to the header. Use header names as the keys", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "set": { + Type: schema.TypeMap, + Optional: true, + Description: "Defines a set of key-value pairs to add to the response header or to replace existing header values with", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "remove": { + Type: schema.TypeList, + Optional: true, + Description: "Defines a list of headers to remove.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (s *serviceRouter) Decode(d *schema.ResourceData) (consulapi.ConfigEntry, error) { + configEntry := &consulapi.ServiceRouterConfigEntry{ + Kind: consulapi.ServiceRouter, + Name: d.Get("name").(string), + Partition: d.Get("partition").(string), + Namespace: d.Get("namespace").(string), + Meta: map[string]string{}, + } + + for k, v := range d.Get("meta").(map[string]interface{}) { + configEntry.Meta[k] = v.(string) + } + + for i, r := range d.Get("routes").([]interface{}) { + route := r.(map[string]interface{}) + + sr := consulapi.ServiceRoute{ + Destination: &consulapi.ServiceRouteDestination{}, + } + + for _, m := range route["match"].([]interface{}) { + match := m.(map[string]interface{}) + + for _, h := range match["http"].([]interface{}) { + http := h.(map[string]interface{}) + + sr.Match = &consulapi.ServiceRouteMatch{ + HTTP: &consulapi.ServiceRouteHTTPMatch{ + PathExact: http["path_exact"].(string), + PathPrefix: http["path_prefix"].(string), + PathRegex: http["path_regex"].(string), + }, + } + + for _, method := range http["methods"].([]interface{}) { + sr.Match.HTTP.Methods = append(sr.Match.HTTP.Methods, method.(string)) + } + + for _, h := range http["header"].([]interface{}) { + header := h.(map[string]interface{}) + + sr.Match.HTTP.Header = append(sr.Match.HTTP.Header, consulapi.ServiceRouteHTTPMatchHeader{ + Name: header["name"].(string), + Exact: header["exact"].(string), + Prefix: header["prefix"].(string), + Suffix: header["suffix"].(string), + Regex: header["regex"].(string), + Present: header["present"].(bool), + Invert: header["invert"].(bool), + }) + } + + for _, q := range http["query_param"].([]interface{}) { + queryParam := q.(map[string]interface{}) + + sr.Match.HTTP.QueryParam = append(sr.Match.HTTP.QueryParam, consulapi.ServiceRouteHTTPMatchQueryParam{ + Name: queryParam["name"].(string), + Exact: queryParam["exact"].(string), + Regex: queryParam["regex"].(string), + Present: queryParam["present"].(bool), + }) + } + } + } + + for _, d := range route["destination"].([]interface{}) { + destination := d.(map[string]interface{}) + + sr.Destination = &consulapi.ServiceRouteDestination{ + Service: destination["service"].(string), + ServiceSubset: destination["service_subset"].(string), + Namespace: destination["namespace"].(string), + Partition: destination["partition"].(string), + PrefixRewrite: destination["prefix_rewrite"].(string), + NumRetries: uint32(destination["num_retries"].(int)), + RetryOnConnectFailure: destination["retry_on_connect_failure"].(bool), + } + + parseDuration := func(name string) (time.Duration, error) { + dur, err := time.ParseDuration(destination[name].(string)) + if err != nil { + return 0, fmt.Errorf("failed to parse routes[%d].destination.%s: %w", i, name, err) + } + return dur, nil + } + + dur, err := parseDuration("request_timeout") + if err != nil { + return nil, err + } + sr.Destination.RequestTimeout = dur + + dur, err = parseDuration("idle_timeout") + if err != nil { + return nil, err + } + sr.Destination.IdleTimeout = dur + + for _, r := range destination["retry_on_status_codes"].([]interface{}) { + sr.Destination.RetryOnStatusCodes = append(sr.Destination.RetryOnStatusCodes, uint32(r.(int))) + } + + for _, r := range destination["retry_on"].([]interface{}) { + sr.Destination.RetryOn = append(sr.Destination.RetryOn, r.(string)) + } + + parseHTTPHeaderModifiers := func(name string) *consulapi.HTTPHeaderModifiers { + if len(destination[name].([]interface{})) == 0 { + return nil + } + + headers := destination[name].([]interface{})[0].(map[string]interface{}) + result := &consulapi.HTTPHeaderModifiers{ + Add: map[string]string{}, + Set: map[string]string{}, + } + + for k, v := range headers["add"].(map[string]interface{}) { + result.Add[k] = v.(string) + } + + for k, v := range headers["set"].(map[string]interface{}) { + result.Add[k] = v.(string) + } + + for _, v := range headers["remove"].([]interface{}) { + result.Remove = append(result.Remove, v.(string)) + } + + return result + } + + sr.Destination.RequestHeaders = parseHTTPHeaderModifiers("request_headers") + sr.Destination.ResponseHeaders = parseHTTPHeaderModifiers("response_headers") + } + + configEntry.Routes = append(configEntry.Routes, sr) + } + + return configEntry, nil +} + +func (s *serviceRouter) Write(ce consulapi.ConfigEntry, d *schema.ResourceData, sw *stateWriter) error { + sr, ok := ce.(*consulapi.ServiceRouterConfigEntry) + if !ok { + return fmt.Errorf("expected '%s' but got '%s'", consulapi.ServiceDefaults, ce.GetKind()) + } + + sw.set("name", sr.Name) + sw.set("partition", sr.Partition) + sw.set("namespace", sr.Namespace) + + meta := map[string]interface{}{} + for k, v := range sr.Meta { + meta[k] = v + } + sw.set("meta", meta) + + routes := make([]map[string]interface{}, 0) + for _, route := range sr.Routes { + result := map[string]interface{}{} + + var http map[string]interface{} + + shouldSet := func() bool { + isEmpty := route.Match == nil || route.Match.HTTP == nil || (route.Match.HTTP.PathExact == "" && + route.Match.HTTP.PathPrefix == "" && + route.Match.HTTP.PathRegex == "" && + len(route.Match.HTTP.Header) == 0 && + len(route.Match.HTTP.QueryParam) == 0 && + len(route.Match.HTTP.Methods) == 0) + + if !isEmpty { + return true + } + + routes := d.Get("routes").([]interface{}) + if len(routes) == 0 { + return false + } + match := routes[0].(map[string]interface{})["match"].([]interface{}) + if len(match) == 0 { + return false + } + http := match[0].(map[string]interface{})["http"].([]interface{}) + if len(http) == 0 { + return false + } + if len(http[0].(map[string]interface{})["header"].([]interface{})) != 0 { + return true + } + if len(http[0].(map[string]interface{})["query_param"].([]interface{})) != 0 { + return true + } + + return false + } + + if shouldSet() { + http = map[string]interface{}{ + "path_exact": route.Match.HTTP.PathExact, + "path_prefix": route.Match.HTTP.PathPrefix, + "path_regex": route.Match.HTTP.PathRegex, + "methods": route.Match.HTTP.Methods, + } + + header := []interface{}{} + for _, h := range route.Match.HTTP.Header { + header = append(header, map[string]interface{}{ + "name": h.Name, + "present": h.Present, + "exact": h.Exact, + "prefix": h.Prefix, + "suffix": h.Suffix, + "regex": h.Regex, + "invert": h.Invert, + }) + } + http["header"] = header + + queryParam := []interface{}{} + for _, q := range route.Match.HTTP.QueryParam { + queryParam = append(queryParam, map[string]interface{}{ + "name": q.Name, + "present": q.Present, + "exact": q.Exact, + "regex": q.Regex, + }) + } + http["query_param"] = queryParam + + result["match"] = []interface{}{ + map[string]interface{}{ + "http": []interface{}{http}, + }, + } + } + + shouldSet = func() bool { + isEmpty := route.Destination == nil || (route.Destination.Service == "" && + route.Destination.ServiceSubset == "" && + route.Destination.Namespace == "" && + route.Destination.Partition == "" && + route.Destination.PrefixRewrite == "" && + route.Destination.RequestTimeout == 0 && + route.Destination.IdleTimeout == 0 && + route.Destination.NumRetries == 0 && + !route.Destination.RetryOnConnectFailure && + len(route.Destination.RetryOnStatusCodes) == 0 && + len(route.Destination.RetryOn) == 0 && (route.Destination.RequestHeaders == nil || len(route.Destination.RequestHeaders.Add)+len(route.Destination.RequestHeaders.Set)+len(route.Destination.RequestHeaders.Remove) == 0) && + route.Destination.ResponseHeaders == nil) + + if !isEmpty { + return true + } + + routes := d.Get("routes").([]interface{}) + if len(routes) == 0 { + return false + } + destination := routes[0].(map[string]interface{})["destination"].([]interface{}) + return len(destination) != 0 + } + + if shouldSet() { + destination := map[string]interface{}{ + "service": route.Destination.Service, + "service_subset": route.Destination.ServiceSubset, + "namespace": route.Destination.Namespace, + "partition": route.Destination.Partition, + "prefix_rewrite": route.Destination.PrefixRewrite, + "request_timeout": route.Destination.RequestTimeout.String(), + "idle_timeout": route.Destination.IdleTimeout.String(), + "num_retries": route.Destination.NumRetries, + "retry_on_connect_failure": route.Destination.RetryOnConnectFailure, + "retry_on": route.Destination.RetryOn, + "retry_on_status_codes": route.Destination.RetryOnStatusCodes, + } + + convertHeaders := func(headers *consulapi.HTTPHeaderModifiers) []interface{} { + if headers == nil { + return []interface{}{} + } + + add := map[string]interface{}{} + for k, v := range headers.Add { + add[k] = v + } + + set := map[string]interface{}{} + for k, v := range headers.Set { + set[k] = v + } + + remove := []interface{}{} + for _, v := range headers.Remove { + remove = append(remove, v) + } + + return []interface{}{ + map[string]interface{}{ + "add": add, + "set": set, + "remove": remove, + }, + } + } + destination["request_headers"] = convertHeaders(route.Destination.RequestHeaders) + result["destination"] = []interface{}{destination} + } + + routes = append(routes, result) + } + sw.set("routes", routes) + + return sw.error() +} diff --git a/consul/resource_consul_config_entry_service_router_ce_test.go b/consul/resource_consul_config_entry_service_router_ce_test.go new file mode 100644 index 00000000..c604142d --- /dev/null +++ b/consul/resource_consul_config_entry_service_router_ce_test.go @@ -0,0 +1,255 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccConsulConfigEntryServiceRouterCETest(t *testing.T) { + providers, _ := startTestServer(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipTestOnConsulEnterpriseEdition(t) }, + Providers: providers, + Steps: []resource.TestStep{ + { + Config: testConsulConfigEntryServiceRouterCE_Empty, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "0"), + ), + }, + { + Config: testConsulConfigEntryServiceRouterCE, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "meta.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.idle_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.num_retries", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.prefix_rewrite", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.response_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_connect_failure", "false"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_status_codes.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service", "admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service_subset", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.header.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.methods.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_exact", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_prefix", "/admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_regex", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.query_param.#", "0"), + ), + }, + { + Config: testConsulConfigEntryServiceRouterCE_noMatch, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "meta.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.idle_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.num_retries", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.prefix_rewrite", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.response_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_connect_failure", "false"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_status_codes.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service", "admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service_subset", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.#", "0"), + ), + }, + { + Config: testConsulConfigEntryServiceRouterCE_noDestination, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "meta.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.header.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.methods.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_exact", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_prefix", "/admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_regex", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.query_param.#", "0"), + ), + }, + }, + }) +} + +const ( + testConsulConfigEntryServiceRouterCE_Empty = ` + resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) + } + + resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) + } + + resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + } +` + + testConsulConfigEntryServiceRouterCE = ` +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + + routes { + match { + http { + path_prefix = "/admin" + } + } + + destination { + service = consul_config_entry.admin_service_defaults.name + } + } +}` + + testConsulConfigEntryServiceRouterCE_noMatch = ` +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + + routes { + destination { + service = consul_config_entry.admin_service_defaults.name + } + } +}` + + testConsulConfigEntryServiceRouterCE_noDestination = ` +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + + routes { + match { + http { + path_prefix = "/admin" + } + } + } +}` +) diff --git a/consul/resource_consul_config_entry_service_router_ee_test.go b/consul/resource_consul_config_entry_service_router_ee_test.go new file mode 100644 index 00000000..9f753746 --- /dev/null +++ b/consul/resource_consul_config_entry_service_router_ee_test.go @@ -0,0 +1,261 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccConsulConfigEntryServiceRouterEETest(t *testing.T) { + providers, _ := startTestServer(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipTestOnConsulCommunityEdition(t) }, + Providers: providers, + Steps: []resource.TestStep{ + { + Config: testConsulConfigEntryServiceRouterEE_Empty, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "0"), + ), + }, + { + Config: testConsulConfigEntryServiceRouterEE, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "meta.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", "ns1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", "pr1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.idle_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.num_retries", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.prefix_rewrite", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.response_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_connect_failure", "false"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_status_codes.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service", "admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service_subset", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.header.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.methods.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_exact", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_prefix", "/admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_regex", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.query_param.#", "0"), + ), + }, + { + Config: testConsulConfigEntryServiceRouterEE_noMatch, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "meta.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", "ns1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", "pr1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.idle_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.num_retries", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.prefix_rewrite", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.request_timeout", "0s"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.response_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_connect_failure", "false"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.retry_on_status_codes.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service", "admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.0.service_subset", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.#", "0"), + ), + }, + { + Config: testConsulConfigEntryServiceRouterEE_noDestination, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "meta.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "namespace", "ns1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "partition", "pr1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.destination.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.header.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.methods.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_exact", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_prefix", "/admin"), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.path_regex", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_router.foo", "routes.0.match.0.http.0.query_param.#", "0"), + ), + }, + }, + }) +} + +const ( + testConsulConfigEntryServiceRouterEE_Empty = ` + resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) + } + + resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) + } + + resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + } +` + + testConsulConfigEntryServiceRouterEE = ` +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + namespace = "ns1" + partition = "pr1" + + routes { + match { + http { + path_prefix = "/admin" + } + } + + destination { + service = consul_config_entry.admin_service_defaults.name + } + } +}` + + testConsulConfigEntryServiceRouterEE_noMatch = ` +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + namespace = "ns1" + partition = "pr1" + + routes { + destination { + service = consul_config_entry.admin_service_defaults.name + } + } +}` + + testConsulConfigEntryServiceRouterEE_noDestination = ` +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry" "admin_service_defaults" { + name = "admin" + kind = "service-defaults" + + config_json = jsonencode({ + Expose = {} + MeshGateway = {} + TransparentProxy = {} + Protocol = "http" + }) +} + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + namespace = "ns1" + partition = "pr1" + + routes { + match { + http { + path_prefix = "/admin" + } + } + } +}` +) diff --git a/consul/resource_provider.go b/consul/resource_provider.go index 0131ecd9..c6ac876e 100644 --- a/consul/resource_provider.go +++ b/consul/resource_provider.go @@ -241,6 +241,7 @@ func Provider() terraform.ResourceProvider { "consul_config_entry_service_defaults": resourceFromConfigEntryImplementation(&serviceDefaults{}), "consul_config_entry_service_intentions": resourceFromConfigEntryImplementation(&serviceIntentions{}), "consul_config_entry_service_resolver": resourceFromConfigEntryImplementation(&serviceResolver{}), + "consul_config_entry_service_router": resourceFromConfigEntryImplementation(&serviceRouter{}), "consul_config_entry_service_splitter": resourceFromConfigEntryImplementation(&serviceSplitter{}), "consul_config_entry": resourceConsulConfigEntry(), "consul_intention": resourceConsulIntention(), diff --git a/docs/resources/config_entry_service_defaults.md b/docs/resources/config_entry_service_defaults.md index 092c3f94..cbacac91 100644 --- a/docs/resources/config_entry_service_defaults.md +++ b/docs/resources/config_entry_service_defaults.md @@ -10,7 +10,36 @@ description: |- The `consul_config_entry_service_defaults` resource configures a [service defaults](https://developer.hashicorp.com/consul/docs/connect/config-entries/service-defaults) config entry that contains common configuration settings for service mesh services, such as upstreams and gateways. - +## Example Usage + +```terraform +resource "consul_config_entry_service_defaults" "dashboard" { + name = "dashboard" + + upstream_config { + defaults = { + mesh_gateway = { + mode = "local" + } + + limits = { + max_connections = 512 + max_pending_requests = 512 + max_concurrent_requests = 512 + } + } + + overrides { + name = "counting" + + mesh_gateway { + mode = "remote" + } + } + } + +} +``` ## Schema diff --git a/docs/resources/config_entry_service_intentions.md b/docs/resources/config_entry_service_intentions.md index b96082d2..2d814b1a 100644 --- a/docs/resources/config_entry_service_intentions.md +++ b/docs/resources/config_entry_service_intentions.md @@ -10,7 +10,52 @@ description: |- The `consul_service_intentions_config_entry` resource configures [service intentions](https://developer.hashicorp.com/consul/docs/connect/config-entries/service-intentions) that are configurations for controlling access between services in the service mesh. A single service intentions configuration entry specifies one destination service and one or more L4 traffic sources, L7 traffic sources, or combination of traffic sources. - +## Example Usage + +```terraform +resource "consul_config_entry" "jwt_provider" { + name = "okta" + kind = "jwt-provider" + + config_json = jsonencode({ + ClockSkewSeconds = 30 + Issuer = "test-issuer" + JSONWebKeySet = { + Remote = { + URI = "https://127.0.0.1:9091" + FetchAsynchronously = true + } + } + }) +} + +resource "consul_config_entry_service_intentions" "web" { + name = "web" + + jwt { + providers { + name = consul_config_entry.jwt_provider.name + + verify_claims { + path = ["perms", "role"] + value = "admin" + } + } + } + + sources { + name = "frontend-webapp" + type = "consul" + action = "allow" + } + + sources { + name = "nightly-cronjob" + type = "consul" + action = "deny" + } +} +``` ## Schema diff --git a/docs/resources/config_entry_service_resolver.md b/docs/resources/config_entry_service_resolver.md index 86985bc5..9c205a4b 100644 --- a/docs/resources/config_entry_service_resolver.md +++ b/docs/resources/config_entry_service_resolver.md @@ -10,7 +10,41 @@ description: |- The `consul_config_entry_service_resolver` resource configures a [service resolver](https://developer.hashicorp.com/consul/docs/connect/config-entries/service-resolver) that creates named subsets of service instances and define their behavior when satisfying upstream requests. - +## Example Usage + +```terraform +resource "consul_config_entry_service_resolver" "web" { + name = "web" + default_subset = "v1" + connect_timeout = "15s" + + subsets { + name = "v1" + filter = "Service.Meta.version == v1" + } + + subsets { + name = "v2" + Filter = "Service.Meta.version == v2" + } + + redirect { + service = "web" + datacenter = "dc2" + } + + failover { + subset_name = "v2" + datacenters = ["dc2"] + } + + failover { + subset_name = "*" + datacenters = ["dc3", "dc4"] + } + +} +``` ## Schema diff --git a/docs/resources/config_entry_service_router.md b/docs/resources/config_entry_service_router.md new file mode 100644 index 00000000..10535e56 --- /dev/null +++ b/docs/resources/config_entry_service_router.md @@ -0,0 +1,151 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "consul_config_entry_service_router Resource - terraform-provider-consul" +subcategory: "" +description: |- + The consul_config_entry_service_router resource configures a service router https://developer.hashicorp.com/consul/docs/connect/config-entries/service-router to redirect a traffic request for a service to one or more specific service instances. +--- + +# consul_config_entry_service_router (Resource) + +The `consul_config_entry_service_router` resource configures a [service router](https://developer.hashicorp.com/consul/docs/connect/config-entries/service-router) to redirect a traffic request for a service to one or more specific service instances. + +## Example Usage + +```terraform +resource "consul_config_entry_service_defaults" "admin_service_defaults" { + name = "web" + protocol = "http" +} + +resource "consul_config_entry_service_defaults" "admin_service_defaults" { + name = "dashboard" + protocol = "http" +} + + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + + routes { + match { + http { + path_prefix = "/admin" + } + } + + destination { + service = consul_config_entry.admin_service.name + } + } +} +``` + + +## Schema + +### Required + +- `name` (String) Specifies a name for the configuration entry. + +### Optional + +- `meta` (Map of String) Specifies key-value pairs to add to the KV store. +- `namespace` (String) Specifies the namespace to apply the configuration entry. +- `partition` (String) Specifies the admin partition to apply the configuration entry. +- `routes` (Block List) Defines the possible routes for L7 requests. (see [below for nested schema](#nestedblock--routes)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `routes` + +Optional: + +- `destination` (Block List, Max: 1) Specifies the target service to route matching requests to, as well as behavior for the request to follow when routed. (see [below for nested schema](#nestedblock--routes--destination)) +- `match` (Block List, Max: 1) Describes a set of criteria that Consul compares incoming L7 traffic with. (see [below for nested schema](#nestedblock--routes--match)) + + +### Nested Schema for `routes.destination` + +Optional: + +- `idle_timeout` (String) Specifies the total amount of time permitted for the request stream to be idle. +- `namespace` (String) Specifies the Consul namespace to resolve the service from instead of the current namespace. +- `num_retries` (Number) Specifies the number of times to retry the request when a retry condition occurs. +- `partition` (String) Specifies the Consul admin partition to resolve the service from instead of the current partition. +- `prefix_rewrite` (String) Specifies rewrites to the HTTP request path before proxying it to its final destination. +- `request_headers` (Block List, Max: 1) Specifies a set of HTTP-specific header modification rules applied to requests routed with the service router. (see [below for nested schema](#nestedblock--routes--destination--request_headers)) +- `request_timeout` (String) Specifies the total amount of time permitted for the entire downstream request to be processed, including retry attempts. +- `response_headers` (Block List, Max: 1) Specifies a set of HTTP-specific header modification rules applied to responses routed with the service router. (see [below for nested schema](#nestedblock--routes--destination--response_headers)) +- `retry_on` (List of String) Specifies a list of conditions for Consul to retry requests based on the response from an upstream service. +- `retry_on_connect_failure` (Boolean) Specifies that connection failure errors that trigger a retry request. +- `retry_on_status_codes` (List of Number) Specifies a list of integers for HTTP response status codes that trigger a retry request. +- `service` (String) Specifies the name of the service to resolve. +- `service_subset` (String) Specifies a named subset of the given service to resolve instead of the one defined as that service's `default_subset` in the service resolver configuration entry. + + +### Nested Schema for `routes.destination.request_headers` + +Optional: + +- `add` (Map of String) Defines a set of key-value pairs to add to the header. Use header names as the keys. +- `remove` (List of String) Defines a list of headers to remove. +- `set` (Map of String) Defines a set of key-value pairs to add to the request header or to replace existing header values with. + + + +### Nested Schema for `routes.destination.response_headers` + +Optional: + +- `add` (Map of String) Defines a set of key-value pairs to add to the header. Use header names as the keys +- `remove` (List of String) Defines a list of headers to remove. +- `set` (Map of String) Defines a set of key-value pairs to add to the response header or to replace existing header values with + + + + +### Nested Schema for `routes.match` + +Optional: + +- `http` (Block List, Max: 1) Specifies a set of HTTP criteria used to evaluate incoming L7 traffic for matches. (see [below for nested schema](#nestedblock--routes--match--http)) + + +### Nested Schema for `routes.match.http` + +Optional: + +- `header` (Block List) Specifies information in the HTTP request header to match with. (see [below for nested schema](#nestedblock--routes--match--http--header)) +- `methods` (List of String) Specifies HTTP methods that the match applies to. +- `path_exact` (String) Specifies the exact path to match on the HTTP request path. +- `path_prefix` (String) Specifies the path prefix to match on the HTTP request path. +- `path_regex` (String) Specifies a regular expression to match on the HTTP request path. +- `query_param` (Block List) Specifies information to match to on HTTP query parameters. (see [below for nested schema](#nestedblock--routes--match--http--query_param)) + + +### Nested Schema for `routes.match.http.header` + +Optional: + +- `exact` (String) Specifies that a request matches when the header with the given name is this exact value. +- `invert` (Boolean) Specifies that the logic for the HTTP header match should be inverted. +- `name` (String) Specifies the name of the HTTP header to match. +- `prefix` (String) Specifies that a request matches when the header with the given name has this prefix. +- `present` (Boolean) Specifies that a request matches when the value in the `name` argument is present anywhere in the HTTP header. +- `regex` (String) Specifies that a request matches when the header with the given name matches this regular expression. +- `suffix` (String) Specifies that a request matches when the header with the given name has this suffix. + + + +### Nested Schema for `routes.match.http.query_param` + +Optional: + +- `exact` (String) Specifies that a request matches when the query parameter with the given name is this exact value. +- `name` (String) Specifies the name of the HTTP query parameter to match. +- `present` (Boolean) Specifies that a request matches when the value in the `name` argument is present anywhere in the HTTP query parameter. +- `regex` (String) Specifies that a request matches when the query parameter with the given name matches this regular expression. diff --git a/docs/resources/config_entry_service_splitter.md b/docs/resources/config_entry_service_splitter.md index 1da77b2a..e760a0f6 100644 --- a/docs/resources/config_entry_service_splitter.md +++ b/docs/resources/config_entry_service_splitter.md @@ -25,26 +25,23 @@ resource "consul_config_entry" "web" { }) } -resource "consul_config_entry" "service_resolver" { - kind = "service-resolver" - name = consul_config_entry.web.name +resource "consul_config_entry_service_resolver" "service_resolver" { + name = "service-resolver" + default_subset = "v1" - config_json = jsonencode({ - DefaultSubset = "v1" + subsets { + name = "v1" + filter = "Service.Meta.version == v1" + } - Subsets = { - "v1" = { - Filter = "Service.Meta.version == v1" - } - "v2" = { - Filter = "Service.Meta.version == v2" - } - } - }) + subsets { + name = "v2" + Filter = "Service.Meta.version == v2" + } } resource "consul_config_entry_service_splitter" "foo" { - name = consul_config_entry.service_resolver.name + name = consul_config_entry_service_resolver.service_resolver.name meta = { key = "value" diff --git a/examples/resources/consul_config_entry_service_defaults/resource.tf b/examples/resources/consul_config_entry_service_defaults/resource.tf new file mode 100644 index 00000000..07806402 --- /dev/null +++ b/examples/resources/consul_config_entry_service_defaults/resource.tf @@ -0,0 +1,26 @@ +resource "consul_config_entry_service_defaults" "dashboard" { + name = "dashboard" + + upstream_config { + defaults = { + mesh_gateway = { + mode = "local" + } + + limits = { + max_connections = 512 + max_pending_requests = 512 + max_concurrent_requests = 512 + } + } + + overrides { + name = "counting" + + mesh_gateway { + mode = "remote" + } + } + } + +} diff --git a/examples/resources/consul_config_entry_service_intentions/resource.tf b/examples/resources/consul_config_entry_service_intentions/resource.tf new file mode 100644 index 00000000..f0ee1e7b --- /dev/null +++ b/examples/resources/consul_config_entry_service_intentions/resource.tf @@ -0,0 +1,42 @@ +resource "consul_config_entry" "jwt_provider" { + name = "okta" + kind = "jwt-provider" + + config_json = jsonencode({ + ClockSkewSeconds = 30 + Issuer = "test-issuer" + JSONWebKeySet = { + Remote = { + URI = "https://127.0.0.1:9091" + FetchAsynchronously = true + } + } + }) +} + +resource "consul_config_entry_service_intentions" "web" { + name = "web" + + jwt { + providers { + name = consul_config_entry.jwt_provider.name + + verify_claims { + path = ["perms", "role"] + value = "admin" + } + } + } + + sources { + name = "frontend-webapp" + type = "consul" + action = "allow" + } + + sources { + name = "nightly-cronjob" + type = "consul" + action = "deny" + } +} diff --git a/examples/resources/consul_config_entry_service_resolver/resource.tf b/examples/resources/consul_config_entry_service_resolver/resource.tf new file mode 100644 index 00000000..211134ad --- /dev/null +++ b/examples/resources/consul_config_entry_service_resolver/resource.tf @@ -0,0 +1,31 @@ +resource "consul_config_entry_service_resolver" "web" { + name = "web" + default_subset = "v1" + connect_timeout = "15s" + + subsets { + name = "v1" + filter = "Service.Meta.version == v1" + } + + subsets { + name = "v2" + Filter = "Service.Meta.version == v2" + } + + redirect { + service = "web" + datacenter = "dc2" + } + + failover { + subset_name = "v2" + datacenters = ["dc2"] + } + + failover { + subset_name = "*" + datacenters = ["dc3", "dc4"] + } + +} diff --git a/examples/resources/consul_config_entry_service_router/resource.tf b/examples/resources/consul_config_entry_service_router/resource.tf new file mode 100644 index 00000000..3edc84b4 --- /dev/null +++ b/examples/resources/consul_config_entry_service_router/resource.tf @@ -0,0 +1,26 @@ +resource "consul_config_entry_service_defaults" "admin_service_defaults" { + name = "web" + protocol = "http" +} + +resource "consul_config_entry_service_defaults" "admin_service_defaults" { + name = "dashboard" + protocol = "http" +} + + +resource "consul_config_entry_service_router" "foo" { + name = consul_config_entry.web.name + + routes { + match { + http { + path_prefix = "/admin" + } + } + + destination { + service = consul_config_entry.admin_service.name + } + } +} diff --git a/examples/resources/consul_config_entry_service_splitter/resource.tf b/examples/resources/consul_config_entry_service_splitter/resource.tf index ff41c299..1d7fed3a 100644 --- a/examples/resources/consul_config_entry_service_splitter/resource.tf +++ b/examples/resources/consul_config_entry_service_splitter/resource.tf @@ -10,26 +10,23 @@ resource "consul_config_entry" "web" { }) } -resource "consul_config_entry" "service_resolver" { - kind = "service-resolver" - name = consul_config_entry.web.name +resource "consul_config_entry_service_resolver" "service_resolver" { + name = "service-resolver" + default_subset = "v1" - config_json = jsonencode({ - DefaultSubset = "v1" + subsets { + name = "v1" + filter = "Service.Meta.version == v1" + } - Subsets = { - "v1" = { - Filter = "Service.Meta.version == v1" - } - "v2" = { - Filter = "Service.Meta.version == v2" - } - } - }) + subsets { + name = "v2" + Filter = "Service.Meta.version == v2" + } } resource "consul_config_entry_service_splitter" "foo" { - name = consul_config_entry.service_resolver.name + name = consul_config_entry_service_resolver.service_resolver.name meta = { key = "value"