From b171e8a74557eea11c7bf1fc72169f99d803ce3b Mon Sep 17 00:00:00 2001 From: Mouhsen Ibrahim <78358035+mouhsen-ibrahim@users.noreply.github.com> Date: Tue, 6 Feb 2024 13:41:04 +0100 Subject: [PATCH] Add support for session persistence to the load balancer (#238) * Add support for session persistence to the load balancer * Set use_source_ip_address optional to true * Add unit tests for SessionPersistence Settings in LoadBalancer * Add acceptance test for using session persistence * Add session persistence to data source and fix acceptance tests * Update stackit/internal/services/loadbalancer/loadbalancer/resource.go Co-authored-by: Vicente Pinto * Update stackit/internal/services/loadbalancer/loadbalancer/datasource.go Co-authored-by: Vicente Pinto --------- Co-authored-by: Vicente Pinto --- .../loadbalancer/loadbalancer/datasource.go | 68 ++++---- .../loadbalancer/loadbalancer/resource.go | 151 +++++++++++++----- .../loadbalancer/resource_test.go | 27 ++++ .../loadbalancer/loadbalancer_acc_test.go | 7 + 4 files changed, 182 insertions(+), 71 deletions(-) diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index 995a4729..7e66d635 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -78,33 +78,35 @@ func (r *loadBalancerDataSource) Configure(ctx context.Context, req datasource.C // Schema defines the schema for the data source. func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ - "main": "Load Balancer resource schema.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"`name`\".", - "project_id": "STACKIT project ID to which the Load Balancer is associated.", - "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", - "listeners": "List of all listeners which will accept traffic. Limited to 20.", - "port": "Port number where we listen for traffic.", - "protocol": "Protocol is the highest network protocol we understand to load balance.", - "target_pool": "Reference target pool by target pool name.", - "name": "Load balancer name.", - "networks": "List of networks that listeners and targets reside in.", - "network_id": "Openstack network ID.", - "role": "The role defines how the load balancer is using the network.", - "options": "Defines any optional functionality you want to have enabled on your load balancer.", - "acl": "Load Balancer is accessible only from an IP address in this range.", - "private_network_only": "If true, Load Balancer is accessible only via a private network IP address.", - "private_address": "Transient private Load Balancer IP address. It can change any time.", - "target_pools": "List of all target pools which will be used in the Load Balancer. Limited to 20.", - "healthy_threshold": "Healthy threshold of the health checking.", - "interval": "Interval duration of health checking in seconds.", - "interval_jitter": "Interval duration threshold of the health checking in seconds.", - "timeout": "Active health checking timeout duration in seconds.", - "unhealthy_threshold": "Unhealthy threshold of the health checking.", - "target_pools.name": "Target pool name.", - "target_port": "Identical port number where each target listens for traffic.", - "targets": "List of all targets which will be used in the pool. Limited to 250.", - "targets.display_name": "Target display name", - "ip": "Target IP", + "main": "Load Balancer resource schema.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"`name`\".", + "project_id": "STACKIT project ID to which the Load Balancer is associated.", + "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "listeners": "List of all listeners which will accept traffic. Limited to 20.", + "port": "Port number where we listen for traffic.", + "protocol": "Protocol is the highest network protocol we understand to load balance.", + "target_pool": "Reference target pool by target pool name.", + "name": "Load balancer name.", + "networks": "List of networks that listeners and targets reside in.", + "network_id": "Openstack network ID.", + "role": "The role defines how the load balancer is using the network.", + "options": "Defines any optional functionality you want to have enabled on your load balancer.", + "acl": "Load Balancer is accessible only from an IP address in this range.", + "private_network_only": "If true, Load Balancer is accessible only via a private network IP address.", + "session_persistence": "Here you can setup various session persistence options, so far only \"`use_source_ip_address`\" is supported.", + "use_source_ip_address": "If true then all connections from one source IP address are redirected to the same target. This setting changes the load balancing algorithm to Maglev.", + "private_address": "Transient private Load Balancer IP address. It can change any time.", + "target_pools": "List of all target pools which will be used in the Load Balancer. Limited to 20.", + "healthy_threshold": "Healthy threshold of the health checking.", + "interval": "Interval duration of health checking in seconds.", + "interval_jitter": "Interval duration threshold of the health checking in seconds.", + "timeout": "Active health checking timeout duration in seconds.", + "unhealthy_threshold": "Unhealthy threshold of the health checking.", + "target_pools.name": "Target pool name.", + "target_port": "Identical port number where each target listens for traffic.", + "targets": "List of all targets which will be used in the pool. Limited to 250.", + "targets.display_name": "Target display name", + "ip": "Target IP", } resp.Schema = schema.Schema{ @@ -250,6 +252,18 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: descriptions["target_port"], Computed: true, }, + "session_persistence": schema.SingleNestedAttribute{ + Description: descriptions["session_persistence"], + Optional: true, + Computed: false, + Attributes: map[string]schema.Attribute{ + "use_source_ip_address": schema.BoolAttribute{ + Description: descriptions["use_source_ip_address"], + Optional: true, + Computed: false, + }, + }, + }, "targets": schema.ListNestedAttribute{ Description: descriptions["targets"], Computed: true, diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 9fc9d16c..d22dd46d 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -81,10 +81,16 @@ var optionsTypes = map[string]attr.Type{ // Struct corresponding to each Model.TargetPool type TargetPool struct { - ActiveHealthCheck types.Object `tfsdk:"active_health_check"` - Name types.String `tfsdk:"name"` - TargetPort types.Int64 `tfsdk:"target_port"` - Targets []Target `tfsdk:"targets"` + ActiveHealthCheck types.Object `tfsdk:"active_health_check"` + Name types.String `tfsdk:"name"` + TargetPort types.Int64 `tfsdk:"target_port"` + Targets []Target `tfsdk:"targets"` + SessionPersistence types.Object `tfsdk:"session_persistence"` +} + +// Struct corresponding to each Model.TargetPool.SessionPersistence +type SessionPersistence struct { + UseSourceIPAddress types.Bool `tfsdk:"use_source_ip_address"` } // Struct corresponding to each Model.TargetPool.ActiveHealthCheck @@ -105,6 +111,11 @@ var activeHealthCheckTypes = map[string]attr.Type{ "unhealthy_threshold": basetypes.Int64Type{}, } +// Types corresponding to SessionPersistence +var sessionPersistenceTypes = map[string]attr.Type{ + "use_source_ip_address": basetypes.BoolType{}, +} + // Struct corresponding to each Model.TargetPool.Targets type Target struct { DisplayName types.String `tfsdk:"display_name"` @@ -166,33 +177,35 @@ func (r *loadBalancerResource) Configure(ctx context.Context, req resource.Confi // Schema defines the schema for the resource. func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ - "main": "Load Balancer resource schema.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"`name`\".", - "project_id": "STACKIT project ID to which the Load Balancer is associated.", - "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", - "listeners": "List of all listeners which will accept traffic. Limited to 20.", - "port": "Port number where we listen for traffic.", - "protocol": "Protocol is the highest network protocol we understand to load balance.", - "target_pool": "Reference target pool by target pool name.", - "name": "Load balancer name.", - "networks": "List of networks that listeners and targets reside in.", - "network_id": "Openstack network ID.", - "role": "The role defines how the load balancer is using the network.", - "options": "Defines any optional functionality you want to have enabled on your load balancer.", - "acl": "Load Balancer is accessible only from an IP address in this range.", - "private_network_only": "If true, Load Balancer is accessible only via a private network IP address.", - "private_address": "Transient private Load Balancer IP address. It can change any time.", - "target_pools": "List of all target pools which will be used in the Load Balancer. Limited to 20.", - "healthy_threshold": "Healthy threshold of the health checking.", - "interval": "Interval duration of health checking in seconds.", - "interval_jitter": "Interval duration threshold of the health checking in seconds.", - "timeout": "Active health checking timeout duration in seconds.", - "unhealthy_threshold": "Unhealthy threshold of the health checking.", - "target_pools.name": "Target pool name.", - "target_port": "Identical port number where each target listens for traffic.", - "targets": "List of all targets which will be used in the pool. Limited to 250.", - "targets.display_name": "Target display name", - "ip": "Target IP", + "main": "Load Balancer resource schema.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"`name`\".", + "project_id": "STACKIT project ID to which the Load Balancer is associated.", + "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", + "listeners": "List of all listeners which will accept traffic. Limited to 20.", + "port": "Port number where we listen for traffic.", + "protocol": "Protocol is the highest network protocol we understand to load balance.", + "target_pool": "Reference target pool by target pool name.", + "name": "Load balancer name.", + "networks": "List of networks that listeners and targets reside in.", + "network_id": "Openstack network ID.", + "role": "The role defines how the load balancer is using the network.", + "options": "Defines any optional functionality you want to have enabled on your load balancer.", + "acl": "Load Balancer is accessible only from an IP address in this range.", + "private_network_only": "If true, Load Balancer is accessible only via a private network IP address.", + "session_persistence": "Here you can setup various session persistence options, so far only \"`use_source_ip_address`\" is supported.", + "use_source_ip_address": "If true then all connections from one source IP address are redirected to the same target. This setting changes the load balancing algorithm to Maglev.", + "private_address": "Transient private Load Balancer IP address. It can change any time.", + "target_pools": "List of all target pools which will be used in the Load Balancer. Limited to 20.", + "healthy_threshold": "Healthy threshold of the health checking.", + "interval": "Interval duration of health checking in seconds.", + "interval_jitter": "Interval duration threshold of the health checking in seconds.", + "timeout": "Active health checking timeout duration in seconds.", + "unhealthy_threshold": "Unhealthy threshold of the health checking.", + "target_pools.name": "Target pool name.", + "target_port": "Identical port number where each target listens for traffic.", + "targets": "List of all targets which will be used in the pool. Limited to 250.", + "targets.display_name": "Target display name", + "ip": "Target IP", } resp.Schema = schema.Schema{ @@ -224,7 +237,7 @@ provider "openstack" { auth_url = "https://keystone.api.iaas.eu01.stackit.cloud/v3" } ` + "\n```" + ` - + ### Configuring the supporting infrastructure The example below uses OpenStack to create the network, router, a public IP address and a compute instance. @@ -446,6 +459,18 @@ The example below uses OpenStack to create the network, router, a public IP addr Description: descriptions["target_port"], Required: true, }, + "session_persistence": schema.SingleNestedAttribute{ + Description: descriptions["session_persistence"], + Optional: true, + Computed: false, + Attributes: map[string]schema.Attribute{ + "use_source_ip_address": schema.BoolAttribute{ + Description: descriptions["use_source_ip_address"], + Optional: true, + Computed: false, + }, + }, + }, "targets": schema.ListNestedAttribute{ Description: descriptions["targets"], Required: true, @@ -783,11 +808,16 @@ func toTargetPoolsPayload(ctx context.Context, model *Model) (*[]loadbalancer.Ta return nil, fmt.Errorf("converting target pool: %w", err) } + session_persistence, err := toSessionPersistencePayload(ctx, utils.Ptr(targetPool)) + if err != nil { + return nil, fmt.Errorf("converting target pool: %w", err) + } targetPools = append(targetPools, loadbalancer.TargetPool{ - ActiveHealthCheck: activeHealthCheck, - Name: conversion.StringValueToPointer(targetPool.Name), - TargetPort: conversion.Int64ValueToPointer(targetPool.TargetPort), - Targets: targets, + ActiveHealthCheck: activeHealthCheck, + Name: conversion.StringValueToPointer(targetPool.Name), + TargetPort: conversion.Int64ValueToPointer(targetPool.TargetPort), + Targets: targets, + SessionPersistence: session_persistence, }) } @@ -806,11 +836,33 @@ func toTargetPoolUpdatePayload(ctx context.Context, targetPool *TargetPool) (*lo targets := toTargetsPayload(targetPool) + session_persistence, err := toSessionPersistencePayload(ctx, targetPool) + if err != nil { + return nil, fmt.Errorf("converting target pool: %w", err) + } + return &loadbalancer.UpdateTargetPoolPayload{ - ActiveHealthCheck: activeHealthCheck, - Name: conversion.StringValueToPointer(targetPool.Name), - TargetPort: conversion.Int64ValueToPointer(targetPool.TargetPort), - Targets: targets, + ActiveHealthCheck: activeHealthCheck, + Name: conversion.StringValueToPointer(targetPool.Name), + TargetPort: conversion.Int64ValueToPointer(targetPool.TargetPort), + Targets: targets, + SessionPersistence: session_persistence, + }, nil +} + +func toSessionPersistencePayload(ctx context.Context, targetPool *TargetPool) (*loadbalancer.SessionPersistence, error) { + if targetPool.SessionPersistence.IsNull() || targetPool.ActiveHealthCheck.IsUnknown() { + return nil, nil + } + + var session_persistence SessionPersistence + diags := targetPool.SessionPersistence.As(ctx, &session_persistence, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting session persistence: %w", core.DiagsToError(diags)) + } + + return &loadbalancer.SessionPersistence{ + UseSourceIpAddress: conversion.BoolValueToPointer(session_persistence.UseSourceIPAddress), }, nil } @@ -967,6 +1019,7 @@ func mapTargetPools(lb *loadbalancer.LoadBalancer, m *Model) error { var targetPools []TargetPool for _, targetPool := range *lb.TargetPools { var activeHealthCheck basetypes.ObjectValue + var sessionPersistence basetypes.ObjectValue if targetPool.ActiveHealthCheck != nil { activeHealthCheckValues := map[string]attr.Value{ "healthy_threshold": types.Int64Value(*targetPool.ActiveHealthCheck.HealthyThreshold), @@ -980,6 +1033,15 @@ func mapTargetPools(lb *loadbalancer.LoadBalancer, m *Model) error { return fmt.Errorf("converting active health check: %w", core.DiagsToError(diags)) } } + if targetPool.SessionPersistence != nil { + sessionPersistenceValues := map[string]attr.Value{ + "use_source_ip_address": types.BoolValue(*targetPool.SessionPersistence.UseSourceIpAddress), + } + sessionPersistence, diags = types.ObjectValue(sessionPersistenceTypes, sessionPersistenceValues) + if diags != nil { + return fmt.Errorf("converting session persistence: %w", core.DiagsToError(diags)) + } + } var targets []Target if targetPool.Targets != nil { @@ -992,10 +1054,11 @@ func mapTargetPools(lb *loadbalancer.LoadBalancer, m *Model) error { } targetPools = append(targetPools, TargetPool{ - ActiveHealthCheck: activeHealthCheck, - Name: types.StringPointerValue(targetPool.Name), - TargetPort: types.Int64Value(*targetPool.TargetPort), - Targets: targets, + ActiveHealthCheck: activeHealthCheck, + Name: types.StringPointerValue(targetPool.Name), + TargetPort: types.Int64Value(*targetPool.TargetPort), + Targets: targets, + SessionPersistence: sessionPersistence, }) } m.TargetPools = targetPools diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go index 86e5aaa7..91f73ed5 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go @@ -88,6 +88,12 @@ func TestToCreatePayload(t *testing.T) { Ip: types.StringValue("ip"), }, }, + SessionPersistence: types.ObjectValueMust( + sessionPersistenceTypes, + map[string]attr.Value{ + "use_source_ip_address": types.BoolValue(true), + }, + ), }, }, }, @@ -135,6 +141,9 @@ func TestToCreatePayload(t *testing.T) { Ip: utils.Ptr("ip"), }, }), + SessionPersistence: utils.Ptr(loadbalancer.SessionPersistence{ + UseSourceIpAddress: utils.Ptr(true), + }), }, }), }, @@ -200,6 +209,12 @@ func TestToTargetPoolUpdatePayload(t *testing.T) { Ip: types.StringValue("ip"), }, }, + SessionPersistence: types.ObjectValueMust( + sessionPersistenceTypes, + map[string]attr.Value{ + "use_source_ip_address": types.BoolValue(false), + }, + ), }, &loadbalancer.UpdateTargetPoolPayload{ ActiveHealthCheck: utils.Ptr(loadbalancer.ActiveHealthCheck{ @@ -217,6 +232,9 @@ func TestToTargetPoolUpdatePayload(t *testing.T) { Ip: utils.Ptr("ip"), }, }), + SessionPersistence: utils.Ptr(loadbalancer.SessionPersistence{ + UseSourceIpAddress: utils.Ptr(false), + }), }, true, }, @@ -322,6 +340,9 @@ func TestMapFields(t *testing.T) { Ip: utils.Ptr("ip"), }, }), + SessionPersistence: utils.Ptr(loadbalancer.SessionPersistence{ + UseSourceIpAddress: utils.Ptr(true), + }), }, }), }, @@ -378,6 +399,12 @@ func TestMapFields(t *testing.T) { Ip: types.StringValue("ip"), }, }, + SessionPersistence: types.ObjectValueMust( + sessionPersistenceTypes, + map[string]attr.Value{ + "use_source_ip_address": types.BoolValue(true), + }, + ), }, }, }, diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 1975783d..3008269d 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -27,6 +27,7 @@ var loadBalancerResource = map[string]string{ "interval_jitter": "5s", "timeout": "10s", "unhealthy_threshold": "3", + "use_source_ip_address": "true", "listener_display_name": "example-listener", "listener_port": "5432", "listener_protocol": "PROTOCOL_TCP", @@ -53,6 +54,9 @@ func configResources(targetPort string) string { ip = openstack_compute_instance_v2.example.network.0.fixed_ip_v4 } ] + session_persistence = { + use_source_ip_address = %s + } active_health_check = { healthy_threshold = %s interval = "%s" @@ -92,6 +96,7 @@ func configResources(targetPort string) string { loadBalancerResource["target_pool_name"], targetPort, loadBalancerResource["target_display_name"], + loadBalancerResource["use_source_ip_address"], loadBalancerResource["healthy_threshold"], loadBalancerResource["interval"], loadBalancerResource["interval_jitter"], @@ -206,6 +211,7 @@ func TestAccLoadBalancerResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval_jitter", loadBalancerResource["interval_jitter"]), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.timeout", loadBalancerResource["timeout"]), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.unhealthy_threshold", loadBalancerResource["unhealthy_threshold"]), + resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.session_persistence.use_source_ip_address", loadBalancerResource["use_source_ip_address"]), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.display_name", loadBalancerResource["listener_display_name"]), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.port", loadBalancerResource["listener_port"]), resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.protocol", loadBalancerResource["listener_protocol"]), @@ -248,6 +254,7 @@ func TestAccLoadBalancerResource(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval_jitter", loadBalancerResource["interval_jitter"]), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.timeout", loadBalancerResource["timeout"]), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.unhealthy_threshold", loadBalancerResource["unhealthy_threshold"]), + resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.session_persistence.use_source_ip_address", loadBalancerResource["use_source_ip_address"]), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.display_name", loadBalancerResource["listener_display_name"]), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.port", loadBalancerResource["listener_port"]), resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.protocol", loadBalancerResource["listener_protocol"]),