From b0030f66cfc23aa26158528e2f5f5ef44d4d5bd7 Mon Sep 17 00:00:00 2001 From: Alexander Dahmen Date: Thu, 23 Jan 2025 12:52:26 +0100 Subject: [PATCH] Allow deleting root volume when deleting server Introduce delete_on_termination field. Signed-off-by: Alexander Dahmen --- docs/data-sources/server.md | 1 + docs/resources/server.md | 3 +- .../services/iaas/server/datasource.go | 4 ++ .../internal/services/iaas/server/resource.go | 53 ++++++++++++++++--- .../services/iaas/server/resource_test.go | 51 ++++++++++++++++-- 5 files changed, 99 insertions(+), 13 deletions(-) diff --git a/docs/data-sources/server.md b/docs/data-sources/server.md index 8068f028..9999722c 100644 --- a/docs/data-sources/server.md +++ b/docs/data-sources/server.md @@ -45,6 +45,7 @@ Server datasource schema. Must have a `region` specified in the provider configu Read-Only: +- `delete_on_termination` (Boolean) Delete the volume during the termination of the server. - `id` (String) The ID of the source, either image ID or volume ID - `performance_class` (String) The performance class of the server. - `size` (Number) The size of the boot volume in GB. diff --git a/docs/resources/server.md b/docs/resources/server.md index befb1717..14fde405 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -384,7 +384,7 @@ resource "stackit_server" "user-data-from-file" { - `affinity_group` (String) The affinity group the server is assigned to. - `availability_zone` (String) The availability zone of the server. - `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume)) -- `desired_status` (String) The desired status of the server resource. Defaults to 'active' Supported values are: `active`, `inactive`, `deallocated`. +- `desired_status` (String) The desired status of the server resource.Supported values are: `active`, `inactive`, `deallocated`. - `image_id` (String) The image ID to be used for an ephemeral disk on the server. - `keypair_name` (String) The name of the keypair used during server creation. - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container @@ -409,5 +409,6 @@ Required: Optional: +- `delete_on_termination` (Boolean) Delete the volume during the termination of the server. Only allowed when `source_type` is `image`. - `performance_class` (String) The performance class of the server. - `size` (Number) The size of the boot volume in GB. Must be provided when `source_type` is `image`. diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go index 84635ba1..1fc58dfc 100644 --- a/stackit/internal/services/iaas/server/datasource.go +++ b/stackit/internal/services/iaas/server/datasource.go @@ -168,6 +168,10 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, Description: "The ID of the source, either image ID or volume ID", Computed: true, }, + "delete_on_termination": schema.BoolAttribute{ + Description: "Delete the volume during the termination of the server.", + Computed: true, + }, }, }, "image_id": schema.StringAttribute{ diff --git a/stackit/internal/services/iaas/server/resource.go b/stackit/internal/services/iaas/server/resource.go index 509d8707..6fc94e56 100644 --- a/stackit/internal/services/iaas/server/resource.go +++ b/stackit/internal/services/iaas/server/resource.go @@ -17,6 +17,7 @@ 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/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" @@ -80,18 +81,20 @@ type Model struct { // Struct corresponding to Model.BootVolume type bootVolumeModel struct { - PerformanceClass types.String `tfsdk:"performance_class"` - Size types.Int64 `tfsdk:"size"` - SourceType types.String `tfsdk:"source_type"` - SourceId types.String `tfsdk:"source_id"` + PerformanceClass types.String `tfsdk:"performance_class"` + Size types.Int64 `tfsdk:"size"` + SourceType types.String `tfsdk:"source_type"` + SourceId types.String `tfsdk:"source_id"` + DeleteOnTermination types.Bool `tfsdk:"delete_on_termination"` } // Types corresponding to bootVolumeModel var bootVolumeTypes = map[string]attr.Type{ - "performance_class": basetypes.StringType{}, - "size": basetypes.Int64Type{}, - "source_type": basetypes.StringType{}, - "source_id": basetypes.StringType{}, + "performance_class": basetypes.StringType{}, + "size": basetypes.Int64Type{}, + "source_type": basetypes.StringType{}, + "source_id": basetypes.StringType{}, + "delete_on_termination": basetypes.BoolType{}, } // NewServerResource is a helper function to simplify the provider implementation. @@ -109,6 +112,29 @@ func (r *serverResource) Metadata(_ context.Context, req resource.MetadataReques resp.TypeName = req.ProviderTypeName + "_server" } +func (r serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + // convert boot volume model + var bootVolume = &bootVolumeModel{} + if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) { + diags := model.BootVolume.As(ctx, bootVolume, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return + } + } + + if !bootVolume.DeleteOnTermination.IsUnknown() && !bootVolume.DeleteOnTermination.IsNull() && !bootVolume.SourceType.IsUnknown() && !bootVolume.SourceType.IsNull() { + if bootVolume.SourceType != types.StringValue("image") { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring server", "You can only provide `delete_on_termination` for `source_type` `image`.") + } + } +} + // ConfigValidators validates the resource configuration func (r *serverResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ @@ -276,6 +302,13 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res stringplanmodifier.RequiresReplace(), }, }, + "delete_on_termination": schema.BoolAttribute{ + Description: "Delete the volume during the termination of the server. Only allowed when `source_type` is `image`.", + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, }, }, "image_id": schema.StringAttribute{ @@ -908,6 +941,10 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo Type: conversion.StringValueToPointer(bootVolume.SourceType), }, } + if !bootVolume.DeleteOnTermination.IsNull() && !bootVolume.DeleteOnTermination.IsNull() && bootVolume.DeleteOnTermination.ValueBool() { + // it is set and true, adjust payload + bootVolumePayload.DeleteOnTermination = conversion.BoolValueToPointer(bootVolume.DeleteOnTermination) + } } var userData *string diff --git a/stackit/internal/services/iaas/server/resource_test.go b/stackit/internal/services/iaas/server/resource_test.go index 40493d13..cbc26b77 100644 --- a/stackit/internal/services/iaas/server/resource_test.go +++ b/stackit/internal/services/iaas/server/resource_test.go @@ -191,10 +191,11 @@ func TestToCreatePayload(t *testing.T) { "key": types.StringValue("value"), }), BootVolume: types.ObjectValueMust(bootVolumeTypes, map[string]attr.Value{ - "performance_class": types.StringValue("class"), - "size": types.Int64Value(1), - "source_type": types.StringValue("type"), - "source_id": types.StringValue("id"), + "performance_class": types.StringValue("class"), + "size": types.Int64Value(1), + "source_type": types.StringValue("type"), + "source_id": types.StringValue("id"), + "delete_on_termination": types.BoolUnknown(), }), ImageId: types.StringValue("image"), KeypairName: types.StringValue("keypair"), @@ -222,6 +223,48 @@ func TestToCreatePayload(t *testing.T) { }, true, }, + { + "delete on termination is set to true", + &Model{ + Name: types.StringValue("name"), + AvailabilityZone: types.StringValue("zone"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + BootVolume: types.ObjectValueMust(bootVolumeTypes, map[string]attr.Value{ + "performance_class": types.StringValue("class"), + "size": types.Int64Value(1), + "source_type": types.StringValue("image"), + "source_id": types.StringValue("id"), + "delete_on_termination": types.BoolValue(true), + }), + ImageId: types.StringValue("image"), + KeypairName: types.StringValue("keypair"), + MachineType: types.StringValue("machine_type"), + UserData: types.StringValue(userData), + }, + &iaas.CreateServerPayload{ + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("zone"), + Labels: &map[string]interface{}{ + "key": "value", + }, + BootVolume: &iaas.CreateServerPayloadBootVolume{ + PerformanceClass: utils.Ptr("class"), + Size: utils.Ptr(int64(1)), + Source: &iaas.BootVolumeSource{ + Type: utils.Ptr("image"), + Id: utils.Ptr("id"), + }, + DeleteOnTermination: utils.Ptr(true), + }, + ImageId: utils.Ptr("image"), + KeypairName: utils.Ptr("keypair"), + MachineType: utils.Ptr("machine_type"), + UserData: utils.Ptr(base64EncodedUserData), + }, + true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) {