From 0dfdeec17e72caee1ab3dda58261c5324d168208 Mon Sep 17 00:00:00 2001 From: Tyler Caslin Date: Wed, 28 Sep 2022 11:56:34 -0400 Subject: [PATCH] Add support for LB adaptive_routing, location_strategy, random_steering, and zero_downtime_failover --- .changelog/1941.txt | 3 + docs/resources/load_balancer.md | 21 ++ .../resource_cloudflare_load_balancer.go | 248 +++++++++++++++++- .../resource_cloudflare_load_balancer_test.go | 164 ++++++++++++ .../schema_cloudflare_load_balancer.go | 18 ++ templates/resources/load_balancer.md | 21 ++ 6 files changed, 472 insertions(+), 3 deletions(-) create mode 100644 .changelog/1941.txt diff --git a/.changelog/1941.txt b/.changelog/1941.txt new file mode 100644 index 00000000000..329643be06d --- /dev/null +++ b/.changelog/1941.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/cloudflare_load_balancer: Add support for adaptive_routing, location_strategy, random_steering, and zero_downtime_failover +``` diff --git a/docs/resources/load_balancer.md b/docs/resources/load_balancer.md index bb41f642e69..ccc614e1f12 100644 --- a/docs/resources/load_balancer.md +++ b/docs/resources/load_balancer.md @@ -79,6 +79,9 @@ The following arguments are supported: - `session_affinity` - (Optional) Associates all requests coming from an end-user with a single origin. Cloudflare will set a cookie on the initial response to the client, such that consequent requests with the cookie in the request will go to the same origin, so long as it is available. Valid values are: `""`, `"none"`, `"cookie"`, and `"ip_cookie"`. Default is `""`. - `session_affinity_ttl` - (Optional) Time, in seconds, until this load balancers session affinity cookie expires after being created. This parameter is ignored unless a supported session affinity policy is set. The current default of 23 hours will be used unless `session_affinity_ttl` is explicitly set. Once the expiry time has been reached, subsequent requests may get sent to a different origin server. Valid values are between 1800 and 604800. - `session_affinity_attributes` - (Optional) Configure cookie attributes for session affinity cookie. See the field documentation below. +- `adaptive_routing` - (Optional) Controls features that modify the routing of requests to pools and origins in response to dynamic conditions, such as during the interval between active health monitoring requests. See the field documentation below. +- `location_strategy` - (Optional) Controls location-based steering for non-proxied requests. See the field documentation below. +- `random_steering` - (Optional) Configures pool weights for random steering. When the `steering_policy="random"`, a random pool is selected with probability proportional to these pool weights. See the field documentation below. - `rules` - (Optional) A list of conditions and overrides for each load balancer operation. See the field documentation below. **region_pools** requires the following: @@ -101,6 +104,21 @@ The following arguments are supported: - `samesite` - (Optional) Configures the SameSite attribute on session affinity cookie. Value "Auto" will be translated to "Lax" or "None" depending if Always Use HTTPS is enabled. Note: when using value "None", the secure attribute can not be set to "Never". Valid values: `"Auto"`, `"Lax"`, `"None"` or `"Strict"`. - `secure` - (Optional) Configures the Secure attribute on session affinity cookie. Value "Always" indicates the Secure attribute will be set in the Set-Cookie header, "Never" indicates the Secure attribute will not be set, and "Auto" will set the Secure attribute depending if Always Use HTTPS is enabled. Valid values: `"Auto"`, `"Always"` or `"Never"`. - `drain_duration` - (Optional) Configures the drain duration in seconds. This field is only used when session affinity is enabled on the load balancer. +- `zero_downtime_failover` - (Optional) Configures the zero-downtime failover between origins within a pool when session affinity is enabled. Value "none" means no failover takes place for sessions pinned to the origin. Value "temporary" means traffic will be sent to another other healthy origin until the originally pinned origin is available; note that this can potentially result in heavy origin flapping. Value "sticky" means the session affinity cookie is updated and subsequent requests are sent to the new origin. This feature is currently incompatible with Argo, Tiered Cache, and Bandwidth Alliance. Valid values: `"none"`, `"temporary"` or `"sticky"`. Default is `"none"`. + +**adaptive_routing** optionally as the following: + +- `failover_across_pools` - (Optional) Extends zero-downtime failover of requests to healthy origins from alternate pools, when no healthy alternate exists in the same pool, according to the failover order defined by traffic and origin steering. When set `false` (the default) zero-downtime failover will only occur between origins within the same pool. + +**location_strategy** optionally as the following: + +- `prefer_ecs` - (Optional) Whether the EDNS Client Subnet (ECS) GeoIP should be preferred as the authoritative location. Value "always" will always prefer ECS, "never" will never prefer ECS, "proximity" will prefer ECS only when `steering_policy="proximity"`, and "geo" will prefer ECS only when `steering_policy="geo"`. Valid values: `"always"`, `"never"`, `"proximity"`, `"geo"` or `""` for the default (`"proximity"`). +- `mode` - (Optional) Determines the authoritative location when ECS is not preferred, does not exist in the request, or its GeoIP lookup is unsuccessful. Value "pop" will use the Cloudflare PoP location. Value "resolver_ip" will use the DNS resolver GeoIP location. If the GeoIP lookup is unsuccessful, it will use the Cloudflare PoP location. Valid values: `"pop"`, `"resolver_ip"`, or `""` for the default (`"pop"`). + +**random_steering** optionally as the following: + +- `pool_weights` - (Optional) A mapping of pool IDs to custom weights. The weight is relative to other pools in the load balancer. +- `default_weight` - (Optional) The default weight for pools in the load balancer that are not specified in the `pool_weights` map. **rules** optionally as the following: @@ -117,6 +135,9 @@ The following arguments are supported: - `session_affinity` - (Optional) See field above. - `session_affinity_ttl` - (Optional) See field above. - `session_affinity_attributes` - (Optional) See field above. +- `adaptive_routing` - (Optional) See field above. +- `location_strategy` - (Optional) See field above. +- `random_steering` - (Optional) See field above. - `ttl` - (Optional) See field above. - `steering_policy` - (Optional) See field above. - `fallback_pool` - (Optional) See fallback_pool_id above. diff --git a/internal/provider/resource_cloudflare_load_balancer.go b/internal/provider/resource_cloudflare_load_balancer.go index 84e49297e42..8be2ae14e73 100644 --- a/internal/provider/resource_cloudflare_load_balancer.go +++ b/internal/provider/resource_cloudflare_load_balancer.go @@ -96,6 +96,24 @@ var rulesElem = &schema.Resource{ }, }, + "adaptive_routing": { + Type: schema.TypeSet, + Optional: true, + Elem: adaptiveRoutingElem, + }, + + "location_strategy": { + Type: schema.TypeSet, + Optional: true, + Elem: locationStrategyElem, + }, + + "random_steering": { + Type: schema.TypeSet, + Optional: true, + Elem: randomSteeringElem, + }, + "ttl": { Type: schema.TypeInt, Optional: true, @@ -175,6 +193,50 @@ var rulesElem = &schema.Resource{ }, } +var adaptiveRoutingElem = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "failover_across_pools": { + Type: schema.TypeBool, + Optional: true, + }, + }, +} + +var locationStrategyElem = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "prefer_ecs": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"", "always", "never", "proximity", "geo"}, false), + }, + + "mode": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"", "pop", "resolver_ip"}, false), + }, + }, +} + +var randomSteeringElem = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "pool_weights": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeFloat, + ValidateFunc: validation.FloatBetween(0, 1), + }, + }, + + "default_weight": { + Type: schema.TypeFloat, + Optional: true, + ValidateFunc: validation.FloatBetween(0, 1), + }, + }, +} + var popPoolElem = &schema.Resource{ Schema: map[string]*schema.Schema{ "pop": { @@ -299,6 +361,18 @@ func resourceCloudflareLoadBalancerCreate(ctx context.Context, d *schema.Resourc newLoadBalancer.SessionAffinityAttributes = sessionAffinityAttributes } + if adaptiveRouting, ok := d.GetOk("adaptive_routing"); ok { + newLoadBalancer.AdaptiveRouting = expandAdaptiveRouting(adaptiveRouting) + } + + if locationStrategy, ok := d.GetOk("location_strategy"); ok { + newLoadBalancer.LocationStrategy = expandLocationStrategy(locationStrategy) + } + + if randomSteering, ok := d.GetOk("random_steering"); ok { + newLoadBalancer.RandomSteering = expandRandomSteering(randomSteering) + } + if rules, ok := d.GetOk("rules"); ok { v, err := expandRules(rules) if err != nil { @@ -382,6 +456,18 @@ func resourceCloudflareLoadBalancerUpdate(ctx context.Context, d *schema.Resourc loadBalancer.SessionAffinityAttributes = sessionAffinityAttributes } + if adaptiveRouting, ok := d.GetOk("adaptive_routing"); ok { + loadBalancer.AdaptiveRouting = expandAdaptiveRouting(adaptiveRouting) + } + + if locationStrategy, ok := d.GetOk("location_strategy"); ok { + loadBalancer.LocationStrategy = expandLocationStrategy(locationStrategy) + } + + if randomSteering, ok := d.GetOk("random_steering"); ok { + loadBalancer.RandomSteering = expandRandomSteering(randomSteering) + } + if rules, ok := d.GetOk("rules"); ok { v, err := expandRules(rules) if err != nil { @@ -451,6 +537,24 @@ func resourceCloudflareLoadBalancerRead(ctx context.Context, d *schema.ResourceD } } + if _, adaptiveRoutingOk := d.GetOk("adaptive_routing"); adaptiveRoutingOk { + if err := d.Set("adaptive_routing", flattenAdaptiveRouting(loadBalancer.AdaptiveRouting)); err != nil { + return diag.FromErr(fmt.Errorf("failed to set adaptive_routing: %w", err)) + } + } + + if _, locationStrategyOk := d.GetOk("location_strategy"); locationStrategyOk { + if err := d.Set("location_strategy", flattenLocationStrategy(loadBalancer.LocationStrategy)); err != nil { + return diag.FromErr(fmt.Errorf("failed to set location_strategy: %w", err)) + } + } + + if _, randomSteeringOk := d.GetOk("random_steering"); randomSteeringOk { + if err := d.Set("random_steering", flattenRandomSteering(loadBalancer.RandomSteering)); err != nil { + return diag.FromErr(fmt.Errorf("failed to set random_steering: %w", err)) + } + } + if len(loadBalancer.Rules) > 0 { fr, err := flattenRules(d, loadBalancer.Rules) if err != nil { @@ -498,9 +602,30 @@ func flattenGeoPools(pools map[string][]string, geoType string) *schema.Set { func flattenSessionAffinityAttrs(attrs *cloudflare.SessionAffinityAttributes) map[string]interface{} { return map[string]interface{}{ - "drain_duration": strconv.Itoa(attrs.DrainDuration), - "samesite": attrs.SameSite, - "secure": attrs.Secure, + "drain_duration": strconv.Itoa(attrs.DrainDuration), + "samesite": attrs.SameSite, + "secure": attrs.Secure, + "zero_downtime_failover": attrs.ZeroDowntimeFailover, + } +} + +func flattenAdaptiveRouting(properties *cloudflare.AdaptiveRouting) map[string]interface{} { + return map[string]interface{}{ + "failover_across_pools": properties.FailoverAcrossPools, + } +} + +func flattenLocationStrategy(properties *cloudflare.LocationStrategy) map[string]interface{} { + return map[string]interface{}{ + "prefer_ecs": properties.PreferECS, + "mode": properties.Mode, + } +} + +func flattenRandomSteering(properties *cloudflare.RandomSteering) map[string]interface{} { + return map[string]interface{}{ + "pool_weights": properties.PoolWeights, + "default_weight": properties.DefaultWeight, } } @@ -629,6 +754,39 @@ func flattenRules(d *schema.ResourceData, rules []*cloudflare.LoadBalancerRule) if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.session_affinity_attributes.secure", idx)); ok { saa["secure"] = o.SessionAffinityAttrs.Secure } + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.session_affinity_attributes.zero_downtime_failover", idx)); ok { + saa["zero_downtime_failover"] = o.SessionAffinityAttrs.ZeroDowntimeFailover + } + } + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.adaptive_routing", idx)); o.AdaptiveRouting != nil && ok { + ar := map[string]interface{}{} + om["adaptive_routing"] = ar + m["overrides"] = []interface{}{om} + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.adaptive_routing.failover_across_pools", idx)); ok { + ar["failover_across_pools"] = o.AdaptiveRouting.FailoverAcrossPools + } + } + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.location_strategy", idx)); o.LocationStrategy != nil && ok { + ls := map[string]interface{}{} + om["location_strategy"] = ls + m["overrides"] = []interface{}{om} + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.location_strategy.prefer_ecs", idx)); ok { + ls["prefer_ecs"] = o.LocationStrategy.PreferECS + } + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.location_strategy.mode", idx)); ok { + ls["mode"] = o.LocationStrategy.Mode + } + } + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.random_steering", idx)); o.RandomSteering != nil && ok { + rs := map[string]interface{}{} + om["random_steering"] = rs + m["overrides"] = []interface{}{om} + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.random_steering.pool_weights", idx)); ok { + rs["pool_weights"] = o.RandomSteering.PoolWeights + } + if _, ok := d.GetOkExists(fmt.Sprintf("rules.%d.overrides.0.random_steering.default_weight", idx)); ok { + rs["default_weight"] = o.RandomSteering.DefaultWeight + } } } @@ -685,6 +843,45 @@ func expandRules(rdata interface{}) ([]*cloudflare.LoadBalancerRule, error) { v.Secure = sec.(string) lbr.Overrides.SessionAffinityAttrs = v } + if zdf, ok := attr["zero_downtime_failover"]; ok { + v.ZeroDowntimeFailover = zdf.(string) + lbr.Overrides.SessionAffinityAttrs = v + } + } + + if ar, ok := ov["adaptive_routing"]; ok { + properties := ar.(map[string]interface{}) + v := &cloudflare.AdaptiveRouting{} + if f, ok := properties["failover_across_pools"]; ok { + v.FailoverAcrossPools = cloudflare.BoolPtr(f.(bool)) + lbr.Overrides.AdaptiveRouting = v + } + } + + if ls, ok := ov["location_strategy"]; ok { + properties := ls.(map[string]interface{}) + v := &cloudflare.LocationStrategy{} + if p, ok := properties["prefer_ecs"]; ok { + v.PreferECS = p.(string) + lbr.Overrides.LocationStrategy = v + } + if m, ok := properties["mode"]; ok { + v.Mode = m.(string) + lbr.Overrides.LocationStrategy = v + } + } + + if rs, ok := ov["random_steering"]; ok { + properties := rs.(map[string]interface{}) + v := &cloudflare.RandomSteering{} + if pw, ok := properties["pool_weights"]; ok { + v.PoolWeights = pw.(map[string]float64) + lbr.Overrides.RandomSteering = v + } + if dw, ok := properties["default_weight"]; ok { + v.DefaultWeight = dw.(float64) + lbr.Overrides.RandomSteering = v + } } if ttl, ok := ov["ttl"]; ok { @@ -771,8 +968,53 @@ func expandSessionAffinityAttrs(attrs interface{}) (*cloudflare.SessionAffinityA if cfSessionAffinityAttrs.DrainDuration, err = strconv.Atoi(v.(string)); err != nil { return nil, err } + case "zero_downtime_failover": + cfSessionAffinityAttrs.ZeroDowntimeFailover = v.(string) } } return &cfSessionAffinityAttrs, nil } + +func expandAdaptiveRouting(properties interface{}) *cloudflare.AdaptiveRouting { + var cfAdaptiveRouting cloudflare.AdaptiveRouting + + for k, v := range properties.(map[string]interface{}) { + switch k { + case "failover_across_pools": + cfAdaptiveRouting.FailoverAcrossPools = cloudflare.BoolPtr(v.(bool)) + } + } + + return &cfAdaptiveRouting +} + +func expandLocationStrategy(properties interface{}) *cloudflare.LocationStrategy { + var cfLocationStrategy cloudflare.LocationStrategy + + for k, v := range properties.(map[string]interface{}) { + switch k { + case "prefer_ecs": + cfLocationStrategy.PreferECS = v.(string) + case "mode": + cfLocationStrategy.Mode = v.(string) + } + } + + return &cfLocationStrategy +} + +func expandRandomSteering(properties interface{}) *cloudflare.RandomSteering { + var cfRandomSteering cloudflare.RandomSteering + + for k, v := range properties.(map[string]interface{}) { + switch k { + case "pool_weights": + cfRandomSteering.PoolWeights = v.(map[string]float64) + case "default_weight": + cfRandomSteering.DefaultWeight = v.(float64) + } + } + + return &cfRandomSteering +} diff --git a/internal/provider/resource_cloudflare_load_balancer_test.go b/internal/provider/resource_cloudflare_load_balancer_test.go index 9623b310ae3..25875d449b4 100644 --- a/internal/provider/resource_cloudflare_load_balancer_test.go +++ b/internal/provider/resource_cloudflare_load_balancer_test.go @@ -117,6 +117,7 @@ func TestAccCloudflareLoadBalancer_SessionAffinity(t *testing.T) { resource.TestCheckResourceAttr(name, "session_affinity_attributes.samesite", "Auto"), resource.TestCheckResourceAttr(name, "session_affinity_attributes.secure", "Auto"), resource.TestCheckResourceAttr(name, "session_affinity_attributes.drain_duration", "60"), + resource.TestCheckResourceAttr(name, "session_affinity_attributes.zero_downtime_failover", "sticky"), // dont check that other specified values are set, this will be evident by lack // of plan diff some values will get empty values resource.TestCheckResourceAttr(name, "pop_pools.#", "0"), @@ -153,6 +154,102 @@ func TestAccCloudflareLoadBalancer_SessionAffinity(t *testing.T) { }) } +func TestAccCloudflareLoadBalancer_AdaptiveRouting(t *testing.T) { + t.Parallel() + var loadBalancer cloudflare.LoadBalancer + zone := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + rnd := generateRandomResourceName() + name := "cloudflare_load_balancer." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckCloudflareLoadBalancerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareLoadBalancerConfigAdaptiveRouting(zoneID, zone, rnd), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareLoadBalancerExists(name, &loadBalancer), + testAccCheckCloudflareLoadBalancerIDIsValid(name, zoneID), + // explicitly verify that adaptive_routing has been set + resource.TestCheckResourceAttr(name, "adaptive_routing.failover_across_pools", "true"), + // dont check that other specified values are set, this will be evident by lack + // of plan diff some values will get empty values + resource.TestCheckResourceAttr(name, "pop_pools.#", "0"), + resource.TestCheckResourceAttr(name, "country_pools.#", "0"), + resource.TestCheckResourceAttr(name, "region_pools.#", "0"), + ), + }, + }, + }) +} + +func TestAccCloudflareLoadBalancer_LocationStrategy(t *testing.T) { + t.Parallel() + var loadBalancer cloudflare.LoadBalancer + zone := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + rnd := generateRandomResourceName() + name := "cloudflare_load_balancer." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckCloudflareLoadBalancerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareLoadBalancerConfigLocationStrategy(zoneID, zone, rnd), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareLoadBalancerExists(name, &loadBalancer), + testAccCheckCloudflareLoadBalancerIDIsValid(name, zoneID), + // explicitly verify that location_strategy has been set + resource.TestCheckResourceAttr(name, "location_strategy.prefer_ecs", "proximity"), + resource.TestCheckResourceAttr(name, "location_strategy.mode", "pop"), + // dont check that other specified values are set, this will be evident by lack + // of plan diff some values will get empty values + resource.TestCheckResourceAttr(name, "pop_pools.#", "0"), + resource.TestCheckResourceAttr(name, "country_pools.#", "0"), + resource.TestCheckResourceAttr(name, "region_pools.#", "0"), + ), + }, + }, + }) +} + +func TestAccCloudflareLoadBalancer_RandomSteering(t *testing.T) { + t.Parallel() + var loadBalancer cloudflare.LoadBalancer + zone := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + rnd := generateRandomResourceName() + name := "cloudflare_load_balancer." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckCloudflareLoadBalancerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareLoadBalancerConfigRandomSteering(zoneID, zone, rnd), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareLoadBalancerExists(name, &loadBalancer), + testAccCheckCloudflareLoadBalancerIDIsValid(name, zoneID), + // explicitly verify that random_steering has been set + resource.TestCheckResourceAttr(name, "random_steering.pool_weights.de90f38ced07c2e2f4df50b1f61d4194", "0.3"), + resource.TestCheckResourceAttr(name, "random_steering.pool_weights.9290f38c5d07c2e2f4df57b1f61d4196", "0.5"), + resource.TestCheckResourceAttr(name, "random_steering.default_weight", "0.2"), + // dont check that other specified values are set, this will be evident by lack + // of plan diff some values will get empty values + resource.TestCheckResourceAttr(name, "pop_pools.#", "0"), + resource.TestCheckResourceAttr(name, "country_pools.#", "0"), + resource.TestCheckResourceAttr(name, "region_pools.#", "0"), + ), + }, + }, + }) +} + func TestAccCloudflareLoadBalancer_GeoBalanced(t *testing.T) { t.Parallel() var loadBalancer cloudflare.LoadBalancer @@ -240,6 +337,13 @@ func TestAccCloudflareLoadBalancer_Rules(t *testing.T) { resource.TestCheckResourceAttr(name, "rules.0.overrides.0.steering_policy", "geo"), resource.TestCheckResourceAttr(name, "rules.0.overrides.0.session_affinity_attributes.samesite", "Auto"), resource.TestCheckResourceAttr(name, "rules.0.overrides.0.session_affinity_attributes.secure", "Auto"), + resource.TestCheckResourceAttr(name, "rules.0.overrides.0.session_affinity_attributes.zero_downtime_failover", "sticky"), + resource.TestCheckResourceAttr(name, "rules.0.overrides.0.adaptive_routing.failover_across_pools", "true"), + resource.TestCheckResourceAttr(name, "rules.0.overrides.0.location_strategy.prefer_ecs", "always"), + resource.TestCheckResourceAttr(name, "rules.0.overrides.0.location_strategy.mode", "resolver_ip"), + resource.TestCheckResourceAttr(name, "rules.0.overrides.0.random_steering.pool_weights.de90f38ced07c2e2f4df50b1f61d4194", "0.3"), + resource.TestCheckResourceAttr(name, "rules.0.overrides.0.random_steering.pool_weights.9290f38c5d07c2e2f4df57b1f61d4196", "0.5"), + resource.TestCheckResourceAttr(name, "rules.0.overrides.0.random_steering.default_weight", "0.2"), resource.TestCheckResourceAttr(name, "rules.#", "3"), resource.TestCheckResourceAttr(name, "rules.1.fixed_response.0.message_body", "hello"), resource.TestCheckResourceAttr(name, "rules.2.overrides.0.region_pools.#", "1"), @@ -475,6 +579,7 @@ resource "cloudflare_load_balancer" "%[3]s" { samesite = "Auto" secure = "Auto" drain_duration = 60 + zero_downtime_failover = "sticky" } }`, zoneID, zone, id) } @@ -490,6 +595,50 @@ resource "cloudflare_load_balancer" "%[3]s" { }`, zoneID, zone, id) } +func testAccCheckCloudflareLoadBalancerConfigAdaptiveRouting(zoneID, zone, id string) string { + return testAccCheckCloudflareLoadBalancerPoolConfigBasic(id) + fmt.Sprintf(` +resource "cloudflare_load_balancer" "%[3]s" { + zone_id = "%[1]s" + name = "tf-testacc-lb-adaptive-routing-%[3]s.%[2]s" + fallback_pool_id = "${cloudflare_load_balancer_pool.%[3]s.id}" + default_pool_ids = ["${cloudflare_load_balancer_pool.%[3]s.id}"] + adaptive_routing = { + failover_across_pools = true + } +}`, zoneID, zone, id) +} + +func testAccCheckCloudflareLoadBalancerConfigLocationStrategy(zoneID, zone, id string) string { + return testAccCheckCloudflareLoadBalancerPoolConfigBasic(id) + fmt.Sprintf(` +resource "cloudflare_load_balancer" "%[3]s" { + zone_id = "%[1]s" + name = "tf-testacc-lb-location-strategy-%[3]s.%[2]s" + fallback_pool_id = "${cloudflare_load_balancer_pool.%[3]s.id}" + default_pool_ids = ["${cloudflare_load_balancer_pool.%[3]s.id}"] + location_strategy = { + prefer_ecs = "proximity" + mode = "pop" + } +}`, zoneID, zone, id) +} + +func testAccCheckCloudflareLoadBalancerConfigRandomSteering(zoneID, zone, id string) string { + return testAccCheckCloudflareLoadBalancerPoolConfigBasic(id) + fmt.Sprintf(` +resource "cloudflare_load_balancer" "%[3]s" { + zone_id = "%[1]s" + name = "tf-testacc-lb-random-steering-%[3]s.%[2]s" + fallback_pool_id = "${cloudflare_load_balancer_pool.%[3]s.id}" + default_pool_ids = ["${cloudflare_load_balancer_pool.%[3]s.id}"] + random_steering = { + pool_weights = { + de90f38ced07c2e2f4df50b1f61d4194 = 0.3 + 9290f38c5d07c2e2f4df57b1f61d4196 = 0.5 + } + default_weight = 0.2 + } +}`, zoneID, zone, id) +} + func testAccCheckCloudflareLoadBalancerConfigGeoBalanced(zoneID, zone, id string) string { return testAccCheckCloudflareLoadBalancerPoolConfigBasic(id) + fmt.Sprintf(` resource "cloudflare_load_balancer" "%[3]s" { @@ -563,7 +712,22 @@ resource "cloudflare_load_balancer" "%[3]s" { session_affinity_attributes = { samesite = "Auto" secure = "Auto" + zero_downtime_failover = "sticky" } + adaptive_routing = { + failover_across_pools = true + } + location_strategy = { + prefer_ecs = "always" + mode = "resolver_ip" + } + random_steering = { + pool_weights = { + de90f38ced07c2e2f4df50b1f61d4194 = 0.3 + 9290f38c5d07c2e2f4df57b1f61d4196 = 0.5 + } + default_weight = 0.2 + } } } rules { diff --git a/internal/provider/schema_cloudflare_load_balancer.go b/internal/provider/schema_cloudflare_load_balancer.go index 2e55a01bb5b..b3d34ab6730 100644 --- a/internal/provider/schema_cloudflare_load_balancer.go +++ b/internal/provider/schema_cloudflare_load_balancer.go @@ -90,6 +90,24 @@ func resourceCloudflareLoadBalancerSchema() map[string]*schema.Schema { }, }, + "adaptive_routing": { + Type: schema.TypeSet, + Optional: true, + Elem: adaptiveRoutingElem, + }, + + "location_strategy": { + Type: schema.TypeSet, + Optional: true, + Elem: locationStrategyElem, + }, + + "random_steering": { + Type: schema.TypeSet, + Optional: true, + Elem: randomSteeringElem, + }, + "rules": { Type: schema.TypeList, Optional: true, diff --git a/templates/resources/load_balancer.md b/templates/resources/load_balancer.md index bb41f642e69..ccc614e1f12 100644 --- a/templates/resources/load_balancer.md +++ b/templates/resources/load_balancer.md @@ -79,6 +79,9 @@ The following arguments are supported: - `session_affinity` - (Optional) Associates all requests coming from an end-user with a single origin. Cloudflare will set a cookie on the initial response to the client, such that consequent requests with the cookie in the request will go to the same origin, so long as it is available. Valid values are: `""`, `"none"`, `"cookie"`, and `"ip_cookie"`. Default is `""`. - `session_affinity_ttl` - (Optional) Time, in seconds, until this load balancers session affinity cookie expires after being created. This parameter is ignored unless a supported session affinity policy is set. The current default of 23 hours will be used unless `session_affinity_ttl` is explicitly set. Once the expiry time has been reached, subsequent requests may get sent to a different origin server. Valid values are between 1800 and 604800. - `session_affinity_attributes` - (Optional) Configure cookie attributes for session affinity cookie. See the field documentation below. +- `adaptive_routing` - (Optional) Controls features that modify the routing of requests to pools and origins in response to dynamic conditions, such as during the interval between active health monitoring requests. See the field documentation below. +- `location_strategy` - (Optional) Controls location-based steering for non-proxied requests. See the field documentation below. +- `random_steering` - (Optional) Configures pool weights for random steering. When the `steering_policy="random"`, a random pool is selected with probability proportional to these pool weights. See the field documentation below. - `rules` - (Optional) A list of conditions and overrides for each load balancer operation. See the field documentation below. **region_pools** requires the following: @@ -101,6 +104,21 @@ The following arguments are supported: - `samesite` - (Optional) Configures the SameSite attribute on session affinity cookie. Value "Auto" will be translated to "Lax" or "None" depending if Always Use HTTPS is enabled. Note: when using value "None", the secure attribute can not be set to "Never". Valid values: `"Auto"`, `"Lax"`, `"None"` or `"Strict"`. - `secure` - (Optional) Configures the Secure attribute on session affinity cookie. Value "Always" indicates the Secure attribute will be set in the Set-Cookie header, "Never" indicates the Secure attribute will not be set, and "Auto" will set the Secure attribute depending if Always Use HTTPS is enabled. Valid values: `"Auto"`, `"Always"` or `"Never"`. - `drain_duration` - (Optional) Configures the drain duration in seconds. This field is only used when session affinity is enabled on the load balancer. +- `zero_downtime_failover` - (Optional) Configures the zero-downtime failover between origins within a pool when session affinity is enabled. Value "none" means no failover takes place for sessions pinned to the origin. Value "temporary" means traffic will be sent to another other healthy origin until the originally pinned origin is available; note that this can potentially result in heavy origin flapping. Value "sticky" means the session affinity cookie is updated and subsequent requests are sent to the new origin. This feature is currently incompatible with Argo, Tiered Cache, and Bandwidth Alliance. Valid values: `"none"`, `"temporary"` or `"sticky"`. Default is `"none"`. + +**adaptive_routing** optionally as the following: + +- `failover_across_pools` - (Optional) Extends zero-downtime failover of requests to healthy origins from alternate pools, when no healthy alternate exists in the same pool, according to the failover order defined by traffic and origin steering. When set `false` (the default) zero-downtime failover will only occur between origins within the same pool. + +**location_strategy** optionally as the following: + +- `prefer_ecs` - (Optional) Whether the EDNS Client Subnet (ECS) GeoIP should be preferred as the authoritative location. Value "always" will always prefer ECS, "never" will never prefer ECS, "proximity" will prefer ECS only when `steering_policy="proximity"`, and "geo" will prefer ECS only when `steering_policy="geo"`. Valid values: `"always"`, `"never"`, `"proximity"`, `"geo"` or `""` for the default (`"proximity"`). +- `mode` - (Optional) Determines the authoritative location when ECS is not preferred, does not exist in the request, or its GeoIP lookup is unsuccessful. Value "pop" will use the Cloudflare PoP location. Value "resolver_ip" will use the DNS resolver GeoIP location. If the GeoIP lookup is unsuccessful, it will use the Cloudflare PoP location. Valid values: `"pop"`, `"resolver_ip"`, or `""` for the default (`"pop"`). + +**random_steering** optionally as the following: + +- `pool_weights` - (Optional) A mapping of pool IDs to custom weights. The weight is relative to other pools in the load balancer. +- `default_weight` - (Optional) The default weight for pools in the load balancer that are not specified in the `pool_weights` map. **rules** optionally as the following: @@ -117,6 +135,9 @@ The following arguments are supported: - `session_affinity` - (Optional) See field above. - `session_affinity_ttl` - (Optional) See field above. - `session_affinity_attributes` - (Optional) See field above. +- `adaptive_routing` - (Optional) See field above. +- `location_strategy` - (Optional) See field above. +- `random_steering` - (Optional) See field above. - `ttl` - (Optional) See field above. - `steering_policy` - (Optional) See field above. - `fallback_pool` - (Optional) See fallback_pool_id above.