Skip to content

Commit e4e2e55

Browse files
authored
feat(loadbalancer): support for idle timeout (TCP and UDP) (#1039)
1 parent e0b2c09 commit e4e2e55

File tree

8 files changed

+371
-75
lines changed

8 files changed

+371
-75
lines changed

docs/data-sources/loadbalancer.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Read-Only:
5757
- `port` (Number) Port number where we listen for traffic.
5858
- `protocol` (String) Protocol is the highest network protocol we understand to load balance.
5959
- `target_pool` (String) Reference target pool by target pool name.
60+
- `tcp` (Attributes) Options that are specific to the TCP protocol. (see [below for nested schema](#nestedatt--listeners--tcp))
61+
- `udp` (Attributes) Options that are specific to the UDP protocol. (see [below for nested schema](#nestedatt--listeners--udp))
6062

6163
<a id="nestedatt--listeners--server_name_indicators"></a>
6264
### Nested Schema for `listeners.server_name_indicators`
@@ -66,6 +68,22 @@ Optional:
6668
- `name` (String) A domain name to match in order to pass TLS traffic to the target pool in the current listener
6769

6870

71+
<a id="nestedatt--listeners--tcp"></a>
72+
### Nested Schema for `listeners.tcp`
73+
74+
Read-Only:
75+
76+
- `idle_timeout` (String) Time after which an idle connection is closed. The default value is set to 5 minutes, and the maximum value is one hour.
77+
78+
79+
<a id="nestedatt--listeners--udp"></a>
80+
### Nested Schema for `listeners.udp`
81+
82+
Read-Only:
83+
84+
- `idle_timeout` (String) Time after which an idle session is closed. The default value is set to 1 minute, and the maximum value is 2 minutes.
85+
86+
6987

7088
<a id="nestedatt--networks"></a>
7189
### Nested Schema for `networks`

docs/resources/loadbalancer.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ resource "stackit_loadbalancer" "example" {
9696
port = 80
9797
protocol = "PROTOCOL_TCP"
9898
target_pool = "example-target-pool"
99+
tcp = {
100+
idle_timeout = "90s"
101+
}
99102
}
100103
]
101104
networks = [
@@ -258,6 +261,8 @@ Optional:
258261

259262
- `display_name` (String)
260263
- `server_name_indicators` (Attributes List) A list of domain names to match in order to pass TLS traffic to the target pool in the current listener (see [below for nested schema](#nestedatt--listeners--server_name_indicators))
264+
- `tcp` (Attributes) Options that are specific to the TCP protocol. (see [below for nested schema](#nestedatt--listeners--tcp))
265+
- `udp` (Attributes) Options that are specific to the UDP protocol. (see [below for nested schema](#nestedatt--listeners--udp))
261266

262267
<a id="nestedatt--listeners--server_name_indicators"></a>
263268
### Nested Schema for `listeners.server_name_indicators`
@@ -267,6 +272,22 @@ Optional:
267272
- `name` (String) A domain name to match in order to pass TLS traffic to the target pool in the current listener
268273

269274

275+
<a id="nestedatt--listeners--tcp"></a>
276+
### Nested Schema for `listeners.tcp`
277+
278+
Optional:
279+
280+
- `idle_timeout` (String) 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
281+
282+
283+
<a id="nestedatt--listeners--udp"></a>
284+
### Nested Schema for `listeners.udp`
285+
286+
Optional:
287+
288+
- `idle_timeout` (String) 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
289+
290+
270291

271292
<a id="nestedatt--networks"></a>
272293
### Nested Schema for `networks`

examples/resources/stackit_loadbalancer/resource.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ resource "stackit_loadbalancer" "example" {
7777
port = 80
7878
protocol = "PROTOCOL_TCP"
7979
target_pool = "example-target-pool"
80+
tcp = {
81+
idle_timeout = "90s"
82+
}
8083
}
8184
]
8285
networks = [

stackit/internal/services/loadbalancer/loadbalancer/datasource.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe
107107
"targets.display_name": "Target display name",
108108
"ip": "Target IP",
109109
"region": "The resource region. If not defined, the provider region is used.",
110+
"tcp_options": "Options that are specific to the TCP protocol.",
111+
"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.",
112+
"udp_options": "Options that are specific to the UDP protocol.",
113+
"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.",
110114
}
111115

112116
resp.Schema = schema.Schema{
@@ -171,6 +175,26 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe
171175
Description: descriptions["target_pool"],
172176
Computed: true,
173177
},
178+
"tcp": schema.SingleNestedAttribute{
179+
Description: descriptions["tcp_options"],
180+
Computed: true,
181+
Attributes: map[string]schema.Attribute{
182+
"idle_timeout": schema.StringAttribute{
183+
Description: descriptions["tcp_options_idle_timeout"],
184+
Computed: true,
185+
},
186+
},
187+
},
188+
"udp": schema.SingleNestedAttribute{
189+
Description: descriptions["udp_options"],
190+
Computed: true,
191+
Attributes: map[string]schema.Attribute{
192+
"idle_timeout": schema.StringAttribute{
193+
Description: descriptions["udp_options_idle_timeout"],
194+
Computed: true,
195+
},
196+
},
197+
},
174198
},
175199
},
176200
},

stackit/internal/services/loadbalancer/loadbalancer/resource.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ type listener struct {
7272
Protocol types.String `tfsdk:"protocol"`
7373
ServerNameIndicators types.List `tfsdk:"server_name_indicators"`
7474
TargetPool types.String `tfsdk:"target_pool"`
75+
TCP types.Object `tfsdk:"tcp"`
76+
UDP types.Object `tfsdk:"udp"`
7577
}
7678

7779
// Types corresponding to listener
@@ -81,6 +83,8 @@ var listenerTypes = map[string]attr.Type{
8183
"protocol": types.StringType,
8284
"server_name_indicators": types.ListType{ElemType: types.ObjectType{AttrTypes: serverNameIndicatorTypes}},
8385
"target_pool": types.StringType,
86+
"tcp": types.ObjectType{AttrTypes: tcpTypes},
87+
"udp": types.ObjectType{AttrTypes: udpTypes},
8488
}
8589

8690
// Struct corresponding to listener.ServerNameIndicators[i]
@@ -93,6 +97,22 @@ var serverNameIndicatorTypes = map[string]attr.Type{
9397
"name": types.StringType,
9498
}
9599

100+
type tcp struct {
101+
IdleTimeout types.String `tfsdk:"idle_timeout"`
102+
}
103+
104+
var tcpTypes = map[string]attr.Type{
105+
"idle_timeout": types.StringType,
106+
}
107+
108+
type udp struct {
109+
IdleTimeout types.String `tfsdk:"idle_timeout"`
110+
}
111+
112+
var udpTypes = map[string]attr.Type{
113+
"idle_timeout": types.StringType,
114+
}
115+
96116
// Struct corresponding to Model.Networks[i]
97117
type network struct {
98118
NetworkId types.String `tfsdk:"network_id"`
@@ -345,6 +365,10 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques
345365
"ip": "Target IP",
346366
"region": "The resource region. If not defined, the provider region is used.",
347367
"security_group_id": "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`.",
368+
"tcp_options": "Options that are specific to the TCP protocol.",
369+
"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",
370+
"udp_options": "Options that are specific to the UDP protocol.",
371+
"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",
348372
}
349373

350374
resp.Schema = schema.Schema{
@@ -456,6 +480,27 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
456480
stringplanmodifier.UseStateForUnknown(),
457481
},
458482
},
483+
"tcp": schema.SingleNestedAttribute{
484+
Description: descriptions["tcp_options"],
485+
Optional: true,
486+
Attributes: map[string]schema.Attribute{
487+
"idle_timeout": schema.StringAttribute{
488+
Description: descriptions["tcp_options_idle_timeout"],
489+
Optional: true,
490+
},
491+
},
492+
},
493+
"udp": schema.SingleNestedAttribute{
494+
Description: descriptions["udp_options"],
495+
Optional: true,
496+
Computed: false,
497+
Attributes: map[string]schema.Attribute{
498+
"idle_timeout": schema.StringAttribute{
499+
Description: descriptions["udp_options_idle_timeout"],
500+
Optional: true,
501+
},
502+
},
503+
},
459504
},
460505
},
461506
},
@@ -907,6 +952,7 @@ func (r *loadBalancerResource) ImportState(ctx context.Context, req resource.Imp
907952
tflog.Info(ctx, "Load balancer state imported")
908953
}
909954

955+
// toCreatePayload and all other toX functions in this file turn a Terraform load balancer model into a createLoadBalancerPayload to be used with the load balancer API.
910956
func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoadBalancerPayload, error) {
911957
if model == nil {
912958
return nil, fmt.Errorf("nil model")
@@ -963,12 +1009,22 @@ func toListenersPayload(ctx context.Context, model *Model) (*[]loadbalancer.List
9631009
if err != nil {
9641010
return nil, fmt.Errorf("converting index %d: converting server_name_indicator: %w", i, err)
9651011
}
1012+
tcp, err := toTCP(ctx, &listenerModel)
1013+
if err != nil {
1014+
return nil, fmt.Errorf("converting index %d: converting tcp: %w", i, err)
1015+
}
1016+
udp, err := toUDP(ctx, &listenerModel)
1017+
if err != nil {
1018+
return nil, fmt.Errorf("converting index %d: converting udp: %w", i, err)
1019+
}
9661020
payload = append(payload, loadbalancer.Listener{
9671021
DisplayName: conversion.StringValueToPointer(listenerModel.DisplayName),
9681022
Port: conversion.Int64ValueToPointer(listenerModel.Port),
9691023
Protocol: loadbalancer.ListenerGetProtocolAttributeType(conversion.StringValueToPointer(listenerModel.Protocol)),
9701024
ServerNameIndicators: serverNameIndicatorsPayload,
9711025
TargetPool: conversion.StringValueToPointer(listenerModel.TargetPool),
1026+
Tcp: tcp,
1027+
Udp: udp,
9721028
})
9731029
}
9741030

@@ -997,6 +1053,44 @@ func toServerNameIndicatorsPayload(ctx context.Context, l *listener) (*[]loadbal
9971053
return &payload, nil
9981054
}
9991055

1056+
func toTCP(ctx context.Context, listener *listener) (*loadbalancer.OptionsTCP, error) {
1057+
if listener.TCP.IsNull() || listener.TCP.IsUnknown() {
1058+
return nil, nil
1059+
}
1060+
1061+
tcp := tcp{}
1062+
diags := listener.TCP.As(ctx, &tcp, basetypes.ObjectAsOptions{})
1063+
if diags.HasError() {
1064+
return nil, core.DiagsToError(diags)
1065+
}
1066+
if tcp.IdleTimeout.IsNull() || tcp.IdleTimeout.IsUnknown() {
1067+
return nil, nil
1068+
}
1069+
1070+
return &loadbalancer.OptionsTCP{
1071+
IdleTimeout: tcp.IdleTimeout.ValueStringPointer(),
1072+
}, nil
1073+
}
1074+
1075+
func toUDP(ctx context.Context, listener *listener) (*loadbalancer.OptionsUDP, error) {
1076+
if listener.UDP.IsNull() || listener.UDP.IsUnknown() {
1077+
return nil, nil
1078+
}
1079+
1080+
udp := udp{}
1081+
diags := listener.UDP.As(ctx, &udp, basetypes.ObjectAsOptions{})
1082+
if diags.HasError() {
1083+
return nil, core.DiagsToError(diags)
1084+
}
1085+
if udp.IdleTimeout.IsNull() || udp.IdleTimeout.IsUnknown() {
1086+
return nil, nil
1087+
}
1088+
1089+
return &loadbalancer.OptionsUDP{
1090+
IdleTimeout: udp.IdleTimeout.ValueStringPointer(),
1091+
}, nil
1092+
}
1093+
10001094
func toNetworksPayload(ctx context.Context, model *Model) (*[]loadbalancer.Network, error) {
10011095
if model.Networks.IsNull() || model.Networks.IsUnknown() {
10021096
return nil, nil
@@ -1222,6 +1316,7 @@ func toTargetsPayload(ctx context.Context, tp *targetPool) (*[]loadbalancer.Targ
12221316
return &payload, nil
12231317
}
12241318

1319+
// mapFields and all other map functions in this file translate an API resource into a Terraform model.
12251320
func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, region string) error {
12261321
if lb == nil {
12271322
return fmt.Errorf("response input is nil")
@@ -1292,6 +1387,16 @@ func mapListeners(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error {
12921387
return fmt.Errorf("mapping index %d, field serverNameIndicators: %w", i, err)
12931388
}
12941389

1390+
err = mapTCP(listenerResp.Tcp, listenerMap)
1391+
if err != nil {
1392+
return fmt.Errorf("mapping index %d, field tcp: %w", i, err)
1393+
}
1394+
1395+
err = mapUDP(listenerResp.Udp, listenerMap)
1396+
if err != nil {
1397+
return fmt.Errorf("mapping index %d, field udp: %w", i, err)
1398+
}
1399+
12951400
listenerTF, diags := types.ObjectValue(listenerTypes, listenerMap)
12961401
if diags.HasError() {
12971402
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
@@ -1344,6 +1449,40 @@ func mapServerNameIndicators(serverNameIndicatorsResp *[]loadbalancer.ServerName
13441449
return nil
13451450
}
13461451

1452+
func mapTCP(tcp *loadbalancer.OptionsTCP, listener map[string]attr.Value) error {
1453+
if tcp == nil || tcp.IdleTimeout == nil || *tcp.IdleTimeout == "" {
1454+
listener["tcp"] = types.ObjectNull(tcpTypes)
1455+
return nil
1456+
}
1457+
1458+
tcpAttr, diags := types.ObjectValue(tcpTypes, map[string]attr.Value{
1459+
"idle_timeout": types.StringValue(*tcp.IdleTimeout),
1460+
})
1461+
if diags.HasError() {
1462+
return core.DiagsToError(diags)
1463+
}
1464+
1465+
listener["tcp"] = tcpAttr
1466+
return nil
1467+
}
1468+
1469+
func mapUDP(udp *loadbalancer.OptionsUDP, listener map[string]attr.Value) error {
1470+
if udp == nil || udp.IdleTimeout == nil || *udp.IdleTimeout == "" {
1471+
listener["udp"] = types.ObjectNull(udpTypes)
1472+
return nil
1473+
}
1474+
1475+
udpAttr, diags := types.ObjectValue(udpTypes, map[string]attr.Value{
1476+
"idle_timeout": types.StringValue(*udp.IdleTimeout),
1477+
})
1478+
if diags.HasError() {
1479+
return core.DiagsToError(diags)
1480+
}
1481+
1482+
listener["udp"] = udpAttr
1483+
return nil
1484+
}
1485+
13471486
func mapNetworks(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error {
13481487
if loadBalancerResp.Networks == nil {
13491488
m.Networks = types.ListNull(types.ObjectType{AttrTypes: networkTypes})

0 commit comments

Comments
 (0)