Skip to content

Commit c314ede

Browse files
authored
feat(iaas): add server agent provisioning fields (#1113)
relates to STACKITRCO-187
1 parent 7ad4100 commit c314ede

8 files changed

Lines changed: 210 additions & 8 deletions

File tree

docs/data-sources/server.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ data "stackit_server" "example" {
3434
### Read-Only
3535

3636
- `affinity_group` (String) The affinity group the server is assigned to.
37+
- `agent` (Attributes) STACKIT Server Agent as setup on the server (see [below for nested schema](#nestedatt--agent))
3738
- `availability_zone` (String) The availability zone of the server.
3839
- `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume))
3940
- `created_at` (String) Date-time when the server was created
@@ -48,6 +49,14 @@ data "stackit_server" "example" {
4849
- `updated_at` (String) Date-time when the server was updated
4950
- `user_data` (String) User data that is passed via cloud-init to the server.
5051

52+
<a id="nestedatt--agent"></a>
53+
### Nested Schema for `agent`
54+
55+
Read-Only:
56+
57+
- `provisioned` (Boolean) Whether a STACKIT Server Agent is provisioned at the server
58+
59+
5160
<a id="nestedatt--boot_volume"></a>
5261
### Nested Schema for `boot_volume`
5362

docs/resources/server.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ import {
404404
### Optional
405405

406406
- `affinity_group` (String) The affinity group the server is assigned to.
407+
- `agent` (Attributes) The STACKIT Server Agent configured for the server (see [below for nested schema](#nestedatt--agent))
407408
- `availability_zone` (String) The availability zone of the server.
408409
- `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume))
409410
- `desired_status` (String) The desired status of the server resource. Possible values are: `active`, `inactive`, `deallocated`.
@@ -422,6 +423,18 @@ import {
422423
- `server_id` (String) The server ID.
423424
- `updated_at` (String) Date-time when the server was updated
424425

426+
<a id="nestedatt--agent"></a>
427+
### Nested Schema for `agent`
428+
429+
Optional:
430+
431+
- `provisioning_policy` (String) Agent provisioning policy: `ALWAYS`, `NEVER`, or `INHERIT`. `INHERIT` follows the image default value.
432+
433+
Read-Only:
434+
435+
- `provisioned` (Boolean) Whether a STACKIT Server Agent is provisioned at the server
436+
437+
425438
<a id="nestedatt--boot_volume"></a>
426439
### Nested Schema for `boot_volume`
427440

stackit/internal/services/iaas/iaas_acc_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ var testConfigServerVarsMax = config.Variables{
149149
"service_account_mail": config.StringVariable(testutil.TestProjectServiceAccountEmail),
150150
"public_key": config.StringVariable(keypairPublicKey),
151151
"desired_status": config.StringVariable("active"),
152+
"agent_policy": config.StringVariable("ALWAYS"),
152153
}
153154

154155
var testConfigServerVarsMaxUpdated = func() config.Variables {
@@ -158,6 +159,7 @@ var testConfigServerVarsMaxUpdated = func() config.Variables {
158159
updatedConfig["machine_type"] = config.StringVariable("t1.2")
159160
updatedConfig["label"] = config.StringVariable("updated")
160161
updatedConfig["desired_status"] = config.StringVariable("inactive")
162+
updatedConfig["agent_policy"] = config.StringVariable("NEVER")
161163
return updatedConfig
162164
}()
163165

@@ -2223,6 +2225,8 @@ func TestAccServerMin(t *testing.T) {
22232225
resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"),
22242226
resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"),
22252227
resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"),
2228+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioning_policy", "INHERIT"),
2229+
resource.TestCheckNoResourceAttr("stackit_server.server", "agent.provisioned"),
22262230
),
22272231
},
22282232
// Data source
@@ -2275,6 +2279,7 @@ func TestAccServerMin(t *testing.T) {
22752279
resource.TestCheckResourceAttrSet("data.stackit_server.server", "created_at"),
22762280
resource.TestCheckResourceAttrSet("data.stackit_server.server", "launched_at"),
22772281
resource.TestCheckResourceAttrSet("data.stackit_server.server", "updated_at"),
2282+
resource.TestCheckNoResourceAttr("data.stackit_server.server", "agent.provisioned"),
22782283
),
22792284
},
22802285
// Import
@@ -2328,6 +2333,8 @@ func TestAccServerMin(t *testing.T) {
23282333
resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"),
23292334
resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"),
23302335
resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"),
2336+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioning_policy", "INHERIT"),
2337+
resource.TestCheckNoResourceAttr("stackit_server.server", "agent.provisioned"),
23312338
),
23322339
},
23332340
// Deletion is done by the framework implicitly
@@ -2352,6 +2359,10 @@ func TestAccServerMax(t *testing.T) {
23522359
resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "policy", testutil.ConvertConfigVariable(testConfigServerVarsMax["policy"])),
23532360
resource.TestCheckResourceAttrSet("stackit_affinity_group.affinity_group", "affinity_group_id"),
23542361

2362+
// Agent
2363+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioning_policy", testutil.ConvertConfigVariable(testConfigServerVarsMax["agent_policy"])),
2364+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioned", "true"),
2365+
23552366
// Volume base
23562367
resource.TestCheckResourceAttr("stackit_volume.base_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])),
23572368
resource.TestCheckResourceAttr("stackit_volume.base_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMax["availability_zone"])),
@@ -2500,6 +2511,7 @@ func TestAccServerMax(t *testing.T) {
25002511
"stackit_key_pair.key_pair", "name",
25012512
"data.stackit_server.server", "keypair_name",
25022513
),
2514+
resource.TestCheckResourceAttr("data.stackit_server.server", "agent.provisioned", "true"),
25032515
// All network interface which was are attached appear here
25042516
resource.TestCheckResourceAttr("data.stackit_server.server", "network_interfaces.#", "2"),
25052517
resource.TestCheckTypeSetElemAttrPair(
@@ -2725,7 +2737,7 @@ func TestAccServerMax(t *testing.T) {
27252737
},
27262738
ImportState: true,
27272739
ImportStateVerify: true,
2728-
ImportStateVerifyIgnore: []string{"boot_volume", "desired_status", "network_interfaces"}, // Field is not mapped as it is only relevant on creation
2740+
ImportStateVerifyIgnore: []string{"boot_volume", "desired_status", "network_interfaces", "agent"}, // Field is not mapped as it is only relevant on creation
27292741
},
27302742
// Update
27312743
{
@@ -2738,6 +2750,10 @@ func TestAccServerMax(t *testing.T) {
27382750
resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "policy", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["policy"])),
27392751
resource.TestCheckResourceAttrSet("stackit_affinity_group.affinity_group", "affinity_group_id"),
27402752

2753+
// Agent
2754+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioning_policy", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["agent_policy"])),
2755+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioned", "false"),
2756+
27412757
// Volume base
27422758
resource.TestCheckResourceAttr("stackit_volume.base_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["project_id"])),
27432759
resource.TestCheckResourceAttr("stackit_volume.base_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["availability_zone"])),
@@ -2843,6 +2859,10 @@ func TestAccServerMax(t *testing.T) {
28432859
resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "policy", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["policy"])),
28442860
resource.TestCheckResourceAttrSet("stackit_affinity_group.affinity_group", "affinity_group_id"),
28452861

2862+
// Agent
2863+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioning_policy", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["agent_policy"])),
2864+
resource.TestCheckResourceAttr("stackit_server.server", "agent.provisioned", "false"),
2865+
28462866
// Volume base
28472867
resource.TestCheckResourceAttr("stackit_volume.base_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["project_id"])),
28482868
resource.TestCheckResourceAttr("stackit_volume.base_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["availability_zone"])),

stackit/internal/services/iaas/server/datasource.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type DataSourceModel struct {
3535
ServerId types.String `tfsdk:"server_id"`
3636
MachineType types.String `tfsdk:"machine_type"`
3737
Name types.String `tfsdk:"name"`
38+
Agent types.Object `tfsdk:"agent"`
3839
AvailabilityZone types.String `tfsdk:"availability_zone"`
3940
BootVolume types.Object `tfsdk:"boot_volume"`
4041
ImageId types.String `tfsdk:"image_id"`
@@ -53,6 +54,10 @@ var bootVolumeDataTypes = map[string]attr.Type{
5354
"delete_on_termination": basetypes.BoolType{},
5455
}
5556

57+
var agentDataTypes = map[string]attr.Type{
58+
"provisioned": basetypes.BoolType{},
59+
}
60+
5661
// NewServerDataSource is a helper function to simplify the provider implementation.
5762
func NewServerDataSource() datasource.DataSource {
5863
return &serverDataSource{}
@@ -124,6 +129,16 @@ func (d *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
124129
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/)",
125130
Computed: true,
126131
},
132+
"agent": schema.SingleNestedAttribute{
133+
Description: "STACKIT Server Agent as setup on the server",
134+
Computed: true,
135+
Attributes: map[string]schema.Attribute{
136+
"provisioned": schema.BoolAttribute{
137+
Description: "Whether a STACKIT Server Agent is provisioned at the server",
138+
Computed: true,
139+
},
140+
},
141+
},
127142
"availability_zone": schema.StringAttribute{
128143
Description: "The availability zone of the server.",
129144
Computed: true,
@@ -305,6 +320,18 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da
305320
model.BootVolume = types.ObjectNull(bootVolumeDataTypes)
306321
}
307322

323+
agentProvisioned := types.BoolNull()
324+
if serverResp.Agent != nil && serverResp.Agent.Provisioned != nil {
325+
agentProvisioned = types.BoolPointerValue(serverResp.Agent.Provisioned)
326+
}
327+
agent, diags := types.ObjectValue(agentDataTypes, map[string]attr.Value{
328+
"provisioned": agentProvisioned,
329+
})
330+
if diags.HasError() {
331+
return fmt.Errorf("failed to map agent: %w", core.DiagsToError(diags))
332+
}
333+
model.Agent = agent
334+
308335
if serverResp.UserData != nil && len(*serverResp.UserData) > 0 {
309336
model.UserData = types.StringValue(string(*serverResp.UserData))
310337
}

stackit/internal/services/iaas/server/datasource_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import (
1010
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
1111
)
1212

13+
var expectedNullAgentData = types.ObjectValueMust(agentDataTypes, map[string]attr.Value{
14+
"provisioned": types.BoolNull(),
15+
})
16+
1317
func TestMapDataSourceFields(t *testing.T) {
1418
type args struct {
1519
state DataSourceModel
@@ -40,6 +44,7 @@ func TestMapDataSourceFields(t *testing.T) {
4044
ServerId: types.StringValue("sid"),
4145
Name: types.StringNull(),
4246
AvailabilityZone: types.StringNull(),
47+
Agent: expectedNullAgentData,
4348
Labels: types.MapNull(types.StringType),
4449
ImageId: types.StringNull(),
4550
NetworkInterfaces: types.ListNull(types.StringType),
@@ -77,7 +82,10 @@ func TestMapDataSourceFields(t *testing.T) {
7782
NicId: new("nic2"),
7883
},
7984
},
80-
KeypairName: new("keypair_name"),
85+
KeypairName: new("keypair_name"),
86+
Agent: &iaas.ServerAgent{
87+
Provisioned: new(true),
88+
},
8189
AffinityGroup: new("group_id"),
8290
CreatedAt: new(testTimestamp()),
8391
UpdatedAt: new(testTimestamp()),
@@ -100,7 +108,10 @@ func TestMapDataSourceFields(t *testing.T) {
100108
types.StringValue("nic1"),
101109
types.StringValue("nic2"),
102110
}),
103-
KeypairName: types.StringValue("keypair_name"),
111+
KeypairName: types.StringValue("keypair_name"),
112+
Agent: types.ObjectValueMust(agentDataTypes, map[string]attr.Value{
113+
"provisioned": types.BoolValue(true),
114+
}),
104115
AffinityGroup: types.StringValue("group_id"),
105116
CreatedAt: types.StringValue(testTimestampValue),
106117
UpdatedAt: types.StringValue(testTimestampValue),
@@ -131,6 +142,7 @@ func TestMapDataSourceFields(t *testing.T) {
131142
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
132143
ImageId: types.StringNull(),
133144
NetworkInterfaces: types.ListNull(types.StringType),
145+
Agent: expectedNullAgentData,
134146
KeypairName: types.StringNull(),
135147
AffinityGroup: types.StringNull(),
136148
UserData: types.StringNull(),

stackit/internal/services/iaas/server/resource.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
2727
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
2828
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
29+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
2930
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
3031
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
3132
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -65,6 +66,7 @@ type Model struct {
6566
ServerId types.String `tfsdk:"server_id"`
6667
MachineType types.String `tfsdk:"machine_type"`
6768
Name types.String `tfsdk:"name"`
69+
Agent types.Object `tfsdk:"agent"`
6870
AvailabilityZone types.String `tfsdk:"availability_zone"`
6971
BootVolume types.Object `tfsdk:"boot_volume"`
7072
ImageId types.String `tfsdk:"image_id"`
@@ -79,6 +81,12 @@ type Model struct {
7981
DesiredStatus types.String `tfsdk:"desired_status"`
8082
}
8183

84+
// Struct corresponding to Model.Agent
85+
type agentModel struct {
86+
Provisioned types.Bool `tfsdk:"provisioned"`
87+
ProvisioningPolicy types.String `tfsdk:"provisioning_policy"`
88+
}
89+
8290
// Struct corresponding to Model.BootVolume
8391
type bootVolumeModel struct {
8492
Id types.String `tfsdk:"id"`
@@ -99,6 +107,12 @@ var bootVolumeTypes = map[string]attr.Type{
99107
"id": basetypes.StringType{},
100108
}
101109

110+
// Types corresponding to agentModel
111+
var agentTypes = map[string]attr.Type{
112+
"provisioned": basetypes.BoolType{},
113+
"provisioning_policy": basetypes.StringType{},
114+
}
115+
102116
// NewServerResource is a helper function to simplify the provider implementation.
103117
func NewServerResource() resource.Resource {
104118
return &serverResource{}
@@ -277,6 +291,35 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
277291
Optional: true,
278292
Computed: true,
279293
},
294+
"agent": schema.SingleNestedAttribute{
295+
Description: "The STACKIT Server Agent configured for the server",
296+
Optional: true,
297+
Computed: true,
298+
PlanModifiers: []planmodifier.Object{
299+
objectplanmodifier.RequiresReplace(),
300+
},
301+
Attributes: map[string]schema.Attribute{
302+
"provisioned": schema.BoolAttribute{
303+
Description: "Whether a STACKIT Server Agent is provisioned at the server",
304+
Computed: true,
305+
PlanModifiers: []planmodifier.Bool{
306+
boolplanmodifier.UseStateForUnknown(),
307+
},
308+
},
309+
"provisioning_policy": schema.StringAttribute{
310+
Description: "Agent provisioning policy: `ALWAYS`, `NEVER`, or `INHERIT`. `INHERIT` follows the image default value.",
311+
Optional: true,
312+
Computed: true,
313+
Default: stringdefault.StaticString("INHERIT"),
314+
Validators: []validator.String{
315+
stringvalidator.OneOf("ALWAYS", "NEVER", "INHERIT"),
316+
},
317+
PlanModifiers: []planmodifier.String{
318+
stringplanmodifier.RequiresReplace(), // trigger recreation on change
319+
},
320+
},
321+
},
322+
},
280323
"boot_volume": schema.SingleNestedAttribute{
281324
Description: "The boot volume for the server",
282325
Optional: true,
@@ -993,6 +1036,33 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model, regio
9931036
model.NetworkInterfaces = types.ListNull(types.StringType)
9941037
}
9951038

1039+
// agent{...} block, determine the intent policy from Terraform state
1040+
currentPolicy := types.StringValue("INHERIT")
1041+
if !(model.Agent.IsNull() || model.Agent.IsUnknown()) {
1042+
var currentAgent agentModel
1043+
diags := model.Agent.As(ctx, &currentAgent, basetypes.ObjectAsOptions{})
1044+
if diags.HasError() {
1045+
return fmt.Errorf("failed to map agent: %w", core.DiagsToError(diags))
1046+
}
1047+
if !currentAgent.ProvisioningPolicy.IsNull() {
1048+
currentPolicy = currentAgent.ProvisioningPolicy
1049+
}
1050+
}
1051+
// agent{...} block, determine the 'provisioned' field from API response
1052+
agentProvisioned := types.BoolNull()
1053+
if serverResp.Agent != nil && serverResp.Agent.Provisioned != nil {
1054+
agentProvisioned = types.BoolPointerValue(serverResp.Agent.Provisioned)
1055+
}
1056+
// agent{...} block, finalizing
1057+
agent, diags := types.ObjectValue(agentTypes, map[string]attr.Value{
1058+
"provisioned": agentProvisioned,
1059+
"provisioning_policy": currentPolicy,
1060+
})
1061+
if diags.HasError() {
1062+
return fmt.Errorf("failed to map agent: %w", core.DiagsToError(diags))
1063+
}
1064+
model.Agent = agent
1065+
9961066
if serverResp.BootVolume != nil {
9971067
// convert boot volume model
9981068
var bootVolumeModel = &bootVolumeModel{}
@@ -1061,6 +1131,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
10611131
}
10621132
}
10631133

1134+
var agent = &agentModel{}
1135+
if !(model.Agent.IsNull() || model.Agent.IsUnknown()) {
1136+
diags := model.Agent.As(ctx, agent, basetypes.ObjectAsOptions{})
1137+
if diags.HasError() {
1138+
return nil, fmt.Errorf("convert agent object to struct: %w", core.DiagsToError(diags))
1139+
}
1140+
}
1141+
10641142
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
10651143
if err != nil {
10661144
return nil, fmt.Errorf("converting to Go map: %w", err)
@@ -1082,6 +1160,16 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
10821160
}
10831161
}
10841162

1163+
var agentPayload *iaas.ServerAgent
1164+
switch agent.ProvisioningPolicy.ValueString() {
1165+
case "ALWAYS":
1166+
agentPayload = &iaas.ServerAgent{Provisioned: conversion.BoolValueToPointer(types.BoolValue(true))}
1167+
case "NEVER":
1168+
agentPayload = &iaas.ServerAgent{Provisioned: conversion.BoolValueToPointer(types.BoolValue(false))}
1169+
case "INHERIT":
1170+
agentPayload = nil // "agent" key is omitted from JSON thanks to omitempty
1171+
}
1172+
10851173
var userData *[]byte
10861174
if !model.UserData.IsNull() && !model.UserData.IsUnknown() {
10871175
src := []byte(model.UserData.ValueString())
@@ -1111,6 +1199,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
11111199
return &iaas.CreateServerPayload{
11121200
AffinityGroup: conversion.StringValueToPointer(model.AffinityGroup),
11131201
AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone),
1202+
Agent: agentPayload,
11141203
BootVolume: bootVolumePayload,
11151204
ImageId: conversion.StringValueToPointer(model.ImageId),
11161205
KeypairName: conversion.StringValueToPointer(model.KeypairName),

0 commit comments

Comments
 (0)