diff --git a/docs/data-sources/server.md b/docs/data-sources/server.md index 631a4ff9e..e7691d6be 100644 --- a/docs/data-sources/server.md +++ b/docs/data-sources/server.md @@ -34,6 +34,7 @@ data "stackit_server" "example" { ### Read-Only - `affinity_group` (String) The affinity group the server is assigned to. +- `agent` (Attributes) STACKIT Server Agent as setup on the server (see [below for nested schema](#nestedatt--agent)) - `availability_zone` (String) The availability zone of the server. - `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume)) - `created_at` (String) Date-time when the server was created @@ -48,6 +49,14 @@ data "stackit_server" "example" { - `updated_at` (String) Date-time when the server was updated - `user_data` (String) User data that is passed via cloud-init to the server. + +### Nested Schema for `agent` + +Read-Only: + +- `provisioned` (Boolean) Whether a STACKIT Server Agent is provisioned at the server + + ### Nested Schema for `boot_volume` diff --git a/docs/ephemeral-resources/access_token.md b/docs/ephemeral-resources/access_token.md deleted file mode 100644 index b45fd715e..000000000 --- a/docs/ephemeral-resources/access_token.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "stackit_access_token Ephemeral Resource - stackit" -subcategory: "" -description: |- - Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. Access tokens generated from service account keys expire after 60 minutes. - ~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). If any other authentication method is configured, this ephemeral resource will fail with an error. - ~> This ephemeral-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_access_token (Ephemeral Resource) - -Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. Access tokens generated from service account keys expire after 60 minutes. - -~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). If any other authentication method is configured, this ephemeral resource will fail with an error. - -~> This ephemeral-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. - -## Example Usage - -```terraform -provider "stackit" { - default_region = "eu01" - service_account_key_path = "/path/to/sa_key.json" - enable_beta_resources = true -} - -ephemeral "stackit_access_token" "example" {} - -locals { - stackit_api_base_url = "https://iaas.api.stackit.cloud" - public_ip_path = "/v2/projects/${var.project_id}/regions/${var.region}/public-ips" - - public_ip_payload = { - labels = { - key = "value" - } - } -} - -# Docs: https://registry.terraform.io/providers/Mastercard/restapi/latest -provider "restapi" { - uri = local.stackit_api_base_url - write_returns_object = true - - headers = { - Authorization = "Bearer ${ephemeral.stackit_access_token.example.access_token}" - Content-Type = "application/json" - } - - create_method = "POST" - update_method = "PATCH" - destroy_method = "DELETE" -} - -resource "restapi_object" "public_ip_restapi" { - path = local.public_ip_path - data = jsonencode(local.public_ip_payload) - - id_attribute = "id" - read_method = "GET" - create_method = "POST" - update_method = "PATCH" - destroy_method = "DELETE" -} -``` - - -## Schema - -### Read-Only - -- `access_token` (String, Sensitive) JWT access token for STACKIT API authentication. diff --git a/docs/resources/server.md b/docs/resources/server.md index e7559dfc0..9c696c92c 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -404,6 +404,7 @@ import { ### Optional - `affinity_group` (String) The affinity group the server is assigned to. +- `agent` (Attributes) The STACKIT Server Agent configured for the server (see [below for nested schema](#nestedatt--agent)) - `availability_zone` (String) The availability zone of the server. - `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume)) - `desired_status` (String) The desired status of the server resource. Possible values are: `active`, `inactive`, `deallocated`. @@ -422,6 +423,14 @@ import { - `server_id` (String) The server ID. - `updated_at` (String) Date-time when the server was updated + +### Nested Schema for `agent` + +Optional: + +- `provisioned` (Boolean) Whether a STACKIT Server Agent should be provisioned at the server + + ### Nested Schema for `boot_volume` diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go index f68f5563a..8afb15efc 100644 --- a/stackit/internal/services/iaas/server/datasource.go +++ b/stackit/internal/services/iaas/server/datasource.go @@ -34,6 +34,7 @@ type DataSourceModel struct { ServerId types.String `tfsdk:"server_id"` MachineType types.String `tfsdk:"machine_type"` Name types.String `tfsdk:"name"` + Agent types.Object `tfsdk:"agent"` AvailabilityZone types.String `tfsdk:"availability_zone"` BootVolume types.Object `tfsdk:"boot_volume"` ImageId types.String `tfsdk:"image_id"` @@ -52,6 +53,10 @@ var bootVolumeDataTypes = map[string]attr.Type{ "delete_on_termination": basetypes.BoolType{}, } +var agentDataTypes = map[string]attr.Type{ + "provisioned": basetypes.BoolType{}, +} + // NewServerDataSource is a helper function to simplify the provider implementation. func NewServerDataSource() datasource.DataSource { return &serverDataSource{} @@ -123,6 +128,16 @@ func (d *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, MarkdownDescription: "Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/)", Computed: true, }, + "agent": schema.SingleNestedAttribute{ + Description: "STACKIT Server Agent as setup on the server", + Computed: true, + Attributes: map[string]schema.Attribute{ + "provisioned": schema.BoolAttribute{ + Description: "Whether a STACKIT Server Agent is provisioned at the server", + Computed: true, + }, + }, + }, "availability_zone": schema.StringAttribute{ Description: "The availability zone of the server.", Computed: true, @@ -304,6 +319,18 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da model.BootVolume = types.ObjectNull(bootVolumeDataTypes) } + if serverResp.Agent != nil { + agent, diags := types.ObjectValue(agentDataTypes, map[string]attr.Value{ + "provisioned": types.BoolPointerValue(serverResp.Agent.Provisioned), + }) + if diags.HasError() { + return fmt.Errorf("failed to map agent: %w", core.DiagsToError(diags)) + } + model.Agent = agent + } else { + model.Agent = types.ObjectNull(agentDataTypes) + } + if serverResp.UserData != nil && len(*serverResp.UserData) > 0 { model.UserData = types.StringValue(string(*serverResp.UserData)) } diff --git a/stackit/internal/services/iaas/server/datasource_test.go b/stackit/internal/services/iaas/server/datasource_test.go index 56c2be530..fb5658710 100644 --- a/stackit/internal/services/iaas/server/datasource_test.go +++ b/stackit/internal/services/iaas/server/datasource_test.go @@ -41,6 +41,7 @@ func TestMapDataSourceFields(t *testing.T) { ServerId: types.StringValue("sid"), Name: types.StringNull(), AvailabilityZone: types.StringNull(), + Agent: types.ObjectNull(agentTypes), Labels: types.MapNull(types.StringType), ImageId: types.StringNull(), NetworkInterfaces: types.ListNull(types.StringType), @@ -78,7 +79,10 @@ func TestMapDataSourceFields(t *testing.T) { NicId: utils.Ptr("nic2"), }, }, - KeypairName: utils.Ptr("keypair_name"), + KeypairName: utils.Ptr("keypair_name"), + Agent: &iaas.ServerAgent{ + Provisioned: utils.Ptr(true), + }, AffinityGroup: utils.Ptr("group_id"), CreatedAt: utils.Ptr(testTimestamp()), UpdatedAt: utils.Ptr(testTimestamp()), @@ -101,7 +105,10 @@ func TestMapDataSourceFields(t *testing.T) { types.StringValue("nic1"), types.StringValue("nic2"), }), - KeypairName: types.StringValue("keypair_name"), + KeypairName: types.StringValue("keypair_name"), + Agent: types.ObjectValueMust(agentTypes, map[string]attr.Value{ + "provisioned": types.BoolValue(true), + }), AffinityGroup: types.StringValue("group_id"), CreatedAt: types.StringValue(testTimestampValue), UpdatedAt: types.StringValue(testTimestampValue), @@ -132,6 +139,7 @@ func TestMapDataSourceFields(t *testing.T) { Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), ImageId: types.StringNull(), NetworkInterfaces: types.ListNull(types.StringType), + Agent: types.ObjectNull(agentTypes), KeypairName: types.StringNull(), AffinityGroup: types.StringNull(), UserData: types.StringNull(), diff --git a/stackit/internal/services/iaas/server/resource.go b/stackit/internal/services/iaas/server/resource.go index cd75a552f..375e19917 100644 --- a/stackit/internal/services/iaas/server/resource.go +++ b/stackit/internal/services/iaas/server/resource.go @@ -61,6 +61,7 @@ type Model struct { ServerId types.String `tfsdk:"server_id"` MachineType types.String `tfsdk:"machine_type"` Name types.String `tfsdk:"name"` + Agent types.Object `tfsdk:"agent"` AvailabilityZone types.String `tfsdk:"availability_zone"` BootVolume types.Object `tfsdk:"boot_volume"` ImageId types.String `tfsdk:"image_id"` @@ -75,6 +76,11 @@ type Model struct { DesiredStatus types.String `tfsdk:"desired_status"` } +// Struct corresponding to Model.Agent +type agentModel struct { + Provisioned types.Bool `tfsdk:"provisioned"` +} + // Struct corresponding to Model.BootVolume type bootVolumeModel struct { Id types.String `tfsdk:"id"` @@ -95,6 +101,11 @@ var bootVolumeTypes = map[string]attr.Type{ "id": basetypes.StringType{}, } +// Types corresponding to agentModel +var agentTypes = map[string]attr.Type{ + "provisioned": basetypes.BoolType{}, +} + // NewServerResource is a helper function to simplify the provider implementation. func NewServerResource() resource.Resource { return &serverResource{} @@ -163,6 +174,14 @@ func (r *serverResource) ValidateConfig(ctx context.Context, req resource.Valida } } + var agent = &agentModel{} + if !(model.Agent.IsNull() || model.Agent.IsUnknown()) { + diags := model.Agent.As(ctx, agent, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return + } + } + if model.NetworkInterfaces.IsNull() || model.NetworkInterfaces.IsUnknown() || len(model.NetworkInterfaces.Elements()) < 1 { core.LogAndAddWarning(ctx, &resp.Diagnostics, "No network interfaces configured", "You have no network interfaces configured for this server. This will be a problem when you want to (re-)create this server. Please note that modifying the network interfaces for an existing server will result in a replacement of the resource. We will provide a clear migration path soon.") } @@ -273,6 +292,23 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res Optional: true, Computed: true, }, + "agent": schema.SingleNestedAttribute{ + Description: "The STACKIT Server Agent configured for the server", + Optional: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, + Attributes: map[string]schema.Attribute{ + "provisioned": schema.BoolAttribute{ + Description: "Whether a STACKIT Server Agent should be provisioned at the server", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + }, + }, "boot_volume": schema.SingleNestedAttribute{ Description: "The boot volume for the server", Optional: true, @@ -962,6 +998,26 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model, regio model.NetworkInterfaces = types.ListNull(types.StringType) } + if serverResp.Agent != nil { + // convert agent model + var modelAgent = &agentModel{} + if !(model.Agent.IsNull() || model.Agent.IsUnknown()) { + diags := model.Agent.As(ctx, modelAgent, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return fmt.Errorf("failed to map agent: %w", core.DiagsToError(diags)) + } + } + agent, diags := types.ObjectValue(agentTypes, map[string]attr.Value{ + "provisioned": types.BoolPointerValue(serverResp.Agent.Provisioned), + }) + if diags.HasError() { + return fmt.Errorf("failed to map agentModel: %w", core.DiagsToError(diags)) + } + model.Agent = agent + } else { + model.Agent = types.ObjectNull(agentTypes) + } + if serverResp.BootVolume != nil { // convert boot volume model var bootVolumeModel = &bootVolumeModel{} @@ -1030,6 +1086,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo } } + var agent = &agentModel{} + if !(model.Agent.IsNull() || model.Agent.IsUnknown()) { + diags := model.Agent.As(ctx, agent, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("convert agent object to struct: %w", core.DiagsToError(diags)) + } + } + labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) @@ -1051,6 +1115,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo } } + var agentPayload *iaas.ServerAgent + // it is set and true, adjust payload + if !agent.Provisioned.IsNull() && !agent.Provisioned.IsUnknown() { + agentPayload = &iaas.ServerAgent{ + Provisioned: conversion.BoolValueToPointer(agent.Provisioned), + } + } + var userData *[]byte if !model.UserData.IsNull() && !model.UserData.IsUnknown() { src := []byte(model.UserData.ValueString()) @@ -1080,6 +1152,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo return &iaas.CreateServerPayload{ AffinityGroup: conversion.StringValueToPointer(model.AffinityGroup), AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone), + Agent: agentPayload, BootVolume: bootVolumePayload, ImageId: conversion.StringValueToPointer(model.ImageId), KeypairName: conversion.StringValueToPointer(model.KeypairName), diff --git a/stackit/internal/services/iaas/server/resource_test.go b/stackit/internal/services/iaas/server/resource_test.go index ad1c70741..ddfc8594a 100644 --- a/stackit/internal/services/iaas/server/resource_test.go +++ b/stackit/internal/services/iaas/server/resource_test.go @@ -59,6 +59,7 @@ func TestMapFields(t *testing.T) { ImageId: types.StringNull(), NetworkInterfaces: types.ListNull(types.StringType), KeypairName: types.StringNull(), + Agent: types.ObjectNull(agentTypes), AffinityGroup: types.StringNull(), UserData: types.StringNull(), CreatedAt: types.StringNull(), @@ -92,7 +93,10 @@ func TestMapFields(t *testing.T) { NicId: utils.Ptr("nic2"), }, }, - KeypairName: utils.Ptr("keypair_name"), + KeypairName: utils.Ptr("keypair_name"), + Agent: &iaas.ServerAgent{ + Provisioned: utils.Ptr(true), + }, AffinityGroup: utils.Ptr("group_id"), CreatedAt: utils.Ptr(testTimestamp()), UpdatedAt: utils.Ptr(testTimestamp()), @@ -113,11 +117,14 @@ func TestMapFields(t *testing.T) { ImageId: types.StringValue("image_id"), NetworkInterfaces: types.ListNull(types.StringType), KeypairName: types.StringValue("keypair_name"), - AffinityGroup: types.StringValue("group_id"), - CreatedAt: types.StringValue(testTimestampValue), - UpdatedAt: types.StringValue(testTimestampValue), - LaunchedAt: types.StringValue(testTimestampValue), - Region: types.StringValue("eu02"), + Agent: types.ObjectValueMust(agentTypes, map[string]attr.Value{ + "provisioned": types.BoolValue(true), + }), + AffinityGroup: types.StringValue("group_id"), + CreatedAt: types.StringValue(testTimestampValue), + UpdatedAt: types.StringValue(testTimestampValue), + LaunchedAt: types.StringValue(testTimestampValue), + Region: types.StringValue("eu02"), }, isValid: true, }, @@ -144,6 +151,7 @@ func TestMapFields(t *testing.T) { ImageId: types.StringNull(), NetworkInterfaces: types.ListNull(types.StringType), KeypairName: types.StringNull(), + Agent: types.ObjectNull(agentTypes), AffinityGroup: types.StringNull(), UserData: types.StringNull(), CreatedAt: types.StringNull(), @@ -216,6 +224,9 @@ func TestToCreatePayload(t *testing.T) { types.StringValue("nic1"), types.StringValue("nic2"), }), + Agent: types.ObjectValueMust(agentTypes, map[string]attr.Value{ + "provisioned": types.BoolValue(true), + }), }, expected: &iaas.CreateServerPayload{ Name: utils.Ptr("name"), @@ -240,6 +251,9 @@ func TestToCreatePayload(t *testing.T) { NicIds: &[]string{"nic1", "nic2"}, }, }, + Agent: &iaas.ServerAgent{ + Provisioned: utils.Ptr(true), + }, }, isValid: true, }, @@ -267,6 +281,9 @@ func TestToCreatePayload(t *testing.T) { types.StringValue("nic1"), types.StringValue("nic2"), }), + Agent: types.ObjectValueMust(agentTypes, map[string]attr.Value{ + "provisioned": types.BoolValue(true), + }), }, expected: &iaas.CreateServerPayload{ Name: utils.Ptr("name"), @@ -292,6 +309,9 @@ func TestToCreatePayload(t *testing.T) { NicIds: &[]string{"nic1", "nic2"}, }, }, + Agent: &iaas.ServerAgent{ + Provisioned: utils.Ptr(true), + }, }, isValid: true, },