feat(loadbalancer): support service plan attribute (#858)

relates to STACKITLB-250

Co-authored-by: Christian Hamm <Christian.Hamm@mail.schwarz>
Co-authored-by: Ruben Hönle <git@hoenle.xyz>
This commit is contained in:
Christian Hamm 2025-07-15 16:09:42 +02:00 committed by GitHub
parent 3255f1e28d
commit ab232d6cb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 174 additions and 0 deletions

View file

@ -38,6 +38,7 @@ data "stackit_loadbalancer" "example" {
- `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners))
- `networks` (Attributes List) List of networks that listeners and targets reside in. (see [below for nested schema](#nestedatt--networks))
- `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options))
- `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`.
- `private_address` (String) Transient private Load Balancer IP address. It can change any time.
- `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))

View file

@ -70,6 +70,7 @@ resource "stackit_server" "boot-from-image" {
resource "stackit_loadbalancer" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-load-balancer"
plan_id = "p10"
target_pools = [
{
name = "example-target-pool"
@ -125,6 +126,7 @@ resource "stackit_loadbalancer" "example" {
- `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed.
- `options` (Attributes) Defines any optional functionality you want to have enabled on your load balancer. (see [below for nested schema](#nestedatt--options))
- `plan_id` (String) The service plan ID. If not defined, the default service plan is `p10`. Possible values are: `p10`, `p50`, `p250`, `p750`.
- `region` (String) The resource region. If not defined, the provider region is used.
### Read-Only

View file

@ -51,6 +51,7 @@ resource "stackit_server" "boot-from-image" {
resource "stackit_loadbalancer" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-load-balancer"
plan_id = "p10"
target_pools = [
{
name = "example-target-pool"

View file

@ -62,6 +62,8 @@ 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) {
servicePlanOptions := []string{"p10", "p50", "p250", "p750"}
descriptions := map[string]string{
"main": "Load Balancer data source schema. Must have a `region` specified in the provider configuration.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".",
@ -72,6 +74,7 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe
"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.",
"plan_id": "The service plan ID. If not defined, the default service plan is `p10`. " + utils.FormatPossibleValues(servicePlanOptions...),
"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.",
@ -122,6 +125,10 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe
Description: descriptions["external_address"],
Computed: true,
},
"plan_id": schema.StringAttribute{
Description: descriptions["plan_id"],
Computed: true,
},
"listeners": schema.ListNestedAttribute{
Description: descriptions["listeners"],
Computed: true,

View file

@ -53,6 +53,7 @@ type Model struct {
ExternalAddress types.String `tfsdk:"external_address"`
Listeners types.List `tfsdk:"listeners"`
Name types.String `tfsdk:"name"`
PlanId types.String `tfsdk:"plan_id"`
Networks types.List `tfsdk:"networks"`
Options types.Object `tfsdk:"options"`
PrivateAddress types.String `tfsdk:"private_address"`
@ -295,6 +296,7 @@ func (r *loadBalancerResource) Configure(ctx context.Context, req resource.Confi
func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
protocolOptions := []string{"PROTOCOL_UNSPECIFIED", "PROTOCOL_TCP", "PROTOCOL_UDP", "PROTOCOL_TCP_PROXY", "PROTOCOL_TLS_PASSTHROUGH"}
roleOptions := []string{"ROLE_UNSPECIFIED", "ROLE_LISTENERS_AND_TARGETS", "ROLE_LISTENERS", "ROLE_TARGETS"}
servicePlanOptions := []string{"p10", "p50", "p250", "p750"}
descriptions := map[string]string{
"main": "Load Balancer resource schema.",
@ -306,6 +308,7 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques
"protocol": "Protocol is the highest network protocol we understand to load balance. " + utils.SupportedValuesDocumentation(protocolOptions),
"target_pool": "Reference target pool by target pool name.",
"name": "Load balancer name.",
"plan_id": "The service plan ID. If not defined, the default service plan is `p10`. " + utils.FormatPossibleValues(servicePlanOptions...),
"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. " + utils.SupportedValuesDocumentation(roleOptions),
@ -370,6 +373,14 @@ The example below creates the supporting infrastructure using the STACKIT Terraf
stringplanmodifier.RequiresReplace(),
},
},
"plan_id": schema.StringAttribute{
Description: descriptions["plan_id"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"listeners": schema.ListNestedAttribute{
Description: descriptions["listeners"],
Required: true,
@ -899,6 +910,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoa
ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress),
Listeners: listenersPayload,
Name: conversion.StringValueToPointer(model.Name),
PlanId: conversion.StringValueToPointer(model.PlanId),
Networks: networksPayload,
Options: optionsPayload,
TargetPools: targetPoolsPayload,
@ -1206,6 +1218,7 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg
m.Name = types.StringValue(name)
m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), name)
m.PlanId = types.StringPointerValue(lb.PlanId)
m.ExternalAddress = types.StringPointerValue(lb.ExternalAddress)
m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress)

View file

@ -184,6 +184,149 @@ func TestToCreatePayload(t *testing.T) {
},
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.Int64Value(80),
"protocol": types.StringValue(string(loadbalancer.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"),
}),
}),
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(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)),
}),
types.ObjectValueMust(networkTypes, map[string]attr.Value{
"network_id": types.StringValue("network_id_2"),
"role": types.StringValue(string(loadbalancer.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.Int64Value(1),
"interval": types.StringValue("2s"),
"interval_jitter": types.StringValue("3s"),
"timeout": types.StringValue("4s"),
"unhealthy_threshold": types.Int64Value(5),
}),
"name": types.StringValue("name"),
"target_port": types.Int64Value(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.CreateLoadBalancerPayload{
PlanId: utils.Ptr("p10"),
ExternalAddress: utils.Ptr("external_address"),
Listeners: &[]loadbalancer.Listener{
{
DisplayName: utils.Ptr("display_name"),
Port: utils.Ptr(int64(80)),
Protocol: loadbalancer.LISTENERPROTOCOL_TCP.Ptr(),
ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{
{
Name: utils.Ptr("domain.com"),
},
},
TargetPool: utils.Ptr("target_pool"),
},
},
Name: utils.Ptr("name"),
Networks: &[]loadbalancer.Network{
{
NetworkId: utils.Ptr("network_id"),
Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(),
},
{
NetworkId: utils.Ptr("network_id_2"),
Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(),
},
},
Options: &loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
AllowedSourceRanges: &[]string{"cidr"},
},
PrivateNetworkOnly: utils.Ptr(true),
Observability: &loadbalancer.LoadbalancerOptionObservability{
Logs: &loadbalancer.LoadbalancerOptionLogs{
CredentialsRef: utils.Ptr("logs-credentials_ref"),
PushUrl: utils.Ptr("logs-push_url"),
},
Metrics: &loadbalancer.LoadbalancerOptionMetrics{
CredentialsRef: utils.Ptr("metrics-credentials_ref"),
PushUrl: utils.Ptr("metrics-push_url"),
},
},
},
TargetPools: &[]loadbalancer.TargetPool{
{
ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{
HealthyThreshold: utils.Ptr(int64(1)),
Interval: utils.Ptr("2s"),
IntervalJitter: utils.Ptr("3s"),
Timeout: utils.Ptr("4s"),
UnhealthyThreshold: utils.Ptr(int64(5)),
},
Name: utils.Ptr("name"),
TargetPort: utils.Ptr(int64(80)),
Targets: &[]loadbalancer.Target{
{
DisplayName: utils.Ptr("display_name"),
Ip: utils.Ptr("ip"),
},
},
SessionPersistence: &loadbalancer.SessionPersistence{
UseSourceIpAddress: utils.Ptr(true),
},
},
},
},
true,
},
{
"nil_model",
nil,

View file

@ -30,6 +30,7 @@ var resourceMaxConfig string
var testConfigVarsMin = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"plan_id": config.StringVariable("p10"),
"network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))),
"server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))),
"loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))),
@ -47,6 +48,7 @@ var testConfigVarsMin = config.Variables{
var testConfigVarsMax = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"plan_id": config.StringVariable("p10"),
"network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))),
"server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))),
"loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))),
@ -146,6 +148,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) {
// Load balancer instance
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])),
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMin["loadbalancer_name"])),
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMin["plan_id"])),
resource.TestCheckResourceAttrPair(
"data.stackit_loadbalancer.loadbalancer", "project_id",
"stackit_loadbalancer.loadbalancer", "project_id",
@ -218,6 +221,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) {
// Load balancer instance resource
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMax["loadbalancer_name"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMax["plan_id"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["target_pool_name"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(testConfigVarsMax["target_port"])),
resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["target_display_name"])),
@ -278,6 +282,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) {
// Load balancer instance
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMax["loadbalancer_name"])),
resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMax["plan_id"])),
resource.TestCheckResourceAttrPair(
"data.stackit_loadbalancer.loadbalancer", "project_id",
"stackit_loadbalancer.loadbalancer", "project_id",

View file

@ -4,6 +4,7 @@ variable "network_name" {}
variable "server_name" {}
variable "loadbalancer_name" {}
variable "plan_id" {}
variable "target_pool_name" {}
variable "target_port" {}
variable "target_display_name" {}
@ -73,6 +74,7 @@ resource "stackit_server" "server" {
resource "stackit_loadbalancer" "loadbalancer" {
project_id = var.project_id
name = var.loadbalancer_name
plan_id = var.plan_id
target_pools = [
{
name = var.target_pool_name