diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go new file mode 100644 index 00000000..995a4729 --- /dev/null +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -0,0 +1,312 @@ +package loadbalancer + +import ( + "context" + "fmt" + + "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/datasource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &loadBalancerDataSource{} +) + +// NewLoadBalancerDataSource is a helper function to simplify the provider implementation. +func NewLoadBalancerDataSource() datasource.DataSource { + return &loadBalancerDataSource{} +} + +// loadBalancerDataSource is the data source implementation. +type loadBalancerDataSource struct { + client *loadbalancer.APIClient +} + +// Metadata returns the data source type name. +func (r *loadBalancerDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_loadbalancer" +} + +// Configure adds the provider configured client to the data source. +func (r *loadBalancerDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.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 != "" { + 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 data source configuration", err)) + return + } + + r.client = apiClient + tflog.Info(ctx, "Load balancer client configured") +} + +// Schema defines the schema for the data source. +func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.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.", + "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, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + }, + }, + "external_address": schema.StringAttribute{ + Description: descriptions["external_address"], + Computed: true, + }, + "listeners": schema.ListNestedAttribute{ + Description: descriptions["listeners"], + Computed: true, + Validators: []validator.List{ + listvalidator.SizeBetween(1, 20), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + Description: descriptions["listeners.display_name"], + Computed: true, + }, + "port": schema.Int64Attribute{ + Description: descriptions["port"], + Computed: true, + }, + "protocol": schema.StringAttribute{ + Description: descriptions["protocol"], + Computed: true, + }, + "target_pool": schema.StringAttribute{ + Description: descriptions["target_pool"], + Computed: true, + }, + }, + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.LengthAtMost(63), + validate.NoSeparator(), + }, + }, + "networks": schema.ListNestedAttribute{ + Description: descriptions["networks"], + Computed: true, + Validators: []validator.List{ + listvalidator.SizeBetween(1, 1), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "network_id": schema.StringAttribute{ + Description: descriptions["network_id"], + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "role": schema.StringAttribute{ + Description: descriptions["role"], + Computed: true, + }, + }, + }, + }, + "options": schema.SingleNestedAttribute{ + Description: descriptions["options"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "acl": schema.SetAttribute{ + Description: descriptions["acl"], + ElementType: types.StringType, + Computed: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + validate.CIDR(), + ), + }, + }, + "private_network_only": schema.BoolAttribute{ + Description: descriptions["private_network_only"], + Computed: true, + }, + }, + }, + "private_address": schema.StringAttribute{ + Description: descriptions["private_address"], + Computed: true, + }, + "target_pools": schema.ListNestedAttribute{ + Description: descriptions["target_pools"], + Computed: 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"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "healthy_threshold": schema.Int64Attribute{ + Description: descriptions["healthy_threshold"], + Computed: true, + }, + "interval": schema.StringAttribute{ + Description: descriptions["interval"], + Computed: true, + }, + "interval_jitter": schema.StringAttribute{ + Description: descriptions["interval_jitter"], + Computed: true, + }, + "timeout": schema.StringAttribute{ + Description: descriptions["timeout"], + Computed: true, + }, + "unhealthy_threshold": schema.Int64Attribute{ + Description: descriptions["unhealthy_threshold"], + Computed: true, + }, + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["target_pools.name"], + Computed: true, + }, + "target_port": schema.Int64Attribute{ + Description: descriptions["target_port"], + Computed: true, + }, + "targets": schema.ListNestedAttribute{ + Description: descriptions["targets"], + Computed: 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"], + Computed: true, + }, + "ip": schema.StringAttribute{ + Description: descriptions["ip"], + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Config.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 { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, 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") +} diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index 9fd83907..659e0efe 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -5,13 +5,20 @@ import ( "fmt" "strings" + "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/diag" + "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" @@ -27,9 +34,9 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &projectResource{} - _ resource.ResourceWithConfigure = &projectResource{} - _ resource.ResourceWithImportState = &projectResource{} + _ resource.Resource = &loadBalancerResource{} + _ resource.ResourceWithConfigure = &loadBalancerResource{} + _ resource.ResourceWithImportState = &loadBalancerResource{} ) type Model struct { @@ -60,13 +67,13 @@ type Network struct { // Struct corresponding to Model.Options type Options struct { - ACL types.List `tfsdk:"acl"` + ACL types.Set `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{}}, + "acl": basetypes.SetType{ElemType: basetypes.StringType{}}, "private_network_only": basetypes.BoolType{}, } @@ -102,23 +109,23 @@ type Target struct { Ip types.String `tfsdk:"ip"` } -// NewProjectResource is a helper function to simplify the provider implementation. -func NewProjectResource() resource.Resource { - return &projectResource{} +// NewLoadBalancerResource is a helper function to simplify the provider implementation. +func NewLoadBalancerResource() resource.Resource { + return &loadBalancerResource{} } -// projectResource is the resource implementation. -type projectResource struct { +// loadBalancerResource is the resource implementation. +type loadBalancerResource struct { client *loadbalancer.APIClient } // Metadata returns the resource type name. -func (r *projectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +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 *projectResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +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 @@ -141,7 +148,7 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR } else { apiClient, err = loadbalancer.NewAPIClient( config.WithCustomAuth(providerData.RoundTripper), - config.WithServiceAccountEmail(providerData.ServiceAccountEmail), + config.WithRegion(providerData.Region), ) } @@ -155,7 +162,7 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR } // Schema defines the schema for the resource. -func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *loadBalancerResource) 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`\".", @@ -197,7 +204,7 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the dns record set is associated.", + Description: descriptions["project_id"], Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), @@ -209,41 +216,65 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re "external_address": schema.StringAttribute{ Description: descriptions["external_address"], Optional: true, - Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "listeners": schema.ListNestedAttribute{ Description: descriptions["listeners"], - Optional: true, - Computed: true, + 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(), + }, }, "port": schema.Int64Attribute{ Description: descriptions["port"], Optional: true, Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, }, "protocol": schema.StringAttribute{ Description: descriptions["protocol"], Optional: true, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("PROTOCOL_UNSPECIFIED", "PROTOCOL_TCP", "PROTOCOL_UDP", "PROTOCOL_TCP_PROXY"), + }, }, "target_pool": schema.StringAttribute{ Description: descriptions["target_pool"], Optional: true, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, }, }, }, "name": schema.StringAttribute{ Description: descriptions["name"], - Optional: true, - Computed: true, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, Validators: []validator.String{ stringvalidator.LengthAtLeast(1), stringvalidator.LengthAtMost(63), @@ -252,14 +283,21 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, "networks": schema.ListNestedAttribute{ Description: descriptions["networks"], - Optional: true, - Computed: true, + 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"], - Optional: true, - Computed: true, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, Validators: []validator.String{ validate.UUID(), validate.NoSeparator(), @@ -269,6 +307,12 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: descriptions["role"], Optional: true, Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("ROLE_UNSPECIFIED", "ROLE_LISTENERS_AND_TARGETS", "ROLE_LISTENERS", "ROLE_TARGETS"), + }, }, }, }, @@ -277,12 +321,18 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: descriptions["options"], Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, Attributes: map[string]schema.Attribute{ "acl": schema.SetAttribute{ Description: descriptions["acl"], ElementType: types.StringType, Optional: true, Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, Validators: []validator.Set{ setvalidator.ValueStringsAre( validate.CIDR(), @@ -293,6 +343,9 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: descriptions["private_network_only"], Optional: true, Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, }, }, }, @@ -305,8 +358,10 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, "target_pools": schema.ListNestedAttribute{ Description: descriptions["target_pools"], - Optional: true, - Computed: true, + Required: true, + Validators: []validator.List{ + listvalidator.SizeBetween(1, 20), + }, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "active_health_check": schema.SingleNestedAttribute{ @@ -343,29 +398,27 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, "name": schema.StringAttribute{ Description: descriptions["target_pools.name"], - Optional: true, - Computed: true, + Required: true, }, - "target_port": schema.StringAttribute{ + "target_port": schema.Int64Attribute{ Description: descriptions["target_port"], - Optional: true, - Computed: true, + Required: true, }, "targets": schema.ListNestedAttribute{ Description: descriptions["targets"], - Optional: true, - Computed: true, + 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"], - Optional: true, - Computed: true, + Required: true, }, "ip": schema.StringAttribute{ Description: descriptions["ip"], - Optional: true, - Computed: true, + Required: true, }, }, }, @@ -378,7 +431,7 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re } // 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 +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) @@ -398,7 +451,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest // If load balancer functionality is not enabled, enable it if *statusResp.Status != wait.FunctionalityStatusReady { - _, err = r.client.EnableLoadBalancing(ctx, projectId).Execute() + _, err = r.client.EnableLoadBalancing(ctx, projectId).XRequestID("").Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error enabling load balancer functionality", fmt.Sprintf("Calling API: %v", err)) return @@ -419,7 +472,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest } // Create a new load balancer - createResp, err := r.client.CreateLoadBalancer(ctx, projectId).CreateLoadBalancerPayload(*payload).Execute() + createResp, err := r.client.CreateLoadBalancer(ctx, projectId).CreateLoadBalancerPayload(*payload).XRequestID("").Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) return @@ -449,7 +502,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest } // Read refreshes the Terraform state with the latest data. -func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +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...) @@ -463,7 +516,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re lbResp, err := r.client.GetLoadBalancer(ctx, projectId, name).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Calling API: %v", err)) return } @@ -484,7 +537,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re } // Update updates the resource and sets the updated Terraform state on success. -func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +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) @@ -538,14 +591,50 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest } // 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 +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: container_id -func (r *projectResource) ImportState(_ context.Context, _ resource.ImportStateRequest, _ *resource.ImportStateResponse) { +// 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) { @@ -684,7 +773,7 @@ func toTargetPoolUpdatePayload(ctx context.Context, targetPool *TargetPool) (*lo } func toActiveHealthCheckPayload(ctx context.Context, targetPool *TargetPool) (*loadbalancer.ActiveHealthCheck, error) { - if targetPool.ActiveHealthCheck.IsNull() { + if targetPool.ActiveHealthCheck.IsNull() || targetPool.ActiveHealthCheck.IsUnknown() { return nil, nil } @@ -799,9 +888,9 @@ func mapOptions(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model) er } var diags diag.Diagnostics - acl := types.ListNull(types.StringType) + acl := types.SetNull(types.StringType) if lb.Options.AccessControl != nil && lb.Options.AccessControl.AllowedSourceRanges != nil { - acl, diags = types.ListValueFrom(ctx, types.StringType, *lb.Options.AccessControl.AllowedSourceRanges) + acl, diags = types.SetValueFrom(ctx, types.StringType, *lb.Options.AccessControl.AllowedSourceRanges) if diags != nil { return fmt.Errorf("converting acl: %w", core.DiagsToError(diags)) } diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go index 2be4f975..86e5aaa7 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go @@ -62,7 +62,7 @@ func TestToCreatePayload(t *testing.T) { Options: types.ObjectValueMust( optionsTypes, map[string]attr.Value{ - "acl": types.ListValueMust( + "acl": types.SetValueMust( types.StringType, []attr.Value{types.StringValue("cidr")}), "private_network_only": types.BoolValue(true), @@ -351,9 +351,8 @@ func TestMapFields(t *testing.T) { Options: types.ObjectValueMust( optionsTypes, map[string]attr.Value{ - "acl": types.ListValueMust( + "acl": types.SetValueMust( types.StringType, - []attr.Value{types.StringValue("cidr")}), "private_network_only": types.BoolValue(true), }, diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index a6027e05..a2659931 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -37,6 +37,7 @@ var ( ArgusCustomEndpoint = os.Getenv("TF_ACC_ARGUS_CUSTOM_ENDPOINT") DnsCustomEndpoint = os.Getenv("TF_ACC_DNS_CUSTOM_ENDPOINT") + LoadBalancerCustomEndpoint = os.Getenv("TF_ACC_LOADBALANCER_CUSTOM_ENDPOINT") LogMeCustomEndpoint = os.Getenv("TF_ACC_LOGME_CUSTOM_ENDPOINT") MariaDBCustomEndpoint = os.Getenv("TF_ACC_MARIADB_CUSTOM_ENDPOINT") MongoDBFlexCustomEndpoint = os.Getenv("TF_ACC_MONGODBFLEX_CUSTOM_ENDPOINT") @@ -77,6 +78,21 @@ func DnsProviderConfig() string { ) } +func LoadBalancerProviderConfig() string { + if LogMeCustomEndpoint == "" { + return ` + provider "stackit" { + region = "eu01" + }` + } + return fmt.Sprintf(` + provider "stackit" { + loadbalancer_custom_endpoint = "%s" + }`, + LoadBalancerCustomEndpoint, + ) +} + func LogMeProviderConfig() string { if LogMeCustomEndpoint == "" { return `