diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 009f543f..d52e7aab 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -119,7 +119,6 @@ resource "stackit_loadbalancer" "example" { ### Required -- `external_address` (String) External Load Balancer IP address where this Load Balancer is exposed. - `listeners` (Attributes List) List of all listeners which will accept traffic. Limited to 20. (see [below for nested schema](#nestedatt--listeners)) - `name` (String) Load balancer name. - `networks` (Attributes List) List of networks that listeners and targets reside in. (see [below for nested schema](#nestedatt--networks)) @@ -128,6 +127,7 @@ resource "stackit_loadbalancer" "example" { ### Optional +- `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)) - `region` (String) The resource region. If not defined, the provider region is used. diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 8cea9a20..82692d85 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -237,6 +238,43 @@ func (r *loadBalancerResource) ModifyPlan(ctx context.Context, req resource.Modi } } +// ConfigValidators validates the resource configuration +func (r *loadBalancerResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + // validation is done in extracted func so it's easier to unit-test it + validateConfig(ctx, &resp.Diagnostics, &model) +} + +func validateConfig(ctx context.Context, diags *diag.Diagnostics, model *Model) { + externalAddressIsSet := !model.ExternalAddress.IsNull() + + lbOptions, err := toOptionsPayload(ctx, model) + if err != nil || lbOptions == nil { + // private_network_only is not set and external_address is not set + if !externalAddressIsSet { + core.LogAndAddError(ctx, diags, "Error configuring load balancer", fmt.Sprintf("You need to provide either the `options.private_network_only = true` or `external_address` field. %v", err)) + } + return + } + if lbOptions.PrivateNetworkOnly == nil || !*lbOptions.PrivateNetworkOnly { + // private_network_only is not set or false and external_address is not set + if !externalAddressIsSet { + core.LogAndAddError(ctx, diags, "Error configuring load balancer", "You need to provide either the `options.private_network_only = true` or `external_address` field.") + } + return + } + + // Both are set + if *lbOptions.PrivateNetworkOnly && externalAddressIsSet { + core.LogAndAddError(ctx, diags, "Error configuring load balancer", "You need to provide either the `options.private_network_only = true` or `external_address` field.") + } +} + // Configure adds the provider configured client to the resource. func (r *loadBalancerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool @@ -327,7 +365,7 @@ The example below creates the supporting infrastructure using the STACKIT Terraf }, "external_address": schema.StringAttribute{ Description: descriptions["external_address"], - Required: true, + Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go index 8873e936..47a4018e 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go @@ -7,11 +7,16 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) +const ( + testExternalAddress = "95.46.74.109" +) + func TestToCreatePayload(t *testing.T) { tests := []struct { description string @@ -688,3 +693,84 @@ func TestMapFields(t *testing.T) { }) } } + +func Test_validateConfig(t *testing.T) { + type args struct { + ExternalAddress *string + PrivateNetworkOnly *bool + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "happy case 1: private_network_only is not set and external_address is set", + args: args{ + ExternalAddress: utils.Ptr(testExternalAddress), + PrivateNetworkOnly: nil, + }, + wantErr: false, + }, + { + name: "happy case 2: private_network_only is set to false and external_address is set", + args: args{ + ExternalAddress: utils.Ptr(testExternalAddress), + PrivateNetworkOnly: utils.Ptr(false), + }, + wantErr: false, + }, + { + name: "happy case 3: private_network_only is set to true and external_address is not set", + args: args{ + ExternalAddress: nil, + PrivateNetworkOnly: utils.Ptr(true), + }, + wantErr: false, + }, + { + name: "error case 1: private_network_only and external_address are set", + args: args{ + ExternalAddress: utils.Ptr(testExternalAddress), + PrivateNetworkOnly: utils.Ptr(true), + }, + wantErr: true, + }, + { + name: "error case 2: private_network_only is not set and external_address is not set", + args: args{ + ExternalAddress: nil, + PrivateNetworkOnly: nil, + }, + wantErr: true, + }, + { + name: "error case 3: private_network_only is set to false and external_address is not set", + args: args{ + ExternalAddress: nil, + PrivateNetworkOnly: utils.Ptr(false), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + diags := diag.Diagnostics{} + model := &Model{ + ExternalAddress: types.StringPointerValue(tt.args.ExternalAddress), + Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ + "acl": types.SetNull(types.StringType), + "observability": types.ObjectNull(observabilityTypes), + "private_network_only": types.BoolPointerValue(tt.args.PrivateNetworkOnly), + }), + } + + validateConfig(ctx, &diags, model) + + if diags.HasError() != tt.wantErr { + t.Errorf("validateConfig() = %v, want %v", diags.HasError(), tt.wantErr) + } + }) + } +}