diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md
index 8f61ae65d..3c967774f 100644
--- a/docs/data-sources/loadbalancer.md
+++ b/docs/data-sources/loadbalancer.md
@@ -43,6 +43,7 @@ data "stackit_loadbalancer" "example" {
- `private_address` (String) Transient private Load Balancer IP address. It can change any time.
- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT Network areas (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.
- `target_pools` (Attributes List) List of all target pools which will be used in the Load Balancer. Limited to 20. (see [below for nested schema](#nestedatt--target_pools))
+- `version` (String) Load balancer resource version.
### Nested Schema for `listeners`
diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md
index 391423fe7..314221ab9 100644
--- a/docs/resources/loadbalancer.md
+++ b/docs/resources/loadbalancer.md
@@ -247,6 +247,7 @@ import {
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`","region","`name`".
- `private_address` (String) Transient private Load Balancer IP address. It can change any time.
- `security_group_id` (String) The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT network areas (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.
+- `version` (String) Load balancer resource version. This is needed to have concurrency safe updates.
### Nested Schema for `listeners`
@@ -365,7 +366,7 @@ Optional:
Optional:
- `credentials_ref` (String) Credentials reference for logs. Not changeable after creation.
-- `push_url` (String) Credentials reference for logs. Not changeable after creation.
+- `push_url` (String) The ARGUS/Loki remote write Push URL to ship the logs to. Not changeable after creation.
@@ -374,4 +375,4 @@ Optional:
Optional:
- `credentials_ref` (String) Credentials reference for metrics. Not changeable after creation.
-- `push_url` (String) Credentials reference for metrics. Not changeable after creation.
+- `push_url` (String) The ARGUS/Prometheus remote write Push URL to ship the metrics to. Not changeable after creation.
diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go
index bf2c0b21a..e6d9260c8 100644
--- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go
+++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go
@@ -112,6 +112,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe
"tcp_options_idle_timeout": "Time after which an idle connection is closed. The default value is set to 5 minutes, and the maximum value is one hour.",
"udp_options": "Options that are specific to the UDP protocol.",
"udp_options_idle_timeout": "Time after which an idle session is closed. The default value is set to 1 minute, and the maximum value is 2 minutes.",
+ "version": "Load balancer resource version.",
}
resp.Schema = schema.Schema{
@@ -375,6 +376,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe
Description: descriptions["security_group_id"],
Computed: true,
},
+ "version": schema.StringAttribute{
+ Description: descriptions["version"],
+ Computed: true,
+ },
},
}
}
diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go
index 13c89d91d..e5d2ddd04 100644
--- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go
+++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go
@@ -7,6 +7,7 @@ import (
"strings"
"time"
+ "github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier"
@@ -62,6 +63,7 @@ type Model struct {
TargetPools types.List `tfsdk:"target_pools"`
Region types.String `tfsdk:"region"`
SecurityGroupId types.String `tfsdk:"security_group_id"`
+ Version types.String `tfsdk:"version"`
}
// Struct corresponding to Model.Listeners[i]
@@ -368,6 +370,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques
"tcp_options_idle_timeout": "Time after which an idle connection is closed. The default value is set to 300 seconds, and the maximum value is 3600 seconds. The format is a duration and the unit must be seconds. Example: 30s",
"udp_options": "Options that are specific to the UDP protocol.",
"udp_options_idle_timeout": "Time after which an idle session is closed. The default value is set to 1 minute, and the maximum value is 2 minutes. The format is a duration and the unit must be seconds. Example: 30s",
+ "version": "Load balancer resource version. This is needed to have concurrency safe updates.",
}
resp.Schema = schema.Schema{
@@ -417,15 +420,12 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
- stringplanmodifier.RequiresReplace(),
+ stringplanmodifier.UseStateForUnknown(),
},
},
"listeners": schema.ListNestedAttribute{
Description: descriptions["listeners"],
Required: true,
- PlanModifiers: []planmodifier.List{
- listplanmodifier.RequiresReplace(),
- },
Validators: []validator.List{
listvalidator.SizeBetween(1, 20),
},
@@ -436,7 +436,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
- stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
},
@@ -444,7 +443,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Description: descriptions["port"],
Required: true,
PlanModifiers: []planmodifier.Int32{
- int32planmodifier.RequiresReplace(),
int32planmodifier.UseStateForUnknown(),
},
},
@@ -452,7 +450,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Description: descriptions["protocol"],
Required: true,
PlanModifiers: []planmodifier.String{
- stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
@@ -476,7 +473,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Description: descriptions["target_pool"],
Required: true,
PlanModifiers: []planmodifier.String{
- stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
},
@@ -557,7 +553,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
- objectplanmodifier.RequiresReplace(),
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
@@ -567,7 +562,6 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Set{
- setplanmodifier.RequiresReplace(),
setplanmodifier.UseStateForUnknown(),
},
Validators: []validator.Set{
@@ -590,8 +584,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
- // API docs says observability options are not changeable after creation
- objectplanmodifier.RequiresReplace(),
+ objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"logs": schema.SingleNestedAttribute{
@@ -605,7 +598,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Computed: true,
},
"push_url": schema.StringAttribute{
- Description: descriptions["observability_logs_credentials_ref"],
+ Description: descriptions["observability_logs_push_url"],
Optional: true,
Computed: true,
},
@@ -622,7 +615,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
Computed: true,
},
"push_url": schema.StringAttribute{
- Description: descriptions["observability_metrics_credentials_ref"],
+ Description: descriptions["observability_metrics_push_url"],
Optional: true,
Computed: true,
},
@@ -737,6 +730,10 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
stringplanmodifier.UseStateForUnknown(),
},
},
+ "version": schema.StringAttribute{
+ Description: descriptions["version"],
+ Computed: true,
+ },
},
}
}
@@ -870,6 +867,14 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe
return
}
+ // Get version from state. It's not included in the plan because it's a computed attribute
+ var stateVersion types.String
+ resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("version"), &stateVersion)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ model.Version = stateVersion
+
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
@@ -879,44 +884,21 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe
ctx = tflog.SetField(ctx, "name", name)
ctx = tflog.SetField(ctx, "region", region)
- targetPoolsModel := []targetPool{}
- diags = model.TargetPools.ElementsAs(ctx, &targetPoolsModel, false)
- resp.Diagnostics.Append(diags...)
- if resp.Diagnostics.HasError() {
+ // Generate API request body from model
+ payload, err := toUpdatePayload(ctx, &model)
+ if err != nil {
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Creating API payload: %v", err))
return
}
- for i := range targetPoolsModel {
- targetPoolModel := targetPoolsModel[i]
- targetPoolName := targetPoolModel.Name.ValueString()
- ctx = tflog.SetField(ctx, "target_pool_name", targetPoolName)
-
- // Generate API request body from model
- payload, err := toTargetPoolUpdatePayload(ctx, new(targetPoolModel))
- if err != nil {
- core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Creating API payload for target pool: %v", err))
- return
- }
- // Update target pool
- _, err = r.client.DefaultAPI.UpdateTargetPool(ctx, projectId, region, name, targetPoolName).UpdateTargetPoolPayload(*payload).Execute()
- if err != nil {
- core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Calling API for target pool: %v", err))
- return
- }
-
- ctx = core.LogResponse(ctx)
- }
- ctx = tflog.SetField(ctx, "target_pool_name", nil)
-
- // Get updated load balancer
- getResp, err := r.client.DefaultAPI.GetLoadBalancer(ctx, projectId, region, name).Execute()
+ loadBalancer, err := r.client.DefaultAPI.UpdateLoadBalancer(ctx, projectId, region, name).UpdateLoadBalancerPayload(*payload).Execute()
if err != nil {
- core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Calling API after update: %v", err))
+ core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Calling API: %v", utils.PrettyApiErr(ctx, &resp.Diagnostics, err)))
return
}
// Map response body to schema
- err = mapFields(ctx, getResp, &model, region)
+ err = mapFields(ctx, loadBalancer, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err))
return
@@ -1024,8 +1006,44 @@ func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoa
}, nil
}
+func toUpdatePayload(ctx context.Context, model *Model) (*loadbalancer.UpdateLoadBalancerPayload, error) {
+ if model == nil {
+ return nil, fmt.Errorf("nil model")
+ }
+
+ listenersPayload, err := toListenersPayload(ctx, model)
+ if err != nil {
+ return nil, fmt.Errorf("converting listeners: %w", err)
+ }
+ networksPayload, err := toNetworksPayload(ctx, model)
+ if err != nil {
+ return nil, fmt.Errorf("converting networks: %w", err)
+ }
+ optionsPayload, err := toOptionsPayload(ctx, model)
+ if err != nil {
+ return nil, fmt.Errorf("converting options: %w", err)
+ }
+ targetPoolsPayload, err := toTargetPoolsPayload(ctx, model)
+ if err != nil {
+ return nil, fmt.Errorf("converting target_pools: %w", err)
+ }
+
+ return &loadbalancer.UpdateLoadBalancerPayload{
+ Listeners: listenersPayload,
+ PlanId: conversion.StringValueToPointer(model.PlanId),
+ Options: optionsPayload,
+ TargetPools: targetPoolsPayload,
+ Version: conversion.StringValueToPointer(model.Version),
+ // Not updatable but must be present
+ Name: conversion.StringValueToPointer(model.Name),
+ ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress),
+ DisableTargetSecurityGroupAssignment: conversion.BoolValueToPointer(model.DisableSecurityGroupAssignment),
+ Networks: networksPayload,
+ }, nil
+}
+
func toListenersPayload(ctx context.Context, model *Model) ([]loadbalancer.Listener, error) {
- if model.Listeners.IsNull() || model.Listeners.IsUnknown() {
+ if utils.IsUndefined(model.Listeners) {
return nil, nil
}
@@ -1069,7 +1087,7 @@ func toListenersPayload(ctx context.Context, model *Model) ([]loadbalancer.Liste
}
func toServerNameIndicatorsPayload(ctx context.Context, l *listener) ([]loadbalancer.ServerNameIndicator, error) {
- if l.ServerNameIndicators.IsNull() || l.ServerNameIndicators.IsUnknown() {
+ if utils.IsUndefined(l.ServerNameIndicators) {
return nil, nil
}
@@ -1091,7 +1109,7 @@ func toServerNameIndicatorsPayload(ctx context.Context, l *listener) ([]loadbala
}
func toTCP(ctx context.Context, listener *listener) (*loadbalancer.OptionsTCP, error) {
- if listener.TCP.IsNull() || listener.TCP.IsUnknown() {
+ if utils.IsUndefined(listener.TCP) {
return nil, nil
}
@@ -1100,7 +1118,7 @@ func toTCP(ctx context.Context, listener *listener) (*loadbalancer.OptionsTCP, e
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
- if tcp.IdleTimeout.IsNull() || tcp.IdleTimeout.IsUnknown() {
+ if utils.IsUndefined(tcp.IdleTimeout) {
return nil, nil
}
@@ -1110,7 +1128,7 @@ func toTCP(ctx context.Context, listener *listener) (*loadbalancer.OptionsTCP, e
}
func toUDP(ctx context.Context, listener *listener) (*loadbalancer.OptionsUDP, error) {
- if listener.UDP.IsNull() || listener.UDP.IsUnknown() {
+ if utils.IsUndefined(listener.UDP) {
return nil, nil
}
@@ -1119,7 +1137,7 @@ func toUDP(ctx context.Context, listener *listener) (*loadbalancer.OptionsUDP, e
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
- if udp.IdleTimeout.IsNull() || udp.IdleTimeout.IsUnknown() {
+ if utils.IsUndefined(udp.IdleTimeout) {
return nil, nil
}
@@ -1129,7 +1147,7 @@ func toUDP(ctx context.Context, listener *listener) (*loadbalancer.OptionsUDP, e
}
func toNetworksPayload(ctx context.Context, model *Model) ([]loadbalancer.Network, error) {
- if model.Networks.IsNull() || model.Networks.IsUnknown() {
+ if utils.IsUndefined(model.Networks) {
return nil, nil
}
@@ -1156,7 +1174,7 @@ func toNetworksPayload(ctx context.Context, model *Model) ([]loadbalancer.Networ
}
func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBalancerOptions, error) {
- if model.Options.IsNull() || model.Options.IsUnknown() {
+ if utils.IsUndefined(model.Options) {
return &loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{},
Observability: &loadbalancer.LoadbalancerOptionObservability{},
@@ -1170,7 +1188,7 @@ func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBala
}
accessControlPayload := &loadbalancer.LoadbalancerOptionAccessControl{}
- if !(optionsModel.ACL.IsNull() || optionsModel.ACL.IsUnknown()) {
+ if !utils.IsUndefined(optionsModel.ACL) {
var aclModel []string
diags := optionsModel.ACL.ElementsAs(ctx, &aclModel, false)
if diags.HasError() {
@@ -1179,9 +1197,10 @@ func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBala
accessControlPayload.AllowedSourceRanges = aclModel
}
- observabilityPayload := &loadbalancer.LoadbalancerOptionObservability{}
- if !(optionsModel.Observability.IsNull() || optionsModel.Observability.IsUnknown()) {
+ var observabilityPayload *loadbalancer.LoadbalancerOptionObservability
+ if !utils.IsUndefined(optionsModel.Observability) {
observabilityModel := observability{}
+ observabilityPayload = &loadbalancer.LoadbalancerOptionObservability{}
diags := optionsModel.Observability.As(ctx, &observabilityModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting observability: %w", core.DiagsToError(diags))
@@ -1220,7 +1239,7 @@ func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBala
}
func toTargetPoolsPayload(ctx context.Context, model *Model) ([]loadbalancer.TargetPool, error) {
- if model.TargetPools.IsNull() || model.TargetPools.IsUnknown() {
+ if utils.IsUndefined(model.TargetPools) {
return nil, nil
}
@@ -1263,35 +1282,8 @@ func toTargetPoolsPayload(ctx context.Context, model *Model) ([]loadbalancer.Tar
return payload, nil
}
-func toTargetPoolUpdatePayload(ctx context.Context, tp *targetPool) (*loadbalancer.UpdateTargetPoolPayload, error) {
- if tp == nil {
- return nil, fmt.Errorf("nil target pool")
- }
-
- activeHealthCheckPayload, err := toActiveHealthCheckPayload(ctx, tp)
- if err != nil {
- return nil, fmt.Errorf("converting active_health_check: %w", err)
- }
- sessionPersistencePayload, err := toSessionPersistencePayload(ctx, tp)
- if err != nil {
- return nil, fmt.Errorf("converting session_persistence: %w", err)
- }
- targetsPayload, err := toTargetsPayload(ctx, tp)
- if err != nil {
- return nil, fmt.Errorf("converting targets: %w", err)
- }
-
- return &loadbalancer.UpdateTargetPoolPayload{
- ActiveHealthCheck: activeHealthCheckPayload,
- Name: conversion.StringValueToPointer(tp.Name),
- SessionPersistence: sessionPersistencePayload,
- TargetPort: conversion.Int32ValueToPointer(tp.TargetPort),
- Targets: targetsPayload,
- }, nil
-}
-
func toSessionPersistencePayload(ctx context.Context, tp *targetPool) (*loadbalancer.SessionPersistence, error) {
- if tp.SessionPersistence.IsNull() || tp.ActiveHealthCheck.IsUnknown() {
+ if utils.IsUndefined(tp.SessionPersistence) {
return nil, nil
}
@@ -1307,7 +1299,7 @@ func toSessionPersistencePayload(ctx context.Context, tp *targetPool) (*loadbala
}
func toActiveHealthCheckPayload(ctx context.Context, tp *targetPool) (*loadbalancer.ActiveHealthCheck, error) {
- if tp.ActiveHealthCheck.IsNull() || tp.ActiveHealthCheck.IsUnknown() {
+ if utils.IsUndefined(tp.ActiveHealthCheck) {
return nil, nil
}
@@ -1327,7 +1319,7 @@ func toActiveHealthCheckPayload(ctx context.Context, tp *targetPool) (*loadbalan
}
func toTargetsPayload(ctx context.Context, tp *targetPool) ([]loadbalancer.Target, error) {
- if tp.Targets.IsNull() || tp.Targets.IsUnknown() {
+ if utils.IsUndefined(tp.Targets) {
return nil, nil
}
@@ -1378,6 +1370,7 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg
m.ExternalAddress = types.StringPointerValue(lb.ExternalAddress)
m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress)
m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment)
+ m.Version = types.StringPointerValue(lb.Version)
if lb.TargetSecurityGroup != nil {
m.SecurityGroupId = types.StringPointerValue(lb.TargetSecurityGroup.Id)
@@ -1563,13 +1556,13 @@ func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer
// If the private_network_only field is nil in the response but is explicitly set to false in the model,
// we set it to false in the TF state to prevent an inconsistent result after apply error
- if !m.Options.IsNull() && !m.Options.IsUnknown() {
+ if !utils.IsUndefined(m.Options) {
optionsModel := options{}
diags := m.Options.As(ctx, &optionsModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return fmt.Errorf("convert options: %w", core.DiagsToError(diags))
}
- if loadBalancerResp.Options.PrivateNetworkOnly == nil && !optionsModel.PrivateNetworkOnly.IsNull() && !optionsModel.PrivateNetworkOnly.IsUnknown() && !optionsModel.PrivateNetworkOnly.ValueBool() {
+ if loadBalancerResp.Options.PrivateNetworkOnly == nil && !utils.IsUndefined(optionsModel.PrivateNetworkOnly) && !optionsModel.PrivateNetworkOnly.ValueBool() {
privateNetworkOnlyTF = types.BoolValue(false)
}
}
@@ -1619,6 +1612,10 @@ func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer
}
optionsMap["observability"] = observabilityTF
+ if loadBalancerResp.Options.Observability == nil {
+ optionsMap["observability"] = types.ObjectNull(observabilityTypes)
+ }
+
optionsTF, diags := types.ObjectValue(optionsTypes, optionsMap)
if diags.HasError() {
return core.DiagsToError(diags)
diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go
index b3846d51c..c5ba0e2e5 100644
--- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go
+++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go
@@ -367,89 +367,6 @@ func TestToCreatePayload(t *testing.T) {
}
}
-func TestToTargetPoolUpdatePayload(t *testing.T) {
- tests := []struct {
- description string
- input *targetPool
- expected *loadbalancer.UpdateTargetPoolPayload
- isValid bool
- }{
- {
- "default_values_ok",
- &targetPool{},
- &loadbalancer.UpdateTargetPoolPayload{},
- true,
- },
- {
- "simple_values_ok",
- &targetPool{
- ActiveHealthCheck: types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{
- "healthy_threshold": types.Int32Value(1),
- "interval": types.StringValue("2s"),
- "interval_jitter": types.StringValue("3s"),
- "timeout": types.StringValue("4s"),
- "unhealthy_threshold": types.Int32Value(5),
- }),
- Name: types.StringValue("name"),
- TargetPort: types.Int32Value(80),
- Targets: types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{
- types.ObjectValueMust(targetTypes, map[string]attr.Value{
- "display_name": types.StringValue("display_name"),
- "ip": types.StringValue("ip"),
- }),
- }),
- SessionPersistence: types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{
- "use_source_ip_address": types.BoolValue(false),
- }),
- },
- &loadbalancer.UpdateTargetPoolPayload{
- ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
- HealthyThreshold: new(int32(1)),
- Interval: new("2s"),
- IntervalJitter: new("3s"),
- Timeout: new("4s"),
- UnhealthyThreshold: new(int32(5)),
- },
- Name: new("name"),
- TargetPort: new(int32(80)),
- Targets: []loadbalancer.Target{
- {
- DisplayName: new("display_name"),
- Ip: new("ip"),
- },
- },
- SessionPersistence: &loadbalancer.SessionPersistence{
- UseSourceIpAddress: new(false),
- },
- },
- true,
- },
- {
- "nil_target_pool",
- nil,
- nil,
- false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.description, func(t *testing.T) {
- output, err := toTargetPoolUpdatePayload(context.Background(), tt.input)
- if !tt.isValid && err == nil {
- t.Fatalf("Should have failed")
- }
- if tt.isValid && err != nil {
- t.Fatalf("Should not have failed: %v", err)
- }
- if tt.isValid {
- diff := cmp.Diff(output, tt.expected)
- if diff != "" {
- t.Fatalf("Data does not match: %s", diff)
- }
- }
- })
- }
-}
-
func TestMapFields(t *testing.T) {
const testRegion = "eu01"
id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "name")
@@ -951,3 +868,375 @@ func Test_validateConfig(t *testing.T) {
})
}
}
+
+func Test_toUpdatePayload(t *testing.T) {
+ tests := []struct {
+ description string
+ input *Model
+ expected *loadbalancer.UpdateLoadBalancerPayload
+ isValid bool
+ }{
+ {
+ "default_values_ok",
+ &Model{},
+ &loadbalancer.UpdateLoadBalancerPayload{
+ ExternalAddress: nil,
+ Listeners: nil,
+ Name: nil,
+ Networks: nil,
+ Options: &loadbalancer.LoadBalancerOptions{
+ AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
+ AllowedSourceRanges: nil,
+ },
+ PrivateNetworkOnly: nil,
+ Observability: &loadbalancer.LoadbalancerOptionObservability{},
+ },
+ TargetPools: nil,
+ },
+ true,
+ },
+ {
+ "default_values_with_version_ok",
+ &Model{
+ Version: types.StringValue("lb-1"),
+ },
+ &loadbalancer.UpdateLoadBalancerPayload{
+ ExternalAddress: nil,
+ Listeners: nil,
+ Name: nil,
+ Networks: nil,
+ Options: &loadbalancer.LoadBalancerOptions{
+ AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
+ AllowedSourceRanges: nil,
+ },
+ PrivateNetworkOnly: nil,
+ Observability: &loadbalancer.LoadbalancerOptionObservability{},
+ },
+ TargetPools: nil,
+ Version: new("lb-1"),
+ },
+ true,
+ },
+ {
+ "simple_values_ok",
+ &Model{
+ ExternalAddress: types.StringValue("external_address"),
+ Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{
+ types.ObjectValueMust(listenerTypes, map[string]attr.Value{
+ "display_name": types.StringValue("display_name"),
+ "port": types.Int32Value(80),
+ "protocol": types.StringValue(string(legacyLoadbalancer.LISTENERPROTOCOL_TCP)),
+ "server_name_indicators": types.ListValueMust(types.ObjectType{AttrTypes: serverNameIndicatorTypes}, []attr.Value{
+ types.ObjectValueMust(
+ serverNameIndicatorTypes,
+ map[string]attr.Value{
+ "name": types.StringValue("domain.com"),
+ },
+ ),
+ },
+ ),
+ "target_pool": types.StringValue("target_pool"),
+ "tcp": types.ObjectValueMust(tcpTypes, map[string]attr.Value{
+ "idle_timeout": types.StringValue("50s"),
+ }),
+ "udp": types.ObjectValueMust(udpTypes, map[string]attr.Value{
+ "idle_timeout": types.StringValue("50s"),
+ }),
+ }),
+ }),
+ Name: types.StringValue("name"),
+ Networks: types.ListValueMust(types.ObjectType{AttrTypes: networkTypes}, []attr.Value{
+ types.ObjectValueMust(networkTypes, map[string]attr.Value{
+ "network_id": types.StringValue("network_id"),
+ "role": types.StringValue(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ }),
+ types.ObjectValueMust(networkTypes, map[string]attr.Value{
+ "network_id": types.StringValue("network_id_2"),
+ "role": types.StringValue(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ }),
+ }),
+ Options: types.ObjectValueMust(
+ optionsTypes,
+ map[string]attr.Value{
+ "acl": types.SetValueMust(
+ types.StringType,
+ []attr.Value{types.StringValue("cidr")}),
+ "private_network_only": types.BoolValue(true),
+ "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{
+ "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{
+ "credentials_ref": types.StringValue("logs-credentials_ref"),
+ "push_url": types.StringValue("logs-push_url"),
+ }),
+ "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{
+ "credentials_ref": types.StringValue("metrics-credentials_ref"),
+ "push_url": types.StringValue("metrics-push_url"),
+ }),
+ }),
+ },
+ ),
+ TargetPools: types.ListValueMust(types.ObjectType{AttrTypes: targetPoolTypes}, []attr.Value{
+ types.ObjectValueMust(targetPoolTypes, map[string]attr.Value{
+ "active_health_check": types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{
+ "healthy_threshold": types.Int32Value(1),
+ "interval": types.StringValue("2s"),
+ "interval_jitter": types.StringValue("3s"),
+ "timeout": types.StringValue("4s"),
+ "unhealthy_threshold": types.Int32Value(5),
+ }),
+ "name": types.StringValue("name"),
+ "target_port": types.Int32Value(80),
+ "targets": types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{
+ types.ObjectValueMust(targetTypes, map[string]attr.Value{
+ "display_name": types.StringValue("display_name"),
+ "ip": types.StringValue("ip"),
+ }),
+ }),
+ "session_persistence": types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{
+ "use_source_ip_address": types.BoolValue(true),
+ }),
+ }),
+ }),
+ },
+ &loadbalancer.UpdateLoadBalancerPayload{
+ ExternalAddress: new("external_address"),
+ Listeners: []loadbalancer.Listener{
+ {
+ DisplayName: new("display_name"),
+ Port: new(int32(80)),
+ Protocol: new(string(legacyLoadbalancer.LISTENERPROTOCOL_TCP)),
+ ServerNameIndicators: []loadbalancer.ServerNameIndicator{
+ {
+ Name: new("domain.com"),
+ },
+ },
+ TargetPool: new("target_pool"),
+ Tcp: new(loadbalancer.OptionsTCP{
+ IdleTimeout: new("50s"),
+ }),
+ Udp: new(loadbalancer.OptionsUDP{
+ IdleTimeout: new("50s"),
+ }),
+ },
+ },
+ Name: new("name"),
+ Networks: []loadbalancer.Network{
+ {
+ NetworkId: new("network_id"),
+ Role: new(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ },
+ {
+ NetworkId: new("network_id_2"),
+ Role: new(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ },
+ },
+ Options: &loadbalancer.LoadBalancerOptions{
+ AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
+ AllowedSourceRanges: []string{"cidr"},
+ },
+ PrivateNetworkOnly: new(true),
+ Observability: &loadbalancer.LoadbalancerOptionObservability{
+ Logs: &loadbalancer.LoadbalancerOptionLogs{
+ CredentialsRef: new("logs-credentials_ref"),
+ PushUrl: new("logs-push_url"),
+ },
+ Metrics: &loadbalancer.LoadbalancerOptionMetrics{
+ CredentialsRef: new("metrics-credentials_ref"),
+ PushUrl: new("metrics-push_url"),
+ },
+ },
+ },
+ TargetPools: []loadbalancer.TargetPool{
+ {
+ ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
+ HealthyThreshold: new(int32(1)),
+ Interval: new("2s"),
+ IntervalJitter: new("3s"),
+ Timeout: new("4s"),
+ UnhealthyThreshold: new(int32(5)),
+ },
+ Name: new("name"),
+ TargetPort: new(int32(80)),
+ Targets: []loadbalancer.Target{
+ {
+ DisplayName: new("display_name"),
+ Ip: new("ip"),
+ },
+ },
+ SessionPersistence: &loadbalancer.SessionPersistence{
+ UseSourceIpAddress: new(true),
+ },
+ },
+ },
+ },
+ true,
+ },
+ {
+ "service_plan_ok",
+ &Model{
+ PlanId: types.StringValue("p10"),
+ ExternalAddress: types.StringValue("external_address"),
+ Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{
+ types.ObjectValueMust(listenerTypes, map[string]attr.Value{
+ "display_name": types.StringValue("display_name"),
+ "port": types.Int32Value(80),
+ "protocol": types.StringValue(string(legacyLoadbalancer.LISTENERPROTOCOL_TCP)),
+ "server_name_indicators": types.ListValueMust(types.ObjectType{AttrTypes: serverNameIndicatorTypes}, []attr.Value{
+ types.ObjectValueMust(
+ serverNameIndicatorTypes,
+ map[string]attr.Value{
+ "name": types.StringValue("domain.com"),
+ },
+ ),
+ },
+ ),
+ "target_pool": types.StringValue("target_pool"),
+ "tcp": types.ObjectNull(tcpTypes),
+ "udp": types.ObjectNull(udpTypes),
+ }),
+ }),
+ Name: types.StringValue("name"),
+ Networks: types.ListValueMust(types.ObjectType{AttrTypes: networkTypes}, []attr.Value{
+ types.ObjectValueMust(networkTypes, map[string]attr.Value{
+ "network_id": types.StringValue("network_id"),
+ "role": types.StringValue(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ }),
+ types.ObjectValueMust(networkTypes, map[string]attr.Value{
+ "network_id": types.StringValue("network_id_2"),
+ "role": types.StringValue(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ }),
+ }),
+ Options: types.ObjectValueMust(
+ optionsTypes,
+ map[string]attr.Value{
+ "acl": types.SetValueMust(
+ types.StringType,
+ []attr.Value{types.StringValue("cidr")}),
+ "private_network_only": types.BoolValue(true),
+ "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{
+ "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{
+ "credentials_ref": types.StringValue("logs-credentials_ref"),
+ "push_url": types.StringValue("logs-push_url"),
+ }),
+ "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{
+ "credentials_ref": types.StringValue("metrics-credentials_ref"),
+ "push_url": types.StringValue("metrics-push_url"),
+ }),
+ }),
+ },
+ ),
+ TargetPools: types.ListValueMust(types.ObjectType{AttrTypes: targetPoolTypes}, []attr.Value{
+ types.ObjectValueMust(targetPoolTypes, map[string]attr.Value{
+ "active_health_check": types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{
+ "healthy_threshold": types.Int32Value(1),
+ "interval": types.StringValue("2s"),
+ "interval_jitter": types.StringValue("3s"),
+ "timeout": types.StringValue("4s"),
+ "unhealthy_threshold": types.Int32Value(5),
+ }),
+ "name": types.StringValue("name"),
+ "target_port": types.Int32Value(80),
+ "targets": types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{
+ types.ObjectValueMust(targetTypes, map[string]attr.Value{
+ "display_name": types.StringValue("display_name"),
+ "ip": types.StringValue("ip"),
+ }),
+ }),
+ "session_persistence": types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{
+ "use_source_ip_address": types.BoolValue(true),
+ }),
+ }),
+ }),
+ },
+ &loadbalancer.UpdateLoadBalancerPayload{
+ PlanId: new("p10"),
+ ExternalAddress: new("external_address"),
+ Listeners: []loadbalancer.Listener{
+ {
+ DisplayName: new("display_name"),
+ Port: new(int32(80)),
+ Protocol: new(string(legacyLoadbalancer.LISTENERPROTOCOL_TCP)),
+ ServerNameIndicators: []loadbalancer.ServerNameIndicator{
+ {
+ Name: new("domain.com"),
+ },
+ },
+ TargetPool: new("target_pool"),
+ },
+ },
+ Name: new("name"),
+ Networks: []loadbalancer.Network{
+ {
+ NetworkId: new("network_id"),
+ Role: new(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ },
+ {
+ NetworkId: new("network_id_2"),
+ Role: new(string(legacyLoadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
+ },
+ },
+ Options: &loadbalancer.LoadBalancerOptions{
+ AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
+ AllowedSourceRanges: []string{"cidr"},
+ },
+ PrivateNetworkOnly: new(true),
+ Observability: &loadbalancer.LoadbalancerOptionObservability{
+ Logs: &loadbalancer.LoadbalancerOptionLogs{
+ CredentialsRef: new("logs-credentials_ref"),
+ PushUrl: new("logs-push_url"),
+ },
+ Metrics: &loadbalancer.LoadbalancerOptionMetrics{
+ CredentialsRef: new("metrics-credentials_ref"),
+ PushUrl: new("metrics-push_url"),
+ },
+ },
+ },
+ TargetPools: []loadbalancer.TargetPool{
+ {
+ ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
+ HealthyThreshold: new(int32(1)),
+ Interval: new("2s"),
+ IntervalJitter: new("3s"),
+ Timeout: new("4s"),
+ UnhealthyThreshold: new(int32(5)),
+ },
+ Name: new("name"),
+ TargetPort: new(int32(80)),
+ Targets: []loadbalancer.Target{
+ {
+ DisplayName: new("display_name"),
+ Ip: new("ip"),
+ },
+ },
+ SessionPersistence: &loadbalancer.SessionPersistence{
+ UseSourceIpAddress: new(true),
+ },
+ },
+ },
+ },
+ true,
+ },
+ {
+ "nil_model",
+ nil,
+ nil,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ output, err := toUpdatePayload(context.Background(), tt.input)
+ if !tt.isValid && err == nil {
+ t.Fatalf("Should have failed")
+ }
+ if tt.isValid && err != nil {
+ t.Fatalf("Should not have failed: %v", err)
+ }
+ if tt.isValid {
+ diff := cmp.Diff(output, tt.expected)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go
index 37ff1885c..6809a0fc7 100644
--- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go
+++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"
loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api/wait"
@@ -95,14 +96,46 @@ var testConfigVarsMax = config.Variables{
func configVarsMinUpdated() config.Variables {
tempConfig := make(config.Variables, len(testConfigVarsMin))
maps.Copy(tempConfig, testConfigVarsMin)
- tempConfig["target_port"] = config.StringVariable("5431")
+ tempConfig["plan_id"] = config.StringVariable("p50")
+
+ tempConfig["target_port"] = config.StringVariable("6543")
+ tempConfig["target_pool_name"] = config.StringVariable("example-target-pool-updated")
+ tempConfig["target_display_name"] = config.StringVariable("example-target-updated")
+
+ tempConfig["listener_protocol"] = config.StringVariable("PROTOCOL_TCP")
+ tempConfig["listener_port"] = config.StringVariable("6543")
return tempConfig
}
func configVarsMaxUpdated() config.Variables {
tempConfig := make(config.Variables, len(testConfigVarsMax))
maps.Copy(tempConfig, testConfigVarsMax)
- tempConfig["sni_target_port"] = config.StringVariable("5431")
+ tempConfig["plan_id"] = config.StringVariable("p50")
+ tempConfig["acl"] = config.StringVariable("10.2.1.0/24")
+ tempConfig["target_display_name"] = config.StringVariable("example-target-updated")
+
+ tempConfig["sni_target_pool_name"] = config.StringVariable("example-target-pool-updated")
+ tempConfig["sni_target_port"] = config.StringVariable("6543")
+ tempConfig["sni_listener_port"] = config.StringVariable("6543")
+ tempConfig["sni_listener_protocol"] = config.StringVariable("PROTOCOL_TCP")
+ tempConfig["sni_idle_timeout"] = config.StringVariable("21s")
+ tempConfig["sni_listener_display_name"] = config.StringVariable("example-listener-updated")
+ tempConfig["sni_listener_server_name_indicators"] = config.StringVariable("")
+ tempConfig["sni_healthy_threshold"] = config.StringVariable("4")
+ tempConfig["sni_health_interval"] = config.StringVariable("12s")
+ tempConfig["sni_health_interval_jitter"] = config.StringVariable("7s")
+ tempConfig["sni_health_timeout"] = config.StringVariable("15s")
+ tempConfig["sni_unhealthy_threshold"] = config.StringVariable("4")
+ tempConfig["sni_use_source_ip_address"] = config.StringVariable("false")
+
+ tempConfig["udp_target_pool_name"] = config.StringVariable("udp-target-pool-updated")
+ tempConfig["udp_target_port"] = config.StringVariable("67")
+ tempConfig["udp_listener_port"] = config.StringVariable("67")
+ tempConfig["udp_idle_timeout"] = config.StringVariable("44s")
+ tempConfig["udp_listener_display_name"] = config.StringVariable("udp-listener-updated")
+
+ tempConfig["observability_logs_push_url"] = config.StringVariable("https://logs.observability.dummy.stackit.cloud")
+ tempConfig["observability_metrics_push_url"] = config.StringVariable("https://metrics.observability.dummy.stackit.cloud")
return tempConfig
}
@@ -136,6 +169,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) {
resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"),
resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"),
resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "version"),
// Loadbalancer observability credentials resource
resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])),
@@ -192,7 +226,12 @@ func TestAccLoadBalancerResourceMin(t *testing.T) {
"stackit_loadbalancer.loadbalancer", "security_group_id",
"data.stackit_loadbalancer.loadbalancer", "security_group_id",
),
- )},
+ resource.TestCheckResourceAttrPair(
+ "stackit_loadbalancer.loadbalancer", "version",
+ "data.stackit_loadbalancer.loadbalancer", "version",
+ ),
+ ),
+ },
// Import
{
ConfigVariables: testConfigVarsMin,
@@ -220,10 +259,46 @@ func TestAccLoadBalancerResourceMin(t *testing.T) {
{
ConfigVariables: configVarsMinUpdated(),
Config: testutil.NewConfigBuilder().BuildProviderConfig() + resourceMinConfig,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction("stackit_loadbalancer.loadbalancer", plancheck.ResourceActionUpdate),
+ plancheck.ExpectResourceAction("stackit_loadbalancer_observability_credential.obs_credential", plancheck.ResourceActionNoop),
+
+ plancheck.ExpectResourceAction("stackit_network.network", plancheck.ResourceActionNoop),
+ plancheck.ExpectResourceAction("stackit_network_interface.network_interface", plancheck.ResourceActionNoop),
+ plancheck.ExpectResourceAction("stackit_public_ip.public_ip", plancheck.ResourceActionNoop),
+ plancheck.ExpectResourceAction("stackit_server.server", plancheck.ResourceActionNoop),
+ },
+ },
Check: resource.ComposeAggregateTestCheckFunc(
- resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])),
- resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMin["loadbalancer_name"])),
+ // Load balancer instance resource
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(configVarsMinUpdated()["loadbalancer_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(configVarsMinUpdated()["target_pool_name"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(configVarsMinUpdated()["target_port"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.display_name", testutil.ConvertConfigVariable(configVarsMinUpdated()["target_display_name"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.ip"),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "listeners.0.display_name"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.port", testutil.ConvertConfigVariable(configVarsMinUpdated()["listener_port"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.protocol", testutil.ConvertConfigVariable(configVarsMinUpdated()["listener_protocol"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.target_pool", testutil.ConvertConfigVariable(configVarsMinUpdated()["target_pool_name"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(configVarsMinUpdated()["network_role"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"),
+ resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"),
+ resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"),
+ resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"),
+ resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "version"),
+
+ // Loadbalancer observability credentials resource
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "display_name", testutil.ConvertConfigVariable(configVarsMinUpdated()["obs_display_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "username", testutil.ConvertConfigVariable(configVarsMinUpdated()["obs_username"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "password", testutil.ConvertConfigVariable(configVarsMinUpdated()["obs_password"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer_observability_credential.obs_credential", "credentials_ref"),
),
},
// Deletion is done by the framework implicitly
@@ -250,6 +325,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) {
resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])),
resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "version"),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_display_name"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.port", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_port"])),
@@ -289,12 +365,12 @@ func TestAccLoadBalancerResourceMax(t *testing.T) {
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url", testutil.ConvertConfigVariable(testConfigVarsMax["observability_metrics_push_url"])),
// Loadbalancer observability credential resource
- resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "display_name", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_logs_name"])),
resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "username", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_logs_username"])),
resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "password", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_logs_password"])),
resource.TestCheckResourceAttrSet("stackit_loadbalancer_observability_credential.logs", "credentials_ref"),
- resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "display_name", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_metrics_name"])),
resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "username", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_metrics_username"])),
resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "password", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_metrics_password"])),
@@ -332,7 +408,8 @@ func TestAccLoadBalancerResourceMax(t *testing.T) {
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])),
resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"),
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])),
- resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"),
+ resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "version"),
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_pool_name"])),
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_port"])),
@@ -368,6 +445,10 @@ func TestAccLoadBalancerResourceMax(t *testing.T) {
"stackit_loadbalancer.loadbalancer", "security_group_id",
"data.stackit_loadbalancer.loadbalancer", "security_group_id",
),
+ resource.TestCheckResourceAttrPair(
+ "stackit_loadbalancer.loadbalancer", "version",
+ "data.stackit_loadbalancer.loadbalancer", "version",
+ ),
)},
// Import
{
@@ -396,10 +477,78 @@ func TestAccLoadBalancerResourceMax(t *testing.T) {
{
ConfigVariables: configVarsMaxUpdated(),
Config: testutil.NewConfigBuilder().BuildProviderConfig() + resourceMaxConfig,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction("stackit_loadbalancer.loadbalancer", plancheck.ResourceActionUpdate),
+ plancheck.ExpectResourceAction("stackit_loadbalancer_observability_credential.logs", plancheck.ResourceActionNoop),
+ plancheck.ExpectResourceAction("stackit_loadbalancer_observability_credential.metrics", plancheck.ResourceActionNoop),
+
+ plancheck.ExpectResourceAction("stackit_network.network", plancheck.ResourceActionNoop),
+ plancheck.ExpectResourceAction("stackit_network_interface.network_interface", plancheck.ResourceActionNoop),
+ plancheck.ExpectResourceAction("stackit_public_ip.public_ip", plancheck.ResourceActionNoop),
+ plancheck.ExpectResourceAction("stackit_server.server", plancheck.ResourceActionNoop),
+ },
+ },
Check: resource.ComposeAggregateTestCheckFunc(
- resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
- resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMax["loadbalancer_name"])),
+ // Load balancer instance resource
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["loadbalancer_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "plan_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["plan_id"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(configVarsMaxUpdated()["network_role"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(configVarsMaxUpdated()["disable_security_group_assignment"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "version"),
+
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.display_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_listener_display_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.port", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_listener_port"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.protocol", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_listener_protocol"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.target_pool", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_target_pool_name"])),
+ resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.server_name_indicators.0.name"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.tcp.idle_timeout", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_idle_timeout"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_target_pool_name"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_target_port"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.display_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["target_display_name"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.ip"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_healthy_threshold"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_health_interval"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval_jitter", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_health_interval_jitter"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.timeout", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_health_timeout"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.unhealthy_threshold", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_unhealthy_threshold"])),
+
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.display_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["udp_listener_display_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.port", testutil.ConvertConfigVariable(configVarsMaxUpdated()["udp_listener_port"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.protocol", testutil.ConvertConfigVariable(configVarsMaxUpdated()["udp_listener_protocol"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.target_pool", testutil.ConvertConfigVariable(configVarsMaxUpdated()["udp_target_pool_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.udp.idle_timeout", testutil.ConvertConfigVariable(configVarsMaxUpdated()["udp_idle_timeout"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.1.name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["udp_target_pool_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.1.target_port", testutil.ConvertConfigVariable(configVarsMaxUpdated()["udp_target_port"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.1.targets.0.display_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["target_display_name"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "target_pools.1.targets.0.ip"),
+
+ resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.session_persistence"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.private_network_only", testutil.ConvertConfigVariable(configVarsMaxUpdated()["private_network_only"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.acl.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["acl"])),
+
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"),
+ resource.TestCheckResourceAttrPair("stackit_loadbalancer_observability_credential.logs", "credentials_ref", "stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_logs_push_url"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"),
+ resource.TestCheckResourceAttrPair("stackit_loadbalancer_observability_credential.metrics", "credentials_ref", "stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_metrics_push_url"])),
+
+ // Loadbalancer observability credential resource
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "display_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_credential_logs_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "username", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_credential_logs_username"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "password", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_credential_logs_password"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer_observability_credential.logs", "credentials_ref"),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "display_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_credential_metrics_name"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "username", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_credential_metrics_username"])),
+ resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "password", testutil.ConvertConfigVariable(configVarsMaxUpdated()["observability_credential_metrics_password"])),
+ resource.TestCheckResourceAttrSet("stackit_loadbalancer_observability_credential.metrics", "credentials_ref"),
),
},
// Deletion is done by the framework implicitly
diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf
index 6e5edc60e..59503afe9 100644
--- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf
+++ b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf
@@ -109,9 +109,9 @@ resource "stackit_loadbalancer" "loadbalancer" {
timeout = var.sni_health_timeout
unhealthy_threshold = var.sni_unhealthy_threshold
}
- session_persistence = {
+ session_persistence = var.sni_use_source_ip_address ? {
use_source_ip_address = var.sni_use_source_ip_address
- }
+ } : null
},
{
name = var.udp_target_pool_name
@@ -130,11 +130,11 @@ resource "stackit_loadbalancer" "loadbalancer" {
port = var.sni_listener_port
protocol = var.sni_listener_protocol
target_pool = var.sni_target_pool_name
- server_name_indicators = [
+ server_name_indicators = var.sni_listener_server_name_indicators != "" ? [
{
name = var.sni_listener_server_name_indicators
}
- ]
+ ] : null
tcp = {
idle_timeout = var.sni_idle_timeout
}