Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

1 change: 1 addition & 0 deletions docs/data-sources/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions docs/resources/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/resources/server_network_interface_attach.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
9 changes: 8 additions & 1 deletion stackit/internal/services/iaas/server/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -197,7 +202,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 {
Expand Down
96 changes: 78 additions & 18 deletions stackit/internal/services/iaas/server/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -875,13 +916,32 @@ 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,
ImageId: conversion.StringValueToPointer(model.ImageId),
KeypairName: conversion.StringValueToPointer(model.KeypairName),
Labels: &labels,
Name: conversion.StringValueToPointer(model.Name),
Networking: network,
MachineType: conversion.StringValueToPointer(model.MachineType),
UserData: userData,
}, nil
Expand Down
70 changes: 42 additions & 28 deletions stackit/internal/services/iaas/server/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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()),
Expand All @@ -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),
Expand All @@ -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,
},
Expand Down
Loading