From cb7f9af6c6b03864668d634bde67970cb7be2a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Tue, 24 Oct 2023 22:41:14 +0200 Subject: [PATCH] Implement consul_config_entry_service_splitter (#364) --- .../resource_consul_config_entry_concrete.go | 129 +++++++++ ...ce_consul_config_entry_service_splitter.go | 270 ++++++++++++++++++ ...nsul_config_entry_service_splitter_test.go | 173 +++++++++++ consul/resource_provider.go | 55 ++-- .../config_entry_service_splitter.md | 156 ++++++++++ .../import.sh | 1 + 6 files changed, 757 insertions(+), 27 deletions(-) create mode 100644 consul/resource_consul_config_entry_concrete.go create mode 100644 consul/resource_consul_config_entry_service_splitter.go create mode 100644 consul/resource_consul_config_entry_service_splitter_test.go create mode 100644 docs/resources/config_entry_service_splitter.md create mode 100644 examples/resources/consul_config_entry_service_splitter/import.sh diff --git a/consul/resource_consul_config_entry_concrete.go b/consul/resource_consul_config_entry_concrete.go new file mode 100644 index 00000000..ad4707ea --- /dev/null +++ b/consul/resource_consul_config_entry_concrete.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "fmt" + "strings" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +// ConfigEntryImplementation is the common implementation for all specific +// config entries. +type ConfigEntryImplementation interface { + GetKind() string + GetDescription() string + GetSchema() map[string]*schema.Schema + Decode(d *schema.ResourceData) (consulapi.ConfigEntry, error) + Write(ce consulapi.ConfigEntry, d *schema.ResourceData, sw *stateWriter) error +} + +func resourceFromConfigEntryImplementation(c ConfigEntryImplementation) *schema.Resource { + return &schema.Resource{ + Description: c.GetDescription(), + Schema: c.GetSchema(), + Create: configEntryImplementationWrite(c), + Update: configEntryImplementationWrite(c), + Read: configEntryImplementationRead(c), + Delete: configEntryImplementationDelete(c), + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + var name, partition, namespace string + switch len(parts) { + case 1: + name = parts[0] + case 3: + partition = parts[0] + namespace = parts[1] + name = parts[2] + default: + return nil, fmt.Errorf(`expected path of the form "" or "//"`) + } + + d.SetId(name) + sw := newStateWriter(d) + sw.set("name", name) + sw.set("partition", partition) + sw.set("namespace", namespace) + + err := sw.error() + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil + }, + }, + } +} + +func configEntryImplementationWrite(impl ConfigEntryImplementation) func(d *schema.ResourceData, meta interface{}) error { + return func(d *schema.ResourceData, meta interface{}) error { + client, qOpts, wOpts := getClient(d, meta) + + configEntry, err := impl.Decode(d) + if err != nil { + return err + } + + if _, _, err := client.ConfigEntries().Set(configEntry, wOpts); err != nil { + return fmt.Errorf("failed to set '%s' config entry: %v", configEntry.GetName(), err) + } + _, _, err = client.ConfigEntries().Get(configEntry.GetKind(), configEntry.GetName(), qOpts) + if err != nil { + if strings.Contains(err.Error(), "Unexpected response code: 404") { + return fmt.Errorf("failed to read config entry after setting it") + } + return fmt.Errorf("failed to read config entry: %v", err) + } + + d.SetId(configEntry.GetName()) + return configEntryImplementationRead(impl)(d, meta) + } +} + +func configEntryImplementationRead(impl ConfigEntryImplementation) func(d *schema.ResourceData, meta interface{}) error { + return func(d *schema.ResourceData, meta interface{}) error { + client, qOpts, _ := getClient(d, meta) + name := d.Get("name").(string) + + fixQOptsForConfigEntry(name, impl.GetKind(), qOpts) + + ce, _, err := client.ConfigEntries().Get(impl.GetKind(), name, qOpts) + if err != nil { + if strings.Contains(err.Error(), "Unexpected response code: 404") { + // The config entry has been removed + d.SetId("") + return nil + } + return fmt.Errorf("failed to fetch '%s' config entry: %v", name, err) + } + if ce == nil { + d.SetId("") + return nil + } + + sw := newStateWriter(d) + if err := impl.Write(ce, d, sw); err != nil { + return err + } + return sw.error() + } +} + +func configEntryImplementationDelete(impl ConfigEntryImplementation) func(d *schema.ResourceData, meta interface{}) error { + return func(d *schema.ResourceData, meta interface{}) error { + client, _, wOpts := getClient(d, meta) + name := d.Get("name").(string) + + if _, err := client.ConfigEntries().Delete(impl.GetKind(), name, wOpts); err != nil { + return fmt.Errorf("failed to delete '%s' config entry: %v", name, err) + } + d.SetId("") + return nil + } +} diff --git a/consul/resource_consul_config_entry_service_splitter.go b/consul/resource_consul_config_entry_service_splitter.go new file mode 100644 index 00000000..e18724a4 --- /dev/null +++ b/consul/resource_consul_config_entry_service_splitter.go @@ -0,0 +1,270 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "fmt" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +type serviceSplitter struct{} + +func (s *serviceSplitter) GetKind() string { + return consulapi.ServiceSplitter +} + +func (s *serviceSplitter) GetDescription() string { + return "The `consul_config_entry_service_splitter` resource configures a [service splitter](https://developer.hashicorp.com/consul/docs/connect/config-entries/service-splitter) that will redirect a percentage of incoming traffic requests for a service to one or more specific service instances." +} + +func (s *serviceSplitter) 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}, + }, + "splits": { + Type: schema.TypeList, + Description: "Defines how much traffic to send to sets of service instances during a traffic split.", + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "weight": { + Type: schema.TypeFloat, + Description: "Specifies the percentage of traffic sent to the set of service instances specified in the `service` field. Each weight must be a floating integer between `0` and `100`. The smallest representable value is `.01`. The sum of weights across all splits must add up to `100`.", + Required: true, + }, + "service": { + Type: schema.TypeString, + Description: "Specifies the name of the service to resolve.", + Required: true, + }, + "service_subset": { + Type: schema.TypeString, + Description: "Specifies a subset of the service to resolve. A service subset assigns a name to a specific subset of discoverable service instances within a datacenter, such as `version2` or `canary`. All services have an unnamed default subset that returns all healthy instances.", + Optional: true, + }, + "namespace": { + Type: schema.TypeString, + Description: "Specifies the namespace to use in the FQDN when resolving the service.", + Optional: true, + }, + "partition": { + Type: schema.TypeString, + Description: "Specifies the admin partition to use in the FQDN when resolving the service.", + Optional: true, + }, + "request_headers": { + Type: schema.TypeList, + MaxItems: 1, + Description: "Specifies a set of HTTP-specific header modification rules applied to requests routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`.", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "add": { + Type: schema.TypeMap, + Description: "Map of one or more key-value pairs. Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "set": { + Type: schema.TypeMap, + Description: "Map of one or more key-value pairs. Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "remove": { + Type: schema.TypeList, + Description: "Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "response_headers": { + Type: schema.TypeList, + MaxItems: 1, + Description: "Specifies a set of HTTP-specific header modification rules applied to responses routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`.", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "add": { + Type: schema.TypeMap, + Description: "Map of one or more key-value pairs. Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "set": { + Type: schema.TypeMap, + Description: "Map of one or more key-value pairs. Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "remove": { + Type: schema.TypeList, + Description: "Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (s *serviceSplitter) Decode(d *schema.ResourceData) (consulapi.ConfigEntry, error) { + configEntry := &consulapi.ServiceSplitterConfigEntry{ + Kind: consulapi.ServiceSplitter, + Name: d.Get("name").(string), + Namespace: d.Get("namespace").(string), + Partition: d.Get("partition").(string), + Meta: map[string]string{}, + } + + for k, v := range d.Get("meta").(map[string]interface{}) { + configEntry.Meta[k] = v.(string) + } + + for _, raw := range d.Get("splits").([]interface{}) { + s := raw.(map[string]interface{}) + split := consulapi.ServiceSplit{ + Weight: float32(s["weight"].(float64)), + Service: s["service"].(string), + ServiceSubset: s["service_subset"].(string), + Namespace: s["namespace"].(string), + Partition: s["partition"].(string), + RequestHeaders: &consulapi.HTTPHeaderModifiers{ + Add: map[string]string{}, + Set: map[string]string{}, + }, + ResponseHeaders: &consulapi.HTTPHeaderModifiers{ + Add: map[string]string{}, + Set: map[string]string{}, + }, + } + + addHeaders := func(modifier *consulapi.HTTPHeaderModifiers, path string) { + elems := s[path].([]interface{}) + if len(elems) == 0 { + return + } + + headers := elems[0].(map[string]interface{}) + for k, v := range headers["add"].(map[string]interface{}) { + modifier.Add[k] = v.(string) + } + for k, v := range headers["set"].(map[string]interface{}) { + modifier.Set[k] = v.(string) + } + for _, v := range headers["remove"].([]interface{}) { + modifier.Remove = append(modifier.Remove, v.(string)) + } + } + addHeaders(split.RequestHeaders, "request_headers") + addHeaders(split.ResponseHeaders, "response_headers") + + configEntry.Splits = append(configEntry.Splits, split) + } + + return configEntry, nil +} + +func (s *serviceSplitter) Write(ce consulapi.ConfigEntry, d *schema.ResourceData, sw *stateWriter) error { + sp, ok := ce.(*consulapi.ServiceSplitterConfigEntry) + if !ok { + return fmt.Errorf("expected '%s' but got '%s'", consulapi.ServiceSplitter, ce.GetKind()) + } + + sw.set("name", sp.Name) + sw.set("partition", sp.Partition) + sw.set("namespace", sp.Namespace) + + meta := map[string]interface{}{} + for k, v := range sp.Meta { + meta[k] = v + } + sw.set("meta", meta) + + splits := make([]interface{}, 0) + for i, s := range sp.Splits { + split := map[string]interface{}{ + "weight": s.Weight, + "service": s.Service, + "service_subset": s.ServiceSubset, + "namespace": s.Namespace, + "partition": s.Partition, + } + addHeaders := func(modifier *consulapi.HTTPHeaderModifiers, path string) { + headers := map[string]interface{}{} + + shouldSet := func() bool { + splits := d.Get("splits").([]interface{}) + if len(splits) <= i { + return true + } + if _, found := splits[i].(map[string]interface{})[path]; !found { + return false + } + + return len(splits[i].(map[string]interface{})[path].([]interface{})) != 0 + }() + + if !shouldSet && len(modifier.Add)+len(modifier.Set)+len(modifier.Remove) == 0 { + return + } + + add := map[string]interface{}{} + for k, v := range modifier.Add { + add[k] = v + } + headers["add"] = add + + set := map[string]interface{}{} + for k, v := range modifier.Set { + set[k] = v + } + headers["set"] = set + + var remove []interface{} + for _, v := range modifier.Remove { + remove = append(remove, v) + } + headers["remove"] = remove + + split[path] = []interface{}{headers} + } + addHeaders(s.RequestHeaders, "request_headers") + addHeaders(s.ResponseHeaders, "response_headers") + splits = append(splits, split) + } + sw.set("splits", splits) + + return sw.error() +} diff --git a/consul/resource_consul_config_entry_service_splitter_test.go b/consul/resource_consul_config_entry_service_splitter_test.go new file mode 100644 index 00000000..21c1f300 --- /dev/null +++ b/consul/resource_consul_config_entry_service_splitter_test.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccConsulConfigEntryServiceSplitterTest(t *testing.T) { + providers, _ := startTestServer(t) + + var config string + if serverIsConsulCommunityEdition(t) { + config = testConsulConfigEntryServiceSplitter("", "") + } else { + config = testConsulConfigEntryServiceSplitter("default", "default") + } + + resource.Test(t, resource.TestCase{ + Providers: providers, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "id", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "meta.%", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "meta.key", "value"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "name", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.#", "3"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.request_headers.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.request_headers.0.add.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.request_headers.0.remove.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.request_headers.0.set.%", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.request_headers.0.set.x-web-version", "from-v1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.response_headers.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.response_headers.0.add.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.response_headers.0.remove.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.response_headers.0.set.%", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.response_headers.0.set.x-web-version", "to-v1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.service", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.service_subset", "v1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.0.weight", "80"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.request_headers.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.request_headers.0.add.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.request_headers.0.remove.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.request_headers.0.set.%", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.request_headers.0.set.x-web-version", "from-v2"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.response_headers.#", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.response_headers.0.add.%", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.response_headers.0.remove.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.response_headers.0.set.%", "1"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.response_headers.0.set.x-web-version", "to-v2"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.service", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.service_subset", "v2"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.1.weight", "10"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.2.namespace", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.2.partition", ""), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.2.request_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.2.response_headers.#", "0"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.2.service", "web"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.2.service_subset", "v2"), + resource.TestCheckResourceAttr("consul_config_entry_service_splitter.foo", "splits.2.weight", "10"), + ), + }, + { + Config: config, + ResourceName: "consul_config_entry_service_splitter.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "splits.2.request_headers.#", + "splits.2.response_headers.#", + }, + }, + }, + }) +} + +func testConsulConfigEntryServiceSplitter(namespace, partition string) string { + return fmt.Sprintf(` +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Protocol = "http" + Expose = {} + MeshGateway = {} + TransparentProxy = {} + }) +} + +resource "consul_config_entry" "service_resolver" { + kind = "service-resolver" + name = consul_config_entry.web.name + + config_json = jsonencode({ + DefaultSubset = "v1" + + Subsets = { + "v1" = { + Filter = "Service.Meta.version == v1" + } + "v2" = { + Filter = "Service.Meta.version == v2" + } + } + }) +} + +resource "consul_config_entry_service_splitter" "foo" { + name = consul_config_entry.service_resolver.name + namespace = "%s" + partition = "%s" + + meta = { + key = "value" + } + + splits { + weight = 80 + service = "web" + service_subset = "v1" + + request_headers { + set = { + "x-web-version" = "from-v1" + } + } + + response_headers { + set = { + "x-web-version" = "to-v1" + } + } + } + + splits { + weight = 10 + service = "web" + service_subset = "v2" + + request_headers { + set = { + "x-web-version" = "from-v2" + } + } + + response_headers { + set = { + "x-web-version" = "to-v2" + } + } + } + + splits { + weight = 10 + service = "web" + service_subset = "v2" + } +} +`, namespace, partition) +} diff --git a/consul/resource_provider.go b/consul/resource_provider.go index 3f3f1720..1b4ca267 100644 --- a/consul/resource_provider.go +++ b/consul/resource_provider.go @@ -225,33 +225,34 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "consul_acl_auth_method": resourceConsulACLAuthMethod(), - "consul_acl_binding_rule": resourceConsulACLBindingRule(), - "consul_acl_policy": resourceConsulACLPolicy(), - "consul_acl_role_policy_attachment": resourceConsulACLRolePolicyAttachment(), - "consul_acl_role": resourceConsulACLRole(), - "consul_acl_token_policy_attachment": resourceConsulACLTokenPolicyAttachment(), - "consul_acl_token_role_attachment": resourceConsulACLTokenRoleAttachment(), - "consul_acl_token": resourceConsulACLToken(), - "consul_admin_partition": resourceConsulAdminPartition(), - "consul_agent_service": resourceConsulAgentService(), - "consul_autopilot_config": resourceConsulAutopilotConfig(), - "consul_catalog_entry": resourceConsulCatalogEntry(), - "consul_certificate_authority": resourceConsulCertificateAuthority(), - "consul_config_entry": resourceConsulConfigEntry(), - "consul_intention": resourceConsulIntention(), - "consul_key_prefix": resourceConsulKeyPrefix(), - "consul_keys": resourceConsulKeys(), - "consul_license": resourceConsulLicense(), - "consul_namespace_policy_attachment": resourceConsulNamespacePolicyAttachment(), - "consul_namespace_role_attachment": resourceConsulNamespaceRoleAttachment(), - "consul_namespace": resourceConsulNamespace(), - "consul_network_area": resourceConsulNetworkArea(), - "consul_node": resourceConsulNode(), - "consul_peering_token": resourceSourceConsulPeeringToken(), - "consul_peering": resourceSourceConsulPeering(), - "consul_prepared_query": resourceConsulPreparedQuery(), - "consul_service": resourceConsulService(), + "consul_acl_auth_method": resourceConsulACLAuthMethod(), + "consul_acl_binding_rule": resourceConsulACLBindingRule(), + "consul_acl_policy": resourceConsulACLPolicy(), + "consul_acl_role_policy_attachment": resourceConsulACLRolePolicyAttachment(), + "consul_acl_role": resourceConsulACLRole(), + "consul_acl_token_policy_attachment": resourceConsulACLTokenPolicyAttachment(), + "consul_acl_token_role_attachment": resourceConsulACLTokenRoleAttachment(), + "consul_acl_token": resourceConsulACLToken(), + "consul_admin_partition": resourceConsulAdminPartition(), + "consul_agent_service": resourceConsulAgentService(), + "consul_autopilot_config": resourceConsulAutopilotConfig(), + "consul_catalog_entry": resourceConsulCatalogEntry(), + "consul_certificate_authority": resourceConsulCertificateAuthority(), + "consul_config_entry_service_splitter": resourceFromConfigEntryImplementation(&serviceSplitter{}), + "consul_config_entry": resourceConsulConfigEntry(), + "consul_intention": resourceConsulIntention(), + "consul_key_prefix": resourceConsulKeyPrefix(), + "consul_keys": resourceConsulKeys(), + "consul_license": resourceConsulLicense(), + "consul_namespace_policy_attachment": resourceConsulNamespacePolicyAttachment(), + "consul_namespace_role_attachment": resourceConsulNamespaceRoleAttachment(), + "consul_namespace": resourceConsulNamespace(), + "consul_network_area": resourceConsulNetworkArea(), + "consul_node": resourceConsulNode(), + "consul_peering_token": resourceSourceConsulPeeringToken(), + "consul_peering": resourceSourceConsulPeering(), + "consul_prepared_query": resourceConsulPreparedQuery(), + "consul_service": resourceConsulService(), }, ConfigureFunc: providerConfigure, diff --git a/docs/resources/config_entry_service_splitter.md b/docs/resources/config_entry_service_splitter.md new file mode 100644 index 00000000..1da77b2a --- /dev/null +++ b/docs/resources/config_entry_service_splitter.md @@ -0,0 +1,156 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "consul_config_entry_service_splitter Resource - terraform-provider-consul" +subcategory: "" +description: |- + The consul_config_entry_service_splitter resource configures a service splitter https://developer.hashicorp.com/consul/docs/connect/config-entries/service-splitter that will redirect a percentage of incoming traffic requests for a service to one or more specific service instances. +--- + +# consul_config_entry_service_splitter (Resource) + +The `consul_config_entry_service_splitter` resource configures a [service splitter](https://developer.hashicorp.com/consul/docs/connect/config-entries/service-splitter) that will redirect a percentage of incoming traffic requests for a service to one or more specific service instances. + +## Example Usage + +```terraform +resource "consul_config_entry" "web" { + name = "web" + kind = "service-defaults" + + config_json = jsonencode({ + Protocol = "http" + Expose = {} + MeshGateway = {} + TransparentProxy = {} + }) +} + +resource "consul_config_entry" "service_resolver" { + kind = "service-resolver" + name = consul_config_entry.web.name + + config_json = jsonencode({ + DefaultSubset = "v1" + + Subsets = { + "v1" = { + Filter = "Service.Meta.version == v1" + } + "v2" = { + Filter = "Service.Meta.version == v2" + } + } + }) +} + +resource "consul_config_entry_service_splitter" "foo" { + name = consul_config_entry.service_resolver.name + + meta = { + key = "value" + } + + splits { + weight = 80 + service = "web" + service_subset = "v1" + + request_headers { + set = { + "x-web-version" = "from-v1" + } + } + + response_headers { + set = { + "x-web-version" = "to-v1" + } + } + } + + splits { + weight = 10 + service = "web" + service_subset = "v2" + + request_headers { + set = { + "x-web-version" = "from-v2" + } + } + + response_headers { + set = { + "x-web-version" = "to-v2" + } + } + } + + splits { + weight = 10 + service = "web" + service_subset = "v2" + } +} +``` + + +## Schema + +### Required + +- `name` (String) Specifies a name for the configuration entry. +- `splits` (Block List, Min: 1) Defines how much traffic to send to sets of service instances during a traffic split. (see [below for nested schema](#nestedblock--splits)) + +### 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. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `splits` + +Required: + +- `service` (String) Specifies the name of the service to resolve. +- `weight` (Number) Specifies the percentage of traffic sent to the set of service instances specified in the `service` field. Each weight must be a floating integer between `0` and `100`. The smallest representable value is `.01`. The sum of weights across all splits must add up to `100`. + +Optional: + +- `namespace` (String) Specifies the namespace to use in the FQDN when resolving the service. +- `partition` (String) Specifies the admin partition to use in the FQDN when resolving the service. +- `request_headers` (Block List, Max: 1) Specifies a set of HTTP-specific header modification rules applied to requests routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`. (see [below for nested schema](#nestedblock--splits--request_headers)) +- `response_headers` (Block List, Max: 1) Specifies a set of HTTP-specific header modification rules applied to responses routed with the service split. You cannot configure request headers if the listener protocol is set to `tcp`. (see [below for nested schema](#nestedblock--splits--response_headers)) +- `service_subset` (String) Specifies a subset of the service to resolve. A service subset assigns a name to a specific subset of discoverable service instances within a datacenter, such as `version2` or `canary`. All services have an unnamed default subset that returns all healthy instances. + + +### Nested Schema for `splits.request_headers` + +Optional: + +- `add` (Map of String) Map of one or more key-value pairs. Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers. +- `remove` (List of String) Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive. +- `set` (Map of String) Map of one or more key-value pairs. Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values. + + + +### Nested Schema for `splits.response_headers` + +Optional: + +- `add` (Map of String) Map of one or more key-value pairs. Defines a set of key-value pairs to add to the header. Use header names as the keys. Header names are not case-sensitive. If header values with the same name already exist, the value is appended and Consul applies both headers. +- `remove` (List of String) Defines an list of headers to remove. Consul removes only headers containing exact matches. Header names are not case-sensitive. +- `set` (Map of String) Map of one or more key-value pairs. Defines a set of key-value pairs to add to the request header or to replace existing header values with. Use header names as the keys. Header names are not case-sensitive. If header values with the same names already exist, Consul replaces the header values. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import consul_config_entry_service_splitter.foo web +``` diff --git a/examples/resources/consul_config_entry_service_splitter/import.sh b/examples/resources/consul_config_entry_service_splitter/import.sh new file mode 100644 index 00000000..2655436b --- /dev/null +++ b/examples/resources/consul_config_entry_service_splitter/import.sh @@ -0,0 +1 @@ +terraform import consul_config_entry_service_splitter.foo web