terraform-provider-stackitp.../stackit/internal/services/loadbalancer/loadbalancer/resource.go
João Palet 3fb28d1248
Document possible values of schema fields (#455)
* Document possible values of schema fields

* Change from possible to supported
2024-07-09 13:14:38 +01:00

1361 lines
47 KiB
Go

package loadbalancer
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"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/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
"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/core/oapierror"
sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils"
"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/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &loadBalancerResource{}
_ resource.ResourceWithConfigure = &loadBalancerResource{}
_ resource.ResourceWithImportState = &loadBalancerResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ExternalAddress types.String `tfsdk:"external_address"`
Listeners types.List `tfsdk:"listeners"`
Name types.String `tfsdk:"name"`
Networks types.List `tfsdk:"networks"`
Options types.Object `tfsdk:"options"`
PrivateAddress types.String `tfsdk:"private_address"`
TargetPools types.List `tfsdk:"target_pools"`
}
// Struct corresponding to Model.Listeners[i]
type listener struct {
DisplayName types.String `tfsdk:"display_name"`
Port types.Int64 `tfsdk:"port"`
Protocol types.String `tfsdk:"protocol"`
ServerNameIndicators types.List `tfsdk:"server_name_indicators"`
TargetPool types.String `tfsdk:"target_pool"`
}
// Types corresponding to listener
var listenerTypes = map[string]attr.Type{
"display_name": types.StringType,
"port": types.Int64Type,
"protocol": types.StringType,
"server_name_indicators": types.ListType{ElemType: types.ObjectType{AttrTypes: serverNameIndicatorTypes}},
"target_pool": types.StringType,
}
// Struct corresponding to listener.ServerNameIndicators[i]
type serverNameIndicator struct {
Name types.String `tfsdk:"name"`
}
// Types corresponding to serverNameIndicator
var serverNameIndicatorTypes = map[string]attr.Type{
"name": types.StringType,
}
// Struct corresponding to Model.Networks[i]
type network struct {
NetworkId types.String `tfsdk:"network_id"`
Role types.String `tfsdk:"role"`
}
// Types corresponding to network
var networkTypes = map[string]attr.Type{
"network_id": types.StringType,
"role": types.StringType,
}
// Struct corresponding to Model.Options
type options struct {
ACL types.Set `tfsdk:"acl"`
PrivateNetworkOnly types.Bool `tfsdk:"private_network_only"`
}
// Types corresponding to options
var optionsTypes = map[string]attr.Type{
"acl": types.SetType{ElemType: types.StringType},
"private_network_only": types.BoolType,
}
// Struct corresponding to Model.TargetPools[i]
type targetPool struct {
ActiveHealthCheck types.Object `tfsdk:"active_health_check"`
Name types.String `tfsdk:"name"`
TargetPort types.Int64 `tfsdk:"target_port"`
Targets types.List `tfsdk:"targets"`
SessionPersistence types.Object `tfsdk:"session_persistence"`
}
// Types corresponding to targetPool
var targetPoolTypes = map[string]attr.Type{
"active_health_check": types.ObjectType{AttrTypes: activeHealthCheckTypes},
"name": types.StringType,
"target_port": types.Int64Type,
"targets": types.ListType{ElemType: types.ObjectType{AttrTypes: targetTypes}},
"session_persistence": types.ObjectType{AttrTypes: sessionPersistenceTypes},
}
// Struct corresponding to 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": types.Int64Type,
"interval": types.StringType,
"interval_jitter": types.StringType,
"timeout": types.StringType,
"unhealthy_threshold": types.Int64Type,
}
// Struct corresponding to targetPool.Targets[i]
type target struct {
DisplayName types.String `tfsdk:"display_name"`
Ip types.String `tfsdk:"ip"`
}
// Types corresponding to target
var targetTypes = map[string]attr.Type{
"display_name": types.StringType,
"ip": types.StringType,
}
// Struct corresponding to targetPool.SessionPersistence
type sessionPersistence struct {
UseSourceIPAddress types.Bool `tfsdk:"use_source_ip_address"`
}
// Types corresponding to SessionPersistence
var sessionPersistenceTypes = map[string]attr.Type{
"use_source_ip_address": types.BoolType,
}
// NewLoadBalancerResource is a helper function to simplify the provider implementation.
func NewLoadBalancerResource() resource.Resource {
return &loadBalancerResource{}
}
// loadBalancerResource is the resource implementation.
type loadBalancerResource struct {
client *loadbalancer.APIClient
}
// Metadata returns the resource type name.
func (r *loadBalancerResource) 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 *loadBalancerResource) 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.WithRegion(providerData.Region),
)
}
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 *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"}
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.",
"port": "Port number where we listen for traffic.",
"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.",
"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),
"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.",
"session_persistence": "Here you can setup various session persistence options, so far only \"`use_source_ip_address`\" is supported.",
"use_source_ip_address": "If true then all connections from one source IP address are redirected to the same target. This setting changes the load balancing algorithm to Maglev.",
"server_name_indicators": "A list of domain names to match in order to pass TLS traffic to the target pool in the current listener",
"server_name_indicators.name": "A domain name to match in order to pass TLS traffic to the target pool in the current listener",
"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"],
MarkdownDescription: `
## Setting up supporting infrastructure` + "\n" + `
### Configuring an OpenStack provider` + "\n" + `
To automate the creation of load balancers, OpenStack can be used to setup the supporting infrastructure.
To set up the OpenStack provider, you can create a token through the STACKIT Portal, in your project's Infrastructure API page.
There, the OpenStack user domain name, username, and password are generated and can be obtained. The provider can then be configured as follows:` + "\n" +
"```terraform" + `
terraform {
required_providers {
(...)
openstack = {
source = "terraform-provider-openstack/openstack"
}
}
}
provider "openstack" {
user_domain_name = "{OpenStack user domain name}"
user_name = "{OpenStack username}"
password = "{OpenStack password}"
region = "RegionOne"
auth_url = "https://keystone.api.iaas.eu01.stackit.cloud/v3"
}
` + "\n```" + `
### Configuring the supporting infrastructure
The example below uses OpenStack to create the network, router, a public IP address and a compute instance.
`,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
},
},
"external_address": schema.StringAttribute{
Description: descriptions["external_address"],
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"listeners": schema.ListNestedAttribute{
Description: descriptions["listeners"],
Required: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.RequiresReplace(),
},
Validators: []validator.List{
listvalidator.SizeBetween(1, 20),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"display_name": schema.StringAttribute{
Description: descriptions["listeners.display_name"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
},
"port": schema.Int64Attribute{
Description: descriptions["port"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
int64planmodifier.UseStateForUnknown(),
},
},
"protocol": schema.StringAttribute{
Description: descriptions["protocol"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
stringvalidator.OneOf(protocolOptions...),
},
},
"server_name_indicators": schema.ListNestedAttribute{
Description: descriptions["server_name_indicators"],
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: descriptions["server_name_indicators.name"],
Optional: true,
},
},
},
},
"target_pool": schema.StringAttribute{
Description: descriptions["target_pool"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
},
},
},
},
"name": schema.StringAttribute{
Description: descriptions["name"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.LengthAtMost(63),
validate.NoSeparator(),
},
},
"networks": schema.ListNestedAttribute{
Description: descriptions["networks"],
Required: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.RequiresReplace(),
},
Validators: []validator.List{
listvalidator.SizeBetween(1, 1),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"network_id": schema.StringAttribute{
Description: descriptions["network_id"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"role": schema.StringAttribute{
Description: descriptions["role"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
stringvalidator.OneOf(roleOptions...),
},
},
},
},
},
"options": schema.SingleNestedAttribute{
Description: descriptions["options"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"acl": schema.SetAttribute{
Description: descriptions["acl"],
ElementType: types.StringType,
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Set{
setplanmodifier.RequiresReplace(),
setplanmodifier.UseStateForUnknown(),
},
Validators: []validator.Set{
setvalidator.ValueStringsAre(
validate.CIDR(),
),
},
},
"private_network_only": schema.BoolAttribute{
Description: descriptions["private_network_only"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
boolplanmodifier.UseStateForUnknown(),
},
},
},
},
"private_address": schema.StringAttribute{
Description: descriptions["private_address"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"target_pools": schema.ListNestedAttribute{
Description: descriptions["target_pools"],
Required: true,
Validators: []validator.List{
listvalidator.SizeBetween(1, 20),
},
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"],
Required: true,
},
"target_port": schema.Int64Attribute{
Description: descriptions["target_port"],
Required: true,
},
"session_persistence": schema.SingleNestedAttribute{
Description: descriptions["session_persistence"],
Optional: true,
Computed: false,
Attributes: map[string]schema.Attribute{
"use_source_ip_address": schema.BoolAttribute{
Description: descriptions["use_source_ip_address"],
Optional: true,
Computed: false,
},
},
},
"targets": schema.ListNestedAttribute{
Description: descriptions["targets"],
Required: true,
Validators: []validator.List{
listvalidator.SizeBetween(1, 250),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"display_name": schema.StringAttribute{
Description: descriptions["targets.display_name"],
Required: true,
},
"ip": schema.StringAttribute{
Description: descriptions["ip"],
Required: true,
},
},
},
},
},
},
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *loadBalancerResource) 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)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create a new load balancer
createResp, err := r.client.CreateLoadBalancer(ctx, projectId).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err))
return
}
waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err))
return
}
// Map response body to schema
err = mapFields(waitResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Load balancer created")
}
// Read refreshes the Terraform state with the latest data.
func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "name", name)
lbResp, err := r.client.GetLoadBalancer(ctx, projectId, name).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(lbResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Load balancer read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // 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()
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "name", name)
targetPoolsModel := []targetPool{}
diags = model.TargetPools.ElementsAs(ctx, &targetPoolsModel, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
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, sdkUtils.Ptr(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.UpdateTargetPool(ctx, projectId, 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 = tflog.SetField(ctx, "target_pool_name", nil)
// Get updated load balancer
getResp, err := r.client.GetLoadBalancer(ctx, projectId, name).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Calling API after update: %v", err))
return
}
// Map response body to schema
err = mapFields(getResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Load balancer updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *loadBalancerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "name", name)
// Delete load balancer
_, err := r.client.DeleteLoadBalancer(ctx, projectId, name).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting load balancer", fmt.Sprintf("Calling API: %v", err))
return
}
_, err = wait.DeleteLoadBalancerWaitHandler(ctx, r.client, projectId, name).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting load balancer", fmt.Sprintf("Load balancer deleting waiting: %v", err))
return
}
tflog.Info(ctx, "Load balancer deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,name
func (r *loadBalancerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing load balancer",
fmt.Sprintf("Expected import identifier with format: [project_id],[name] Got: %q", req.ID),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[1])...)
tflog.Info(ctx, "Load balancer state imported")
}
func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoadBalancerPayload, 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.CreateLoadBalancerPayload{
ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress),
Listeners: listenersPayload,
Name: conversion.StringValueToPointer(model.Name),
Networks: networksPayload,
Options: optionsPayload,
TargetPools: targetPoolsPayload,
}, nil
}
func toListenersPayload(ctx context.Context, model *Model) (*[]loadbalancer.Listener, error) {
if model.Listeners.IsNull() || model.Listeners.IsUnknown() {
return nil, nil
}
listenersModel := []listener{}
diags := model.Listeners.ElementsAs(ctx, &listenersModel, false)
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
if len(listenersModel) == 0 {
return nil, nil
}
payload := []loadbalancer.Listener{}
for i := range listenersModel {
listenerModel := listenersModel[i]
serverNameIndicatorsPayload, err := toServerNameIndicatorsPayload(ctx, &listenerModel)
if err != nil {
return nil, fmt.Errorf("converting index %d: converting server_name_indicator: %w", i, err)
}
payload = append(payload, loadbalancer.Listener{
DisplayName: conversion.StringValueToPointer(listenerModel.DisplayName),
Port: conversion.Int64ValueToPointer(listenerModel.Port),
Protocol: conversion.StringValueToPointer(listenerModel.Protocol),
ServerNameIndicators: serverNameIndicatorsPayload,
TargetPool: conversion.StringValueToPointer(listenerModel.TargetPool),
})
}
return &payload, nil
}
func toServerNameIndicatorsPayload(ctx context.Context, l *listener) (*[]loadbalancer.ServerNameIndicator, error) {
if l.ServerNameIndicators.IsNull() || l.ServerNameIndicators.IsUnknown() {
return nil, nil
}
serverNameIndicatorsModel := []serverNameIndicator{}
diags := l.ServerNameIndicators.ElementsAs(ctx, &serverNameIndicatorsModel, false)
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
payload := []loadbalancer.ServerNameIndicator{}
for i := range serverNameIndicatorsModel {
indicatorModel := serverNameIndicatorsModel[i]
payload = append(payload, loadbalancer.ServerNameIndicator{
Name: conversion.StringValueToPointer(indicatorModel.Name),
})
}
return &payload, nil
}
func toNetworksPayload(ctx context.Context, model *Model) (*[]loadbalancer.Network, error) {
if model.Networks.IsNull() || model.Networks.IsUnknown() {
return nil, nil
}
networksModel := []network{}
diags := model.Networks.ElementsAs(ctx, &networksModel, false)
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
if len(networksModel) == 0 {
return nil, nil
}
payload := []loadbalancer.Network{}
for i := range networksModel {
networkModel := networksModel[i]
payload = append(payload, loadbalancer.Network{
NetworkId: conversion.StringValueToPointer(networkModel.NetworkId),
Role: conversion.StringValueToPointer(networkModel.Role),
})
}
return &payload, nil
}
func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBalancerOptions, error) {
if model.Options.IsNull() || model.Options.IsUnknown() {
return &loadbalancer.LoadBalancerOptions{
AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{},
}, nil
}
optionsModel := options{}
diags := model.Options.As(ctx, &optionsModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
accessControlPayload := &loadbalancer.LoadbalancerOptionAccessControl{}
if !(optionsModel.ACL.IsNull() || optionsModel.ACL.IsUnknown()) {
var aclModel []string
diags := optionsModel.ACL.ElementsAs(ctx, &aclModel, false)
if diags.HasError() {
return nil, fmt.Errorf("converting acl: %w", core.DiagsToError(diags))
}
accessControlPayload.AllowedSourceRanges = &aclModel
}
payload := loadbalancer.LoadBalancerOptions{
AccessControl: accessControlPayload,
PrivateNetworkOnly: conversion.BoolValueToPointer(optionsModel.PrivateNetworkOnly),
}
return &payload, nil
}
func toTargetPoolsPayload(ctx context.Context, model *Model) (*[]loadbalancer.TargetPool, error) {
if model.TargetPools.IsNull() || model.TargetPools.IsUnknown() {
return nil, nil
}
targetPoolsModel := []targetPool{}
diags := model.TargetPools.ElementsAs(ctx, &targetPoolsModel, false)
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
if len(targetPoolsModel) == 0 {
return nil, nil
}
payload := []loadbalancer.TargetPool{}
for i := range targetPoolsModel {
targetPoolModel := targetPoolsModel[i]
activeHealthCheckPayload, err := toActiveHealthCheckPayload(ctx, &targetPoolModel)
if err != nil {
return nil, fmt.Errorf("converting index %d: converting active_health_check: %w", i, err)
}
sessionPersistencePayload, err := toSessionPersistencePayload(ctx, &targetPoolModel)
if err != nil {
return nil, fmt.Errorf("converting index %d: converting session_persistence: %w", i, err)
}
targetsPayload, err := toTargetsPayload(ctx, &targetPoolModel)
if err != nil {
return nil, fmt.Errorf("converting index %d: converting targets: %w", i, err)
}
payload = append(payload, loadbalancer.TargetPool{
ActiveHealthCheck: activeHealthCheckPayload,
Name: conversion.StringValueToPointer(targetPoolModel.Name),
SessionPersistence: sessionPersistencePayload,
TargetPort: conversion.Int64ValueToPointer(targetPoolModel.TargetPort),
Targets: targetsPayload,
})
}
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.Int64ValueToPointer(tp.TargetPort),
Targets: targetsPayload,
}, nil
}
func toSessionPersistencePayload(ctx context.Context, tp *targetPool) (*loadbalancer.SessionPersistence, error) {
if tp.SessionPersistence.IsNull() || tp.ActiveHealthCheck.IsUnknown() {
return nil, nil
}
sessionPersistenceModel := sessionPersistence{}
diags := tp.SessionPersistence.As(ctx, &sessionPersistenceModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
return &loadbalancer.SessionPersistence{
UseSourceIpAddress: conversion.BoolValueToPointer(sessionPersistenceModel.UseSourceIPAddress),
}, nil
}
func toActiveHealthCheckPayload(ctx context.Context, tp *targetPool) (*loadbalancer.ActiveHealthCheck, error) {
if tp.ActiveHealthCheck.IsNull() || tp.ActiveHealthCheck.IsUnknown() {
return nil, nil
}
activeHealthCheckModel := activeHealthCheck{}
diags := tp.ActiveHealthCheck.As(ctx, &activeHealthCheckModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting active health check: %w", core.DiagsToError(diags))
}
return &loadbalancer.ActiveHealthCheck{
HealthyThreshold: conversion.Int64ValueToPointer(activeHealthCheckModel.HealthyThreshold),
Interval: conversion.StringValueToPointer(activeHealthCheckModel.Interval),
IntervalJitter: conversion.StringValueToPointer(activeHealthCheckModel.IntervalJitter),
Timeout: conversion.StringValueToPointer(activeHealthCheckModel.Timeout),
UnhealthyThreshold: conversion.Int64ValueToPointer(activeHealthCheckModel.UnhealthyThreshold),
}, nil
}
func toTargetsPayload(ctx context.Context, tp *targetPool) (*[]loadbalancer.Target, error) {
if tp.Targets.IsNull() || tp.Targets.IsUnknown() {
return nil, nil
}
targetsModel := []target{}
diags := tp.Targets.ElementsAs(ctx, &targetsModel, false)
if diags.HasError() {
return nil, fmt.Errorf("converting Targets list: %w", core.DiagsToError(diags))
}
if len(targetsModel) == 0 {
return nil, nil
}
payload := []loadbalancer.Target{}
for i := range targetsModel {
targetModel := targetsModel[i]
payload = append(payload, loadbalancer.Target{
DisplayName: conversion.StringValueToPointer(targetModel.DisplayName),
Ip: conversion.StringValueToPointer(targetModel.Ip),
})
}
return &payload, nil
}
func mapFields(lb *loadbalancer.LoadBalancer, m *Model) error {
if lb == nil {
return fmt.Errorf("response input is nil")
}
if m == nil {
return fmt.Errorf("model input is nil")
}
var name string
if m.Name.ValueString() != "" {
name = m.Name.ValueString()
} else if lb.Name != nil {
name = *lb.Name
} else {
return fmt.Errorf("name not present")
}
m.Name = types.StringValue(name)
idParts := []string{
m.ProjectId.ValueString(),
name,
}
m.Id = types.StringValue(
strings.Join(idParts, core.Separator),
)
m.ExternalAddress = types.StringPointerValue(lb.ExternalAddress)
m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress)
err := mapListeners(lb, m)
if err != nil {
return fmt.Errorf("mapping listeners: %w", err)
}
err = mapNetworks(lb, m)
if err != nil {
return fmt.Errorf("mapping network: %w", err)
}
err = mapOptions(lb, m)
if err != nil {
return fmt.Errorf("mapping options: %w", err)
}
err = mapTargetPools(lb, m)
if err != nil {
return fmt.Errorf("mapping target pools: %w", err)
}
return nil
}
func mapListeners(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error {
if loadBalancerResp.Listeners == nil {
m.Listeners = types.ListNull(types.ObjectType{AttrTypes: listenerTypes})
return nil
}
listenersList := []attr.Value{}
for i, listenerResp := range *loadBalancerResp.Listeners {
listenerMap := map[string]attr.Value{
"display_name": types.StringPointerValue(listenerResp.DisplayName),
"port": types.Int64PointerValue(listenerResp.Port),
"protocol": types.StringPointerValue(listenerResp.Protocol),
"target_pool": types.StringPointerValue(listenerResp.TargetPool),
}
err := mapServerNameIndicators(listenerResp.ServerNameIndicators, listenerMap)
if err != nil {
return fmt.Errorf("mapping index %d, field serverNameIndicators: %w", i, err)
}
listenerTF, diags := types.ObjectValue(listenerTypes, listenerMap)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
listenersList = append(listenersList, listenerTF)
}
listenersTF, diags := types.ListValue(
types.ObjectType{AttrTypes: listenerTypes},
listenersList,
)
if diags.HasError() {
return core.DiagsToError(diags)
}
m.Listeners = listenersTF
return nil
}
func mapServerNameIndicators(serverNameIndicatorsResp *[]loadbalancer.ServerNameIndicator, l map[string]attr.Value) error {
if serverNameIndicatorsResp == nil || *serverNameIndicatorsResp == nil {
l["server_name_indicators"] = types.ListNull(types.ObjectType{AttrTypes: serverNameIndicatorTypes})
return nil
}
serverNameIndicatorsList := []attr.Value{}
for i, serverNameIndicatorResp := range *serverNameIndicatorsResp {
serverNameIndicatorMap := map[string]attr.Value{
"name": types.StringPointerValue(serverNameIndicatorResp.Name),
}
serverNameIndicatorTF, diags := types.ObjectValue(serverNameIndicatorTypes, serverNameIndicatorMap)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
serverNameIndicatorsList = append(serverNameIndicatorsList, serverNameIndicatorTF)
}
serverNameIndicatorsTF, diags := types.ListValue(
types.ObjectType{AttrTypes: serverNameIndicatorTypes},
serverNameIndicatorsList,
)
if diags.HasError() {
return core.DiagsToError(diags)
}
l["server_name_indicators"] = serverNameIndicatorsTF
return nil
}
func mapNetworks(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error {
if loadBalancerResp.Networks == nil {
m.Networks = types.ListNull(types.ObjectType{AttrTypes: networkTypes})
return nil
}
networksList := []attr.Value{}
for i, networkResp := range *loadBalancerResp.Networks {
networkMap := map[string]attr.Value{
"network_id": types.StringPointerValue(networkResp.NetworkId),
"role": types.StringPointerValue(networkResp.Role),
}
networkTF, diags := types.ObjectValue(networkTypes, networkMap)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
networksList = append(networksList, networkTF)
}
networksTF, diags := types.ListValue(
types.ObjectType{AttrTypes: networkTypes},
networksList,
)
if diags.HasError() {
return core.DiagsToError(diags)
}
m.Networks = networksTF
return nil
}
func mapOptions(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error {
if loadBalancerResp.Options == nil {
m.Options = types.ObjectNull(optionsTypes)
return nil
}
optionsMap := map[string]attr.Value{
"private_network_only": types.BoolPointerValue(loadBalancerResp.Options.PrivateNetworkOnly),
}
err := mapACL(loadBalancerResp.Options.AccessControl, optionsMap)
if err != nil {
return fmt.Errorf("mapping field ACL: %w", err)
}
optionsTF, diags := types.ObjectValue(optionsTypes, optionsMap)
if diags.HasError() {
return core.DiagsToError(diags)
}
m.Options = optionsTF
return nil
}
func mapACL(accessControlResp *loadbalancer.LoadbalancerOptionAccessControl, o map[string]attr.Value) error {
if accessControlResp == nil || accessControlResp.AllowedSourceRanges == nil {
o["acl"] = types.SetNull(types.StringType)
return nil
}
aclList := []attr.Value{}
for _, rangeResp := range *accessControlResp.AllowedSourceRanges {
rangeTF := types.StringValue(rangeResp)
aclList = append(aclList, rangeTF)
}
aclTF, diags := types.SetValue(types.StringType, aclList)
if diags.HasError() {
return core.DiagsToError(diags)
}
o["acl"] = aclTF
return nil
}
func mapTargetPools(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error {
if loadBalancerResp.TargetPools == nil {
m.TargetPools = types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes})
return nil
}
targetPoolsList := []attr.Value{}
for i, targetPoolResp := range *loadBalancerResp.TargetPools {
targetPoolMap := map[string]attr.Value{
"name": types.StringPointerValue(targetPoolResp.Name),
"target_port": types.Int64PointerValue(targetPoolResp.TargetPort),
}
err := mapActiveHealthCheck(targetPoolResp.ActiveHealthCheck, targetPoolMap)
if err != nil {
return fmt.Errorf("mapping index %d, field ActiveHealthCheck: %w", i, err)
}
err = mapTargets(targetPoolResp.Targets, targetPoolMap)
if err != nil {
return fmt.Errorf("mapping index %d, field Targets: %w", i, err)
}
err = mapSessionPersistence(targetPoolResp.SessionPersistence, targetPoolMap)
if err != nil {
return fmt.Errorf("mapping index %d, field SessionPersistence: %w", i, err)
}
targetPoolTF, diags := types.ObjectValue(targetPoolTypes, targetPoolMap)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
targetPoolsList = append(targetPoolsList, targetPoolTF)
}
targetPoolsTF, diags := types.ListValue(
types.ObjectType{AttrTypes: targetPoolTypes},
targetPoolsList,
)
if diags.HasError() {
return core.DiagsToError(diags)
}
m.TargetPools = targetPoolsTF
return nil
}
func mapActiveHealthCheck(activeHealthCheckResp *loadbalancer.ActiveHealthCheck, tp map[string]attr.Value) error {
if activeHealthCheckResp == nil {
tp["active_health_check"] = types.ObjectNull(activeHealthCheckTypes)
return nil
}
activeHealthCheckMap := map[string]attr.Value{
"healthy_threshold": types.Int64PointerValue(activeHealthCheckResp.HealthyThreshold),
"interval": types.StringPointerValue(activeHealthCheckResp.Interval),
"interval_jitter": types.StringPointerValue(activeHealthCheckResp.IntervalJitter),
"timeout": types.StringPointerValue(activeHealthCheckResp.Timeout),
"unhealthy_threshold": types.Int64PointerValue(activeHealthCheckResp.UnhealthyThreshold),
}
activeHealthCheckTF, diags := types.ObjectValue(activeHealthCheckTypes, activeHealthCheckMap)
if diags.HasError() {
return core.DiagsToError(diags)
}
tp["active_health_check"] = activeHealthCheckTF
return nil
}
func mapTargets(targetsResp *[]loadbalancer.Target, tp map[string]attr.Value) error {
if targetsResp == nil || *targetsResp == nil {
tp["targets"] = types.ListNull(types.ObjectType{AttrTypes: targetTypes})
return nil
}
targetsList := []attr.Value{}
for i, targetResp := range *targetsResp {
targetMap := map[string]attr.Value{
"display_name": types.StringPointerValue(targetResp.DisplayName),
"ip": types.StringPointerValue(targetResp.Ip),
}
targetTF, diags := types.ObjectValue(targetTypes, targetMap)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
targetsList = append(targetsList, targetTF)
}
targetsTF, diags := types.ListValue(
types.ObjectType{AttrTypes: targetTypes},
targetsList,
)
if diags.HasError() {
return core.DiagsToError(diags)
}
tp["targets"] = targetsTF
return nil
}
func mapSessionPersistence(sessionPersistenceResp *loadbalancer.SessionPersistence, tp map[string]attr.Value) error {
if sessionPersistenceResp == nil {
tp["session_persistence"] = types.ObjectNull(sessionPersistenceTypes)
return nil
}
sessionPersistenceMap := map[string]attr.Value{
"use_source_ip_address": types.BoolPointerValue(sessionPersistenceResp.UseSourceIpAddress),
}
sessionPersistenceTF, diags := types.ObjectValue(sessionPersistenceTypes, sessionPersistenceMap)
if diags.HasError() {
return core.DiagsToError(diags)
}
tp["session_persistence"] = sessionPersistenceTF
return nil
}