Onboard Load Balancer (part 1: implement creation payload helpers) (#107)

* Add initial resource schema and model

* Configure client

* Implement toCreatePayload and test

* Unwire load balancer resource from the provider

* Add schema fields descriptions

* Review adjustments

* Lint adjustments
This commit is contained in:
João Palet 2023-10-27 11:40:29 +02:00 committed by GitHub
parent 529225d06b
commit 8323db836d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 753 additions and 0 deletions

1
go.mod
View file

@ -14,6 +14,7 @@ require (
github.com/stackitcloud/stackit-sdk-go/core v0.3.0
github.com/stackitcloud/stackit-sdk-go/services/argus v0.5.0
github.com/stackitcloud/stackit-sdk-go/services/dns v0.4.0
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.4.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.5.0
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.5.0
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.5.0

2
go.sum
View file

@ -137,6 +137,8 @@ github.com/stackitcloud/stackit-sdk-go/services/argus v0.5.0 h1:BhmXA4W9P0/ttfX8
github.com/stackitcloud/stackit-sdk-go/services/argus v0.5.0/go.mod h1:fXyRrqMy2UegCRYXBYpVHcGLU41Ms0LCeTzxoYde7OI=
github.com/stackitcloud/stackit-sdk-go/services/dns v0.4.0 h1:UKHDM/hKTkv5rsOH+a/7osGQaWq20MvKwETpico8akE=
github.com/stackitcloud/stackit-sdk-go/services/dns v0.4.0/go.mod h1:Eyn/LRYMRtc4BB8uBxV21QA/xMUdUTkTG5ub0Q6pLVM=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.4.0 h1:YekLgXsp+nGjU3YY0mBDMc1lfNJnAuxoL1Ll7YWwv5k=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.4.0/go.mod h1:1NYHA58uZ6FJiTHBK3+HveSsz16PbuW0/wstq6iVT0I=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.5.0 h1:Y/AX0/yDcVvFZrK4uLJX26AdYsLxVxBlWt1mvgt/bG4=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.5.0/go.mod h1:Y0rtIUiDcfJLTSNbLX7wQ2N9nF1AC/WdAt45WZvZ1AM=
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.5.0 h1:VFm22pao/FZRVQidPA3bg2mR76uIG8Mc2Um6EBN4rPk=

View file

@ -19,6 +19,7 @@ type ProviderData struct {
Region string
ArgusCustomEndpoint string
DnsCustomEndpoint string
LoadBalancerCustomEndpoint string
LogMeCustomEndpoint string
MariaDBCustomEndpoint string
MongoDBFlexCustomEndpoint string

View file

@ -0,0 +1,578 @@
package loadbalancer
import (
"context"
"fmt"
"time"
"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/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &projectResource{}
_ resource.ResourceWithConfigure = &projectResource{}
_ resource.ResourceWithImportState = &projectResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ExternalAddress types.String `tfsdk:"external_address"`
Listeners []Listener `tfsdk:"listeners"`
Name types.String `tfsdk:"name"`
Networks []Network `tfsdk:"networks"`
Options types.Object `tfsdk:"options"`
PrivateAddress types.String `tfsdk:"private_address"`
TargetPools []TargetPool `tfsdk:"target_pools"`
}
// Struct corresponding to each Model.Listener
type Listener struct {
DisplayName types.String `tfsdk:"display_name"`
Name types.String `tfsdk:"name"`
Port types.Int64 `tfsdk:"port"`
Protocol types.String `tfsdk:"protocol"`
TargetPool types.String `tfsdk:"target_pool"`
}
// Struct corresponding to each Model.Network
type Network struct {
NetworkId types.String `tfsdk:"network_id"`
Role types.String `tfsdk:"role"`
}
// Struct corresponding to Model.Options
type Options struct {
ACL types.List `tfsdk:"acl"`
PrivateNetworkOnly types.Bool `tfsdk:"private_network_only"`
}
// Types corresponding to Options
var optionsTypes = map[string]attr.Type{
"acl": basetypes.ListType{ElemType: basetypes.StringType{}},
"private_network_only": basetypes.BoolType{},
}
// Struct corresponding to each Model.TargetPool
type TargetPool struct {
ActiveHealthCheck types.Object `tfsdk:"active_health_check"`
Name types.String `tfsdk:"name"`
TargetPort types.Int64 `tfsdk:"target_port"`
Targets []Target `tfsdk:"targets"`
}
// Struct corresponding to each Model.TargetPool.ActiveHealthCheck
type ActiveHealthCheck struct {
HealthyThreshold types.Int64 `tfsdk:"healthy_threshold"`
Interval types.String `tfsdk:"interval"`
IntervalJitter types.String `tfsdk:"interval_jitter"`
Timeout types.String `tfsdk:"timeout"`
UnhealthyThreshold types.Int64 `tfsdk:"unhealthy_threshold"`
}
// Types corresponding to ActiveHealthCheck
var activeHealthCheckTypes = map[string]attr.Type{
"healthy_threshold": basetypes.Int64Type{},
"interval": basetypes.StringType{},
"interval_jitter": basetypes.StringType{},
"timeout": basetypes.StringType{},
"unhealthy_threshold": basetypes.Int64Type{},
}
// Struct corresponding to each Model.TargetPool.Targets
type Target struct {
DisplayName types.String `tfsdk:"display_name"`
Ip types.String `tfsdk:"ip"`
}
// NewProjectResource is a helper function to simplify the provider implementation.
func NewProjectResource() resource.Resource {
return &projectResource{}
}
// projectResource is the resource implementation.
type projectResource struct {
client *loadbalancer.APIClient
}
// Metadata returns the resource type name.
func (r *projectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_loadbalancer"
}
// Configure adds the provider configured client to the resource.
func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
var apiClient *loadbalancer.APIClient
var err error
if providerData.LoadBalancerCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "loadbalancer_custom_endpoint", providerData.LoadBalancerCustomEndpoint)
apiClient, err = loadbalancer.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.LoadBalancerCustomEndpoint),
)
} else {
apiClient, err = loadbalancer.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithServiceAccountEmail(providerData.ServiceAccountEmail),
)
}
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
return
}
r.client = apiClient
tflog.Info(ctx, "Load Balancer client configured")
}
// Schema defines the schema for the resource.
func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
descriptions := map[string]string{
"main": "Load Balancer resource schema.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"`name`\".",
"project_id": "STACKIT project ID to which the Load Balancer is associated.",
"external_address": "External Load Balancer IP address where this Load Balancer is exposed.",
"listeners": "List of all listeners which will accept traffic. Limited to 20.",
"listeners.name": "Will be used to reference a listener and will replace display name in the future.",
"port": "Port number where we listen for traffic.",
"protocol": "Protocol is the highest network protocol we understand to load balance.",
"target_pool": "Reference target pool by target pool name.",
"name": "Load balancer name.",
"networks": "List of networks that listeners and targets reside in.",
"network_id": "Openstack network ID.",
"role": "The role defines how the load balancer is using the network.",
"options": "Defines any optional functionality you want to have enabled on your load balancer.",
"acl": "Load Balancer is accessible only from an IP address in this range.",
"private_network_only": "If true, Load Balancer is accessible only via a private network IP address.",
"private_address": "Transient private Load Balancer IP address. It can change any time.",
"target_pools": "List of all target pools which will be used in the Load Balancer. Limited to 20.",
"healthy_threshold": "Healthy threshold of the health checking.",
"interval": "Interval duration of health checking in seconds.",
"interval_jitter": "Interval duration threshold of the health checking in seconds.",
"timeout": "Active health checking timeout duration in seconds.",
"unhealthy_threshold": "Unhealthy threshold of the health checking.",
"target_pools.name": "Target pool name.",
"target_port": "Identical port number where each target listens for traffic.",
"targets": "List of all targets which will be used in the pool. Limited to 250.",
"targets.display_name": "Target display name",
"ip": "Target IP",
}
resp.Schema = schema.Schema{
Description: descriptions["main"],
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"project_id": schema.StringAttribute{
Description: "STACKIT project ID to which the dns record set is associated.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
},
},
"external_address": schema.StringAttribute{
Description: descriptions["external_address"],
Optional: true,
Computed: true,
},
"listeners": schema.ListNestedAttribute{
Description: descriptions["listeners"],
Optional: true,
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"display_name": schema.StringAttribute{
Description: descriptions["listeners.display_name"],
Optional: true,
Computed: true,
},
"name": schema.StringAttribute{
Description: descriptions["listeners.display_name"],
Computed: true,
},
"port": schema.Int64Attribute{
Description: descriptions["port"],
Optional: true,
Computed: true,
},
"protocol": schema.StringAttribute{
Description: descriptions["protocol"],
Optional: true,
Computed: true,
},
"target_pool": schema.StringAttribute{
Description: descriptions["target_pool"],
Optional: true,
Computed: true,
},
},
},
},
"name": schema.StringAttribute{
Description: descriptions["name"],
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.LengthAtMost(63),
validate.NoSeparator(),
},
},
"networks": schema.ListNestedAttribute{
Description: descriptions["networks"],
Optional: true,
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"network_id": schema.StringAttribute{
Description: descriptions["network_id"],
Optional: true,
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"role": schema.StringAttribute{
Description: descriptions["role"],
Optional: true,
Computed: true,
},
},
},
},
"options": schema.SingleNestedAttribute{
Description: descriptions["options"],
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"acl": schema.SetAttribute{
Description: descriptions["acl"],
ElementType: types.StringType,
Optional: true,
Computed: true,
Validators: []validator.Set{
setvalidator.ValueStringsAre(
validate.CIDR(),
),
},
},
"private_network_only": schema.BoolAttribute{
Description: descriptions["private_network_only"],
Optional: true,
Computed: true,
},
},
},
"private_address": schema.StringAttribute{
Description: descriptions["private_address"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"target_pools": schema.ListNestedAttribute{
Description: descriptions["target_pools"],
Optional: true,
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"active_health_check": schema.SingleNestedAttribute{
Description: descriptions["active_health_check"],
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"healthy_threshold": schema.Int64Attribute{
Description: descriptions["healthy_threshold"],
Optional: true,
Computed: true,
},
"interval": schema.StringAttribute{
Description: descriptions["interval"],
Optional: true,
Computed: true,
},
"interval_jitter": schema.StringAttribute{
Description: descriptions["interval_jitter"],
Optional: true,
Computed: true,
},
"timeout": schema.StringAttribute{
Description: descriptions["timeout"],
Optional: true,
Computed: true,
},
"unhealthy_threshold": schema.Int64Attribute{
Description: descriptions["unhealthy_threshold"],
Optional: true,
Computed: true,
},
},
},
"name": schema.StringAttribute{
Description: descriptions["target_pools.name"],
Optional: true,
Computed: true,
},
"target_port": schema.StringAttribute{
Description: descriptions["target_port"],
Optional: true,
Computed: true,
},
"targets": schema.ListNestedAttribute{
Description: descriptions["targets"],
Optional: true,
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"display_name": schema.StringAttribute{
Description: descriptions["targets.display_name"],
Optional: true,
Computed: true,
},
"ip": schema.StringAttribute{
Description: descriptions["ip"],
Optional: true,
Computed: true,
},
},
},
},
},
},
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
// Get status of load balancer functionality
statusResp, err := r.client.GetStatus(ctx, projectId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error getting status of load balancer functionality", fmt.Sprintf("Calling API: %v", err))
return
}
if *statusResp.Status != wait.FunctionalityStatusReady {
_, err = r.client.EnableLoadBalancing(ctx, projectId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error enabling load balancer functionality", fmt.Sprintf("Calling API: %v", err))
return
}
wr, err := wait.EnableLoadBalancingWaitHandler(ctx, r.client, projectId).SetTimeout(15 * time.Minute).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error enabling load balancer functionality", fmt.Sprintf("Waiting for enablement: %v", err))
return
}
_, ok := wr.(*loadbalancer.StatusResponse)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr))
return
}
}
// Generate API request body from model
_, err = toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err))
return
}
}
// Read refreshes the Terraform state with the latest data.
func (r *projectResource) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *projectResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *projectResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: container_id
func (r *projectResource) ImportState(_ context.Context, _ resource.ImportStateRequest, _ *resource.ImportStateResponse) {
}
func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoadBalancerPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
listeners := toListenersPayload(model)
networks := toNetworksPayload(model)
options, err := toOptionsPayload(ctx, model)
if err != nil {
return nil, fmt.Errorf("converting options: %w", err)
}
targetPools, err := toTargetPoolsPayload(ctx, model)
if err != nil {
return nil, fmt.Errorf("converting target pools: %w", err)
}
return &loadbalancer.CreateLoadBalancerPayload{
ExternalAddress: model.ExternalAddress.ValueStringPointer(),
Listeners: listeners,
Name: model.Name.ValueStringPointer(),
Networks: networks,
Options: options,
TargetPools: targetPools,
}, nil
}
func toListenersPayload(model *Model) *[]loadbalancer.Listener {
if model.Listeners == nil {
return nil
}
listeners := []loadbalancer.Listener{}
for _, listener := range model.Listeners {
listeners = append(listeners, loadbalancer.Listener{
DisplayName: listener.DisplayName.ValueStringPointer(),
Port: listener.Port.ValueInt64Pointer(),
Protocol: listener.Protocol.ValueStringPointer(),
TargetPool: listener.TargetPool.ValueStringPointer(),
})
}
return &listeners
}
func toNetworksPayload(model *Model) *[]loadbalancer.Network {
if model.Networks == nil {
return nil
}
networks := []loadbalancer.Network{}
for _, network := range model.Networks {
networks = append(networks, loadbalancer.Network{
NetworkId: network.NetworkId.ValueStringPointer(),
Role: network.Role.ValueStringPointer(),
})
}
return &networks
}
func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBalancerOptions, error) {
var optionsModel = &Options{}
if !(model.Options.IsNull() || model.Options.IsUnknown()) {
diags := model.Options.As(ctx, optionsModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("%w", core.DiagsToError(diags))
}
}
accessControl := &loadbalancer.LoadbalancerOptionAccessControl{}
if !(optionsModel.ACL.IsNull() || optionsModel.ACL.IsUnknown()) {
var acl []string
diags := optionsModel.ACL.ElementsAs(ctx, &acl, false)
if diags.HasError() {
return nil, fmt.Errorf("converting acl: %w", core.DiagsToError(diags))
}
accessControl.AllowedSourceRanges = &acl
}
options := &loadbalancer.LoadBalancerOptions{
AccessControl: accessControl,
PrivateNetworkOnly: optionsModel.PrivateNetworkOnly.ValueBoolPointer(),
}
return options, nil
}
func toTargetPoolsPayload(ctx context.Context, model *Model) (*[]loadbalancer.TargetPool, error) {
if model.TargetPools == nil {
return nil, nil
}
var targetPools []loadbalancer.TargetPool
for _, targetPool := range model.TargetPools {
var activeHealthCheck *loadbalancer.ActiveHealthCheck
if !(targetPool.ActiveHealthCheck.IsNull() || targetPool.ActiveHealthCheck.IsUnknown()) {
var activeHealthCheckModel ActiveHealthCheck
diags := targetPool.ActiveHealthCheck.As(ctx, &activeHealthCheckModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting active health check: %w", core.DiagsToError(diags))
}
activeHealthCheck = &loadbalancer.ActiveHealthCheck{
HealthyThreshold: activeHealthCheckModel.HealthyThreshold.ValueInt64Pointer(),
Interval: activeHealthCheckModel.Interval.ValueStringPointer(),
IntervalJitter: activeHealthCheckModel.IntervalJitter.ValueStringPointer(),
Timeout: activeHealthCheckModel.Timeout.ValueStringPointer(),
UnhealthyThreshold: activeHealthCheckModel.UnhealthyThreshold.ValueInt64Pointer(),
}
}
var targets []loadbalancer.Target
for _, target := range targetPool.Targets {
targets = append(targets, loadbalancer.Target{
DisplayName: target.DisplayName.ValueStringPointer(),
Ip: target.Ip.ValueStringPointer(),
})
}
targetPools = append(targetPools, loadbalancer.TargetPool{
ActiveHealthCheck: activeHealthCheck,
Name: targetPool.Name.ValueStringPointer(),
TargetPort: targetPool.TargetPort.ValueInt64Pointer(),
Targets: &targets,
})
}
return &targetPools, nil
}

View file

@ -0,0 +1,167 @@
package loadbalancer
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/loadbalancer"
)
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *loadbalancer.CreateLoadBalancerPayload
isValid bool
}{
{
"default_values_ok",
&Model{},
&loadbalancer.CreateLoadBalancerPayload{
ExternalAddress: nil,
Listeners: nil,
Name: nil,
Networks: nil,
Options: &loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
AllowedSourceRanges: nil,
},
PrivateNetworkOnly: nil,
},
TargetPools: nil,
},
true,
},
{
"simple_values_ok",
&Model{
ExternalAddress: types.StringValue("external_address"),
Listeners: []Listener{
{
DisplayName: types.StringValue("display_name"),
Port: types.Int64Value(80),
Protocol: types.StringValue("protocol"),
TargetPool: types.StringValue("target_pool"),
},
},
Name: types.StringValue("name"),
Networks: []Network{
{
NetworkId: types.StringValue("network_id"),
Role: types.StringValue("role"),
},
{
NetworkId: types.StringValue("network_id_2"),
Role: types.StringValue("role_2"),
},
},
Options: types.ObjectValueMust(
optionsTypes,
map[string]attr.Value{
"acl": types.ListValueMust(
types.StringType,
[]attr.Value{types.StringValue("cidr")}),
"private_network_only": types.BoolValue(true),
},
),
TargetPools: []TargetPool{
{
ActiveHealthCheck: 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"),
TargetPort: types.Int64Value(80),
Targets: []Target{
{
DisplayName: types.StringValue("display_name"),
Ip: types.StringValue("ip"),
},
},
},
},
},
&loadbalancer.CreateLoadBalancerPayload{
ExternalAddress: utils.Ptr("external_address"),
Listeners: utils.Ptr([]loadbalancer.Listener{
{
DisplayName: utils.Ptr("display_name"),
Port: utils.Ptr(int64(80)),
Protocol: utils.Ptr("protocol"),
TargetPool: utils.Ptr("target_pool"),
},
}),
Name: utils.Ptr("name"),
Networks: utils.Ptr([]loadbalancer.Network{
{
NetworkId: utils.Ptr("network_id"),
Role: utils.Ptr("role"),
},
{
NetworkId: utils.Ptr("network_id_2"),
Role: utils.Ptr("role_2"),
},
}),
Options: utils.Ptr(loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{
AllowedSourceRanges: utils.Ptr([]string{"cidr"}),
},
PrivateNetworkOnly: utils.Ptr(true),
}),
TargetPools: utils.Ptr([]loadbalancer.TargetPool{
{
ActiveHealthCheck: utils.Ptr(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: utils.Ptr([]loadbalancer.Target{
{
DisplayName: utils.Ptr("display_name"),
Ip: utils.Ptr("ip"),
},
}),
},
}),
},
true,
},
{
"nil_model",
nil,
nil,
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(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)
}
}
})
}
}

View file

@ -82,6 +82,7 @@ type providerModel struct {
PostgreSQLCustomEndpoint types.String `tfsdk:"postgresql_custom_endpoint"`
PostgresFlexCustomEndpoint types.String `tfsdk:"postgresflex_custom_endpoint"`
MongoDBFlexCustomEndpoint types.String `tfsdk:"mongodbflex_custom_endpoint"`
LoadBalancerCustomEndpoint types.String `tfsdk:"loadbalancer_custom_endpoint"`
LogMeCustomEndpoint types.String `tfsdk:"logme_custom_endpoint"`
RabbitMQCustomEndpoint types.String `tfsdk:"rabbitmq_custom_endpoint"`
MariaDBCustomEndpoint types.String `tfsdk:"mariadb_custom_endpoint"`
@ -276,6 +277,9 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
if !(providerConfig.MongoDBFlexCustomEndpoint.IsUnknown() || providerConfig.MongoDBFlexCustomEndpoint.IsNull()) {
providerData.MongoDBFlexCustomEndpoint = providerConfig.MongoDBFlexCustomEndpoint.ValueString()
}
if !(providerConfig.LoadBalancerCustomEndpoint.IsUnknown() || providerConfig.LoadBalancerCustomEndpoint.IsNull()) {
providerData.LoadBalancerCustomEndpoint = providerConfig.LoadBalancerCustomEndpoint.ValueString()
}
if !(providerConfig.LogMeCustomEndpoint.IsUnknown() || providerConfig.LogMeCustomEndpoint.IsNull()) {
providerData.LogMeCustomEndpoint = providerConfig.LogMeCustomEndpoint.ValueString()
}