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: Read-Only:
- `delete_on_termination` (Boolean) Delete the volume during the termination of the server. - `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 - `id` (String) The ID of the boot volume
- `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`.

View file

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

View file

@ -58,5 +58,3 @@ Read-Only:
- `name` (String) - `name` (String)
- `retention_period` (Number) - `retention_period` (Number)
- `volume_ids` (List of String) - `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`. - `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. - `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`. - `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: Optional:
- `volume_ids` (List of String) - `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 // 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.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"),
resource.TestCheckNoResourceAttr("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.size", serverResource["size"]),
resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", serverResource["source_type"]), resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", serverResource["source_type"]),
resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_id", serverResource["source_id"]), 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.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "network_interface_id"),
resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "name", networkInterfaceResource["name"]), 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 // Import
@ -1028,8 +1032,8 @@ func TestAccServer(t *testing.T) {
resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"),
resource.TestCheckResourceAttr("stackit_network.network", "name", fmt.Sprintf("%s-updated", networkResource["name"])), 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.#", "2"),
resource.TestCheckResourceAttr("stackit_network.network", "nameservers.0", networkResource["nameserver0"]), resource.TestCheckTypeSetElemAttr("stackit_network.network", "nameservers.*", networkResource["nameserver0"]),
resource.TestCheckResourceAttr("stackit_network.network", "nameservers.1", networkResource["nameserver1"]), resource.TestCheckTypeSetElemAttr("stackit_network.network", "nameservers.*", networkResource["nameserver1"]),
resource.TestCheckResourceAttr("stackit_network.network", "ipv4_gateway", networkResource["ipv4_gateway"]), resource.TestCheckResourceAttr("stackit_network.network", "ipv4_gateway", networkResource["ipv4_gateway"]),
// Server // Server

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag" "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/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas" "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/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
) )
@ -46,6 +46,11 @@ type DataSourceModel struct {
UpdatedAt types.String `tfsdk:"updated_at"` 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. // NewServerDataSource is a helper function to simplify the provider implementation.
func NewServerDataSource() datasource.DataSource { func NewServerDataSource() datasource.DataSource {
return &serverDataSource{} return &serverDataSource{}
@ -139,20 +144,8 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
Description: "The boot volume for the server", Description: "The boot volume for the server",
Computed: true, Computed: true,
Attributes: map[string]schema.Attribute{ 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{ "id": schema.StringAttribute{
Description: "The ID of the source, either image ID or volume ID", Description: "The ID of the boot volume",
Computed: true, Computed: true,
}, },
"delete_on_termination": schema.BoolAttribute{ "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) 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.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone)
model.ServerId = types.StringValue(serverId) model.ServerId = types.StringValue(serverId)
model.MachineType = types.StringPointerValue(serverResp.MachineType) model.MachineType = types.StringPointerValue(serverResp.MachineType)

View file

@ -75,6 +75,7 @@ type Model struct {
// Struct corresponding to Model.BootVolume // Struct corresponding to Model.BootVolume
type bootVolumeModel struct { type bootVolumeModel struct {
Id types.String `tfsdk:"id"`
PerformanceClass types.String `tfsdk:"performance_class"` PerformanceClass types.String `tfsdk:"performance_class"`
Size types.Int64 `tfsdk:"size"` Size types.Int64 `tfsdk:"size"`
SourceType types.String `tfsdk:"source_type"` SourceType types.String `tfsdk:"source_type"`
@ -89,6 +90,7 @@ var bootVolumeTypes = map[string]attr.Type{
"source_type": basetypes.StringType{}, "source_type": basetypes.StringType{},
"source_id": basetypes.StringType{}, "source_id": basetypes.StringType{},
"delete_on_termination": basetypes.BoolType{}, "delete_on_termination": basetypes.BoolType{},
"id": basetypes.StringType{},
} }
// NewServerResource is a helper function to simplify the provider implementation. // 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(), objectplanmodifier.RequiresReplace(),
}, },
Attributes: map[string]schema.Attribute{ 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{ "performance_class": schema.StringAttribute{
Description: "The performance class of the server.", Description: "The performance class of the server.",
Optional: true, Optional: true,
@ -911,6 +920,34 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error
model.NetworkInterfaces = types.ListNull(types.StringType) 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.ServerId = types.StringValue(serverId)
model.MachineType = types.StringPointerValue(serverResp.MachineType) model.MachineType = types.StringPointerValue(serverResp.MachineType)

View file

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