diff --git a/docs/data-sources/server.md b/docs/data-sources/server.md index 36a7f415..8068f028 100644 --- a/docs/data-sources/server.md +++ b/docs/data-sources/server.md @@ -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. diff --git a/docs/resources/server.md b/docs/resources/server.md index 5246dfb3..befb1717 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -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 diff --git a/docs/resources/server_network_interface_attach.md b/docs/resources/server_network_interface_attach.md index 6b2d5262..e71e861c 100644 --- a/docs/resources/server_network_interface_attach.md +++ b/docs/resources/server_network_interface_attach.md @@ -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. diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go index a50aa5d4..312089fd 100644 --- a/stackit/internal/services/iaas/server/datasource.go +++ b/stackit/internal/services/iaas/server/datasource.go @@ -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 { diff --git a/stackit/internal/services/iaas/server/resource.go b/stackit/internal/services/iaas/server/resource.go index 38d3d85f..509d8707 100644 --- a/stackit/internal/services/iaas/server/resource.go +++ b/stackit/internal/services/iaas/server/resource.go @@ -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 diff --git a/stackit/internal/services/iaas/server/resource_test.go b/stackit/internal/services/iaas/server/resource_test.go index 1debdfa9..40493d13 100644 --- a/stackit/internal/services/iaas/server/resource_test.go +++ b/stackit/internal/services/iaas/server/resource_test.go @@ -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, },