fix(server): Handle boot bolume correctly (#749)

* fix(server): Handle boot bolume correctly

- Display id and delete_on_termination in datasource
- Handle id and delete_on_termination in resource

Signed-off-by: Alexander Dahmen <alexander.dahmen@inovex.de>

* fixup

Signed-off-by: Alexander Dahmen <alexander.dahmen@inovex.de>

---------

Signed-off-by: Alexander Dahmen <alexander.dahmen@inovex.de>
This commit is contained in:
Alexander Dahmen 2025-03-28 13:31:36 +01:00 committed by GitHub
parent f5f99d1709
commit 68859a3fad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 70 additions and 26 deletions

View file

@ -43,7 +43,4 @@ 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.
- `type` (String) The type of the source. Supported values are: `volume`, `image`.
- `id` (String) The ID of the boot volume

View file

@ -52,5 +52,3 @@ Read-Only:
- `name` (String)
- `retention_period` (Number)
- `volume_ids` (List of String)

View file

@ -58,5 +58,3 @@ Read-Only:
- `name` (String)
- `retention_period` (Number)
- `volume_ids` (List of String)

View file

@ -410,3 +410,7 @@ 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`.
Read-Only:
- `id` (String) The ID of the boot volume

View file

@ -62,5 +62,3 @@ Required:
Optional:
- `volume_ids` (List of String)

View file

@ -733,6 +733,7 @@ func TestAccServer(t *testing.T) {
// The network interface which was attached by "stackit_server_network_interface_attach" should not appear here
resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"),
resource.TestCheckNoResourceAttr("stackit_server.server", "network_interfaces.1"),
resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.id"),
resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.size", serverResource["size"]),
resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", serverResource["source_type"]),
resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_id", serverResource["source_id"]),
@ -871,6 +872,9 @@ func TestAccServer(t *testing.T) {
),
resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "network_interface_id"),
resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "name", networkInterfaceResource["name"]),
// Boot volume
resource.TestCheckResourceAttrSet("data.stackit_server.server", "boot_volume.id"),
resource.TestCheckResourceAttr("data.stackit_server.server", "boot_volume.delete_on_termination", serverResource["delete_on_termination"]),
),
},
// Import
@ -1028,8 +1032,8 @@ func TestAccServer(t *testing.T) {
resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"),
resource.TestCheckResourceAttr("stackit_network.network", "name", fmt.Sprintf("%s-updated", networkResource["name"])),
resource.TestCheckResourceAttr("stackit_network.network", "nameservers.#", "2"),
resource.TestCheckResourceAttr("stackit_network.network", "nameservers.0", networkResource["nameserver0"]),
resource.TestCheckResourceAttr("stackit_network.network", "nameservers.1", networkResource["nameserver1"]),
resource.TestCheckTypeSetElemAttr("stackit_network.network", "nameservers.*", networkResource["nameserver0"]),
resource.TestCheckTypeSetElemAttr("stackit_network.network", "nameservers.*", networkResource["nameserver1"]),
resource.TestCheckResourceAttr("stackit_network.network", "ipv4_gateway", networkResource["ipv4_gateway"]),
// Server

View file

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
@ -18,7 +19,6 @@ import (
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"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"
)
@ -46,6 +46,11 @@ type DataSourceModel struct {
UpdatedAt types.String `tfsdk:"updated_at"`
}
var bootVolumeDataTypes = map[string]attr.Type{
"id": basetypes.StringType{},
"delete_on_termination": basetypes.BoolType{},
}
// NewServerDataSource is a helper function to simplify the provider implementation.
func NewServerDataSource() datasource.DataSource {
return &serverDataSource{}
@ -139,20 +144,8 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
Description: "The boot volume for the server",
Computed: true,
Attributes: map[string]schema.Attribute{
"performance_class": schema.StringAttribute{
Description: "The performance class of the server.",
Computed: true,
},
"size": schema.Int64Attribute{
Description: "The size of the boot volume in GB.",
Computed: true,
},
"type": schema.StringAttribute{
Description: "The type of the source. " + utils.SupportedValuesDocumentation(supportedSourceTypes),
Computed: true,
},
"id": schema.StringAttribute{
Description: "The ID of the source, either image ID or volume ID",
Description: "The ID of the boot volume",
Computed: true,
},
"delete_on_termination": schema.BoolAttribute{
@ -312,6 +305,19 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da
model.NetworkInterfaces = types.ListNull(types.StringType)
}
if serverResp.BootVolume != nil {
bootVolume, diags := types.ObjectValue(bootVolumeDataTypes, map[string]attr.Value{
"id": types.StringPointerValue(serverResp.BootVolume.Id),
"delete_on_termination": types.BoolPointerValue(serverResp.BootVolume.DeleteOnTermination),
})
if diags.HasError() {
return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags))
}
model.BootVolume = bootVolume
} else {
model.BootVolume = types.ObjectNull(bootVolumeDataTypes)
}
model.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone)
model.ServerId = types.StringValue(serverId)
model.MachineType = types.StringPointerValue(serverResp.MachineType)

View file

@ -75,6 +75,7 @@ type Model struct {
// Struct corresponding to Model.BootVolume
type bootVolumeModel struct {
Id types.String `tfsdk:"id"`
PerformanceClass types.String `tfsdk:"performance_class"`
Size types.Int64 `tfsdk:"size"`
SourceType types.String `tfsdk:"source_type"`
@ -89,6 +90,7 @@ var bootVolumeTypes = map[string]attr.Type{
"source_type": basetypes.StringType{},
"source_id": basetypes.StringType{},
"delete_on_termination": basetypes.BoolType{},
"id": basetypes.StringType{},
}
// NewServerResource is a helper function to simplify the provider implementation.
@ -253,6 +255,13 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
objectplanmodifier.RequiresReplace(),
},
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The ID of the boot volume",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"performance_class": schema.StringAttribute{
Description: "The performance class of the server.",
Optional: true,
@ -911,6 +920,34 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error
model.NetworkInterfaces = types.ListNull(types.StringType)
}
if serverResp.BootVolume != nil {
// convert boot volume model
var bootVolumeModel = &bootVolumeModel{}
if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) {
diags := model.BootVolume.As(ctx, bootVolumeModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags))
}
}
// Only the id and delete_on_termination is returned via response.
// Take the other values from the model.
bootVolume, diags := types.ObjectValue(bootVolumeTypes, map[string]attr.Value{
"id": types.StringPointerValue(serverResp.BootVolume.Id),
"delete_on_termination": types.BoolPointerValue(serverResp.BootVolume.DeleteOnTermination),
"source_id": bootVolumeModel.SourceId,
"size": bootVolumeModel.Size,
"source_type": bootVolumeModel.SourceType,
"performance_class": bootVolumeModel.PerformanceClass,
})
if diags.HasError() {
return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags))
}
model.BootVolume = bootVolume
} else {
model.BootVolume = types.ObjectNull(bootVolumeTypes)
}
model.ServerId = types.StringValue(serverId)
model.MachineType = types.StringPointerValue(serverResp.MachineType)

View file

@ -193,6 +193,7 @@ func TestToCreatePayload(t *testing.T) {
"source_type": types.StringValue("type"),
"source_id": types.StringValue("id"),
"delete_on_termination": types.BoolUnknown(),
"id": types.StringValue("id"),
}),
ImageId: types.StringValue("image"),
KeypairName: types.StringValue("keypair"),
@ -234,6 +235,7 @@ func TestToCreatePayload(t *testing.T) {
"source_type": types.StringValue("image"),
"source_id": types.StringValue("id"),
"delete_on_termination": types.BoolValue(true),
"id": types.StringValue("id"),
}),
ImageId: types.StringValue("image"),
KeypairName: types.StringValue("keypair"),