feature: Add "network_interfaces" field to server resource (#628)

* Add network_interfaces field to server resource

* Update docs

* Update description of stackit_server_network_interface_attach

Co-authored-by: João Palet <joao.palet@outlook.com>

---------

Co-authored-by: João Palet <joao.palet@outlook.com>
This commit is contained in:
Marcel Jacek 2025-01-22 11:22:39 +01:00 committed by GitHub
parent 9b969ae583
commit 4d6f860b26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 132 additions and 49 deletions

View file

@ -36,6 +36,7 @@ Server datasource schema. Must have a `region` specified in the provider configu
- `launched_at` (String) Date-time when the server was launched
- `machine_type` (String) Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html)
- `name` (String) The name of the server.
- `network_interfaces` (List of String) The IDs of network interfaces which should be attached to the server. Updating it will recreate the server.
- `updated_at` (String) Date-time when the server was updated
- `user_data` (String) User data that is passed via cloud-init to the server.

View file

@ -388,6 +388,7 @@ resource "stackit_server" "user-data-from-file" {
- `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
- `network_interfaces` (List of String) The IDs of network interfaces which should be attached to the server. Updating it will recreate the server.
- `user_data` (String) User data that is passed via cloud-init to the server.
### Read-Only

View file

@ -3,13 +3,13 @@
page_title: "stackit_server_network_interface_attach Resource - stackit"
subcategory: ""
description: |-
Network interface attachment resource schema. Attaches a network interface to a server. Must have a region specified in the provider configuration.
Network interface attachment resource schema. Attaches a network interface to a server. Must have a region specified in the provider configuration. The attachment takes only effect after server reboot.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
---
# stackit_server_network_interface_attach (Resource)
Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration.
Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration. The attachment takes only effect after server reboot.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.

View file

@ -151,6 +151,11 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
Description: "The image ID to be used for an ephemeral disk on the server.",
Computed: true,
},
"network_interfaces": schema.ListAttribute{
Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server.",
Computed: true,
ElementType: types.StringType,
},
"keypair_name": schema.StringAttribute{
Description: "The name of the keypair used during server creation.",
Computed: true,
@ -201,7 +206,9 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
serverResp, err := r.client.GetServer(ctx, projectId, serverId).Execute()
serverReq := r.client.GetServer(ctx, projectId, serverId)
serverReq = serverReq.Details(true)
serverResp, err := serverReq.Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {

View file

@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
@ -17,6 +18,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"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/stringplanmodifier"
@ -57,22 +59,23 @@ const (
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ServerId types.String `tfsdk:"server_id"`
MachineType types.String `tfsdk:"machine_type"`
Name types.String `tfsdk:"name"`
AvailabilityZone types.String `tfsdk:"availability_zone"`
BootVolume types.Object `tfsdk:"boot_volume"`
ImageId types.String `tfsdk:"image_id"`
KeypairName types.String `tfsdk:"keypair_name"`
Labels types.Map `tfsdk:"labels"`
AffinityGroup types.String `tfsdk:"affinity_group"`
UserData types.String `tfsdk:"user_data"`
CreatedAt types.String `tfsdk:"created_at"`
LaunchedAt types.String `tfsdk:"launched_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
DesiredStatus types.String `tfsdk:"desired_status"`
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
ServerId types.String `tfsdk:"server_id"`
MachineType types.String `tfsdk:"machine_type"`
Name types.String `tfsdk:"name"`
AvailabilityZone types.String `tfsdk:"availability_zone"`
BootVolume types.Object `tfsdk:"boot_volume"`
ImageId types.String `tfsdk:"image_id"`
NetworkInterfaces types.List `tfsdk:"network_interfaces"`
KeypairName types.String `tfsdk:"keypair_name"`
Labels types.Map `tfsdk:"labels"`
AffinityGroup types.String `tfsdk:"affinity_group"`
UserData types.String `tfsdk:"user_data"`
CreatedAt types.String `tfsdk:"created_at"`
LaunchedAt types.String `tfsdk:"launched_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
DesiredStatus types.String `tfsdk:"desired_status"`
}
// Struct corresponding to Model.BootVolume
@ -286,6 +289,20 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
stringplanmodifier.RequiresReplace(),
},
},
"network_interfaces": schema.ListAttribute{
Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server.",
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(
validate.UUID(),
validate.NoSeparator(),
),
},
PlanModifiers: []planmodifier.List{
listplanmodifier.RequiresReplace(),
},
},
"keypair_name": schema.StringAttribute{
Description: "The name of the keypair used during server creation.",
Optional: true,
@ -422,13 +439,21 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
}
serverId := *server.Id
server, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
_, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err))
return
}
ctx = tflog.SetField(ctx, "server_id", serverId)
// Get Server with details
serverReq := r.client.GetServer(ctx, projectId, serverId)
serverReq = serverReq.Details(true)
server, err = serverReq.Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("get server details: %v", err))
}
// Map response body to schema
err = mapFields(ctx, server, &model)
if err != nil {
@ -574,7 +599,9 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
serverResp, err := r.client.GetServer(ctx, projectId, serverId).Execute()
serverReq := r.client.GetServer(ctx, projectId, serverId)
serverReq = serverReq.Details(true)
serverResp, err := serverReq.Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -815,6 +842,20 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error
launchedAtValue := *serverResp.LaunchedAt
launchedAt = types.StringValue(launchedAtValue.Format(time.RFC3339))
}
if serverResp.Nics != nil {
var respNics []string
for _, nic := range *serverResp.Nics {
respNics = append(respNics, *nic.NicId)
}
nicTF, diags := types.ListValueFrom(ctx, types.StringType, respNics)
if diags.HasError() {
return fmt.Errorf("failed to map networkInterfaces: %w", core.DiagsToError(diags))
}
model.NetworkInterfaces = nicTF
} else {
model.NetworkInterfaces = types.ListNull(types.StringType)
}
model.ServerId = types.StringValue(serverId)
model.MachineType = types.StringPointerValue(serverResp.MachineType)
@ -875,6 +916,24 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
userData = &encodedUserData
}
var network *iaas.CreateServerPayloadNetworking
if !model.NetworkInterfaces.IsNull() && !model.NetworkInterfaces.IsUnknown() {
var nicIds []string
for _, nic := range model.NetworkInterfaces.Elements() {
nicString, ok := nic.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
nicIds = append(nicIds, nicString.ValueString())
}
network = &iaas.CreateServerPayloadNetworking{
CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{
NicIds: &nicIds,
},
}
}
return &iaas.CreateServerPayload{
AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone),
BootVolume: bootVolumePayload,
@ -882,6 +941,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
KeypairName: conversion.StringValueToPointer(model.KeypairName),
Labels: &labels,
Name: conversion.StringValueToPointer(model.Name),
Networking: network,
MachineType: conversion.StringValueToPointer(model.MachineType),
UserData: userData,
}, nil

View file

@ -43,19 +43,20 @@ func TestMapFields(t *testing.T) {
Id: utils.Ptr("sid"),
},
Model{
Id: types.StringValue("pid,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
AvailabilityZone: types.StringNull(),
Labels: types.MapNull(types.StringType),
ImageId: types.StringNull(),
KeypairName: types.StringNull(),
AffinityGroup: types.StringNull(),
UserData: types.StringNull(),
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Id: types.StringValue("pid,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
AvailabilityZone: types.StringNull(),
Labels: types.MapNull(types.StringType),
ImageId: types.StringNull(),
NetworkInterfaces: types.ListNull(types.StringType),
KeypairName: types.StringNull(),
AffinityGroup: types.StringNull(),
UserData: types.StringNull(),
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
},
true,
},
@ -72,7 +73,15 @@ func TestMapFields(t *testing.T) {
Labels: &map[string]interface{}{
"key": "value",
},
ImageId: utils.Ptr("image_id"),
ImageId: utils.Ptr("image_id"),
Nics: &[]iaas.ServerNetwork{
{
NicId: utils.Ptr("nic1"),
},
{
NicId: utils.Ptr("nic2"),
},
},
KeypairName: utils.Ptr("keypair_name"),
AffinityGroup: utils.Ptr("group_id"),
CreatedAt: utils.Ptr(testTimestamp()),
@ -89,7 +98,11 @@ func TestMapFields(t *testing.T) {
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
ImageId: types.StringValue("image_id"),
ImageId: types.StringValue("image_id"),
NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nic1"),
types.StringValue("nic2"),
}),
KeypairName: types.StringValue("keypair_name"),
AffinityGroup: types.StringValue("group_id"),
CreatedAt: types.StringValue(testTimestampValue),
@ -109,19 +122,20 @@ func TestMapFields(t *testing.T) {
Id: utils.Ptr("sid"),
},
Model{
Id: types.StringValue("pid,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
AvailabilityZone: types.StringNull(),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
ImageId: types.StringNull(),
KeypairName: types.StringNull(),
AffinityGroup: types.StringNull(),
UserData: types.StringNull(),
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Id: types.StringValue("pid,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
AvailabilityZone: types.StringNull(),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
ImageId: types.StringNull(),
NetworkInterfaces: types.ListNull(types.StringType),
KeypairName: types.StringNull(),
AffinityGroup: types.StringNull(),
UserData: types.StringNull(),
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
},
true,
},