diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index 2cad184b..dd50aa05 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -43,6 +43,8 @@ var networkAreaRouteResource = map[string]string{ "network_area_id": networkAreaResource["network_area_id"], "prefix": "1.1.1.0/24", "next_hop": "1.1.1.1", + "label1": "value1", + "label1-updated": "value1-updated", } var networkInterfaceResource = map[string]string{ @@ -139,17 +141,21 @@ func networkAreaResourceConfig(areaname, networkranges string) string { ) } -func networkAreaRouteResourceConfig() string { +func networkAreaRouteResourceConfig(labelValue string) string { return fmt.Sprintf(` resource "stackit_network_area_route" "network_area_route" { organization_id = stackit_network_area.network_area.organization_id network_area_id = stackit_network_area.network_area.network_area_id prefix = "%s" next_hop = "%s" + labels = { + "label1" = "%s" + } } `, networkAreaRouteResource["prefix"], networkAreaRouteResource["next_hop"], + labelValue, ) } @@ -289,11 +295,11 @@ func serviceAccountAttachmentResourceConfig() string { ) } -func testAccNetworkAreaConfig(areaname, networkranges string) string { +func testAccNetworkAreaConfig(areaname, networkranges, routeLabelValue string) string { return fmt.Sprintf("%s\n\n%s\n\n%s", testutil.IaaSProviderConfig(), networkAreaResourceConfig(areaname, networkranges), - networkAreaRouteResourceConfig(), + networkAreaRouteResourceConfig(routeLabelValue), ) } @@ -350,6 +356,7 @@ func TestAccNetworkArea(t *testing.T) { Config: testAccNetworkAreaConfig( networkAreaResource["name"], networkAreaResource["networkrange0"], + networkAreaRouteResource["label1"], ), Check: resource.ComposeAggregateTestCheckFunc( // Network Area @@ -372,6 +379,7 @@ func TestAccNetworkArea(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", networkAreaRouteResource["prefix"]), resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", networkAreaRouteResource["next_hop"]), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "labels.label1", networkAreaRouteResource["label1"]), ), }, // Data source @@ -393,6 +401,7 @@ func TestAccNetworkArea(t *testing.T) { testAccNetworkAreaConfig( networkAreaResource["name"], networkAreaResource["networkrange0"], + networkAreaRouteResource["label1"], ), ), Check: resource.ComposeAggregateTestCheckFunc( @@ -419,6 +428,7 @@ func TestAccNetworkArea(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", networkAreaRouteResource["prefix"]), resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", networkAreaRouteResource["next_hop"]), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "labels.label1", networkAreaRouteResource["label1"]), ), }, // Import @@ -463,6 +473,7 @@ func TestAccNetworkArea(t *testing.T) { Config: testAccNetworkAreaConfig( fmt.Sprintf("%s-updated", networkAreaResource["name"]), networkAreaResource["networkrange0"], + networkAreaRouteResource["label1-updated"], ), Check: resource.ComposeAggregateTestCheckFunc( // Network area @@ -471,6 +482,20 @@ func TestAccNetworkArea(t *testing.T) { resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", fmt.Sprintf("%s-updated", networkAreaResource["name"])), resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", networkAreaResource["networkrange0"]), + + // Network area route + resource.TestCheckResourceAttrPair( + "stackit_network_area_route.network_area_route", "organization_id", + "stackit_network_area.network_area", "organization_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_network_area_route.network_area_route", "network_area_id", + "stackit_network_area.network_area", "network_area_id", + ), + resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "prefix", networkAreaRouteResource["prefix"]), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop", networkAreaRouteResource["next_hop"]), + resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "labels.label1", networkAreaRouteResource["label1-updated"]), ), }, // Deletion is done by the framework implicitly diff --git a/stackit/internal/services/iaas/networkarearoute/resource.go b/stackit/internal/services/iaas/networkarearoute/resource.go index cb1c91bf..3525c78e 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource.go +++ b/stackit/internal/services/iaas/networkarearoute/resource.go @@ -10,7 +10,6 @@ import ( "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/mapplanmodifier" "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" @@ -179,9 +178,6 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe Description: "Labels are key-value string pairs which can be attached to a resource container", ElementType: types.StringType, Optional: true, - PlanModifiers: []planmodifier.Map{ - mapplanmodifier.RequiresReplace(), - }, }, }, } @@ -315,9 +311,55 @@ func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.Dele tflog.Info(ctx, "Network area route deleted") } -func (r *networkAreaRouteResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", "Network area route can't be updated") +// Update updates the resource and sets the updated Terraform state on success. +func (r *networkAreaRouteResource) 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 + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + networkAreaRouteId := model.NetworkAreaRouteId.ValueString() + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) + + // Retrieve values from state + var stateModel Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from model + payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Creating API payload: %v", err)) + return + } + // Update existing network area route + networkAreaRouteResp, err := r.client.UpdateNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).UpdateNetworkAreaRoutePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFields(ctx, networkAreaRouteResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Network area route updated") } // ImportState imports a resource into the Terraform state on success. @@ -413,3 +455,18 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea }, }, nil } + +func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateNetworkAreaRoutePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + return &iaas.UpdateNetworkAreaRoutePayload{ + Labels: &labels, + }, nil +} diff --git a/stackit/internal/services/iaas/networkarearoute/resource_test.go b/stackit/internal/services/iaas/networkarearoute/resource_test.go index 52dd889d..d210e059 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource_test.go +++ b/stackit/internal/services/iaas/networkarearoute/resource_test.go @@ -160,3 +160,46 @@ func TestToCreatePayload(t *testing.T) { }) } } + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *iaas.UpdateNetworkAreaRoutePayload + isValid bool + }{ + { + "default_ok", + &Model{ + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key1": types.StringValue("value1"), + "key2": types.StringValue("value2"), + }), + }, + &iaas.UpdateNetworkAreaRoutePayload{ + Labels: &map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) + 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, cmp.AllowUnexported(iaas.NullableString{})) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +}