Skip to content

Commit 663403c

Browse files
authored
Anti-affinity groups on instance create (#414)
Depends on oxidecomputer/oxide.go#276 Closes: #411 Tested against dogfood rack ```console $ TEST_ACC_NAME=TestAccCloudResourceInstance make testacc -> Running terraform acceptance tests === RUN TestAccCloudResourceInstance_full === PAUSE TestAccCloudResourceInstance_full === RUN TestAccCloudResourceInstance_extIPs === PAUSE TestAccCloudResourceInstance_extIPs === RUN TestAccCloudResourceInstance_sshKeys === PAUSE TestAccCloudResourceInstance_sshKeys === RUN TestAccCloudResourceInstance_nic === PAUSE TestAccCloudResourceInstance_nic === RUN TestAccCloudResourceInstance_disk === PAUSE TestAccCloudResourceInstance_disk === RUN TestAccCloudResourceInstance_update === PAUSE TestAccCloudResourceInstance_update === RUN TestAccCloudResourceInstance_antiAffinityGroups === PAUSE TestAccCloudResourceInstance_antiAffinityGroups === CONT TestAccCloudResourceInstance_full === CONT TestAccCloudResourceInstance_disk === CONT TestAccCloudResourceInstance_antiAffinityGroups === CONT TestAccCloudResourceInstance_sshKeys === CONT TestAccCloudResourceInstance_extIPs === CONT TestAccCloudResourceInstance_nic --- PASS: TestAccCloudResourceInstance_extIPs (9.82s) === CONT TestAccCloudResourceInstance_update --- PASS: TestAccCloudResourceInstance_sshKeys (9.87s) --- PASS: TestAccCloudResourceInstance_full (39.50s) --- PASS: TestAccCloudResourceInstance_antiAffinityGroups (44.39s) --- PASS: TestAccCloudResourceInstance_nic (45.10s) --- PASS: TestAccCloudResourceInstance_disk (58.12s) --- PASS: TestAccCloudResourceInstance_update (67.77s) PASS ok github.com/oxidecomputer/terraform-provider-oxide/internal/provider 77.921s ```
1 parent 021b0f8 commit 663403c

File tree

6 files changed

+481
-85
lines changed

6 files changed

+481
-85
lines changed

.changelog/0.8.0.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ title = ""
33
description = ""
44

55
[[features]]
6-
title = ""
7-
description = ""
6+
title = "New instance argument"
7+
description = "It is now possible to manage anti-affinity group assigments on instance resources. [#414](https://github.com/oxidecomputer/terraform-provider-oxide/pull/414)."
88

99
[[features]]
1010
title = "New resource"

docs/resources/oxide_instance.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ page_title: "oxide_instance Resource - terraform-provider-oxide"
66

77
This resource manages instances.
88

9-
-> Updates will stop and reboot the instance.
9+
!> Updates will stop and start the instance.
1010

1111
-> When setting a boot disk using `boot_disk_id`, the boot disk ID must also be
1212
present in `disk_attachments`.
@@ -27,19 +27,20 @@ resource "oxide_instance" "example" {
2727
}
2828
```
2929

30-
### Instance with user data and an SSH public key
30+
### Instance with user data and an SSH public key and anti-affinity group
3131

3232
```hcl
3333
resource "oxide_instance" "example" {
34-
project_id = "c1dee930-a8e4-11ed-afa1-0242ac120002"
35-
description = "Example instance."
36-
name = "myinstance"
37-
host_name = "myhostname"
38-
memory = 10737418240
39-
ncpus = 1
40-
disk_attachments = ["611bb17d-6883-45be-b3aa-8a186fdeafe8"]
41-
ssh_public_keys = ["066cab1b-c550-4aea-8a80-8422fd3bfc40"]
42-
user_data = filebase64("path/to/init.sh")
34+
project_id = "c1dee930-a8e4-11ed-afa1-0242ac120002"
35+
description = "Example instance."
36+
name = "myinstance"
37+
host_name = "myhostname"
38+
memory = 10737418240
39+
ncpus = 1
40+
anti_affinity_groups = ["9b9f9be1-96bf-44ad-864a-0dedae3b3999"]
41+
disk_attachments = ["611bb17d-6883-45be-b3aa-8a186fdeafe8"]
42+
ssh_public_keys = ["066cab1b-c550-4aea-8a80-8422fd3bfc40"]
43+
user_data = filebase64("path/to/init.sh")
4344
}
4445
```
4546

@@ -107,6 +108,7 @@ resource "oxide_instance" "example" {
107108

108109
### Optional
109110

111+
- `anti_affinity_groups` (Set of String, Optional) The IDs of the anti-affinity groups this instance should belong to.
110112
- `boot_disk_id` (String, Optional) ID of the disk to boot the instance from. When provided, this ID must also be present in `disk_attachments`.
111113
- `disk_attachments` (Set of String, Optional) IDs of the disks to be attached to the instance. When multiple disk IDs are provided, set `book_disk_id` to specify the boot disk for the instance. Otherwise, a boot disk will be chosen randomly.
112114
- `external_ips` (Set of Object, Optional) External IP addresses associated with the instance. See [below for nested schema](#nestedatt--ips).

internal/provider/resource_instance.go

Lines changed: 153 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,24 @@ type instanceResource struct {
4646
}
4747

4848
type instanceResourceModel struct {
49-
BootDiskID types.String `tfsdk:"boot_disk_id"`
50-
Description types.String `tfsdk:"description"`
51-
DiskAttachments types.Set `tfsdk:"disk_attachments"`
52-
ExternalIPs []instanceResourceExternalIPModel `tfsdk:"external_ips"`
53-
HostName types.String `tfsdk:"host_name"`
54-
ID types.String `tfsdk:"id"`
55-
Memory types.Int64 `tfsdk:"memory"`
56-
Name types.String `tfsdk:"name"`
57-
NetworkInterfaces []instanceResourceNICModel `tfsdk:"network_interfaces"`
58-
NCPUs types.Int64 `tfsdk:"ncpus"`
59-
ProjectID types.String `tfsdk:"project_id"`
60-
SSHPublicKeys types.Set `tfsdk:"ssh_public_keys"`
61-
StartOnCreate types.Bool `tfsdk:"start_on_create"`
62-
TimeCreated types.String `tfsdk:"time_created"`
63-
TimeModified types.String `tfsdk:"time_modified"`
64-
Timeouts timeouts.Value `tfsdk:"timeouts"`
65-
UserData types.String `tfsdk:"user_data"`
49+
AntiAffinityGroups types.Set `tfsdk:"anti_affinity_groups"`
50+
BootDiskID types.String `tfsdk:"boot_disk_id"`
51+
Description types.String `tfsdk:"description"`
52+
DiskAttachments types.Set `tfsdk:"disk_attachments"`
53+
ExternalIPs []instanceResourceExternalIPModel `tfsdk:"external_ips"`
54+
HostName types.String `tfsdk:"host_name"`
55+
ID types.String `tfsdk:"id"`
56+
Memory types.Int64 `tfsdk:"memory"`
57+
Name types.String `tfsdk:"name"`
58+
NetworkInterfaces []instanceResourceNICModel `tfsdk:"network_interfaces"`
59+
NCPUs types.Int64 `tfsdk:"ncpus"`
60+
ProjectID types.String `tfsdk:"project_id"`
61+
SSHPublicKeys types.Set `tfsdk:"ssh_public_keys"`
62+
StartOnCreate types.Bool `tfsdk:"start_on_create"`
63+
TimeCreated types.String `tfsdk:"time_created"`
64+
TimeModified types.String `tfsdk:"time_modified"`
65+
Timeouts timeouts.Value `tfsdk:"timeouts"`
66+
UserData types.String `tfsdk:"user_data"`
6667
}
6768

6869
type instanceResourceNICModel struct {
@@ -141,6 +142,11 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest,
141142
Required: true,
142143
Description: "Number of CPUs allocated for this instance.",
143144
},
145+
"anti_affinity_groups": schema.SetAttribute{
146+
Optional: true,
147+
Description: "IDs of the anti-affinity groups this instance should belong to.",
148+
ElementType: types.StringType,
149+
},
144150
"boot_disk_id": schema.StringAttribute{
145151
Optional: true,
146152
Description: "ID of the disk the instance should be booted from.",
@@ -372,14 +378,20 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
372378
}
373379
}
374380

375-
// TODO: Double check we're not triggering the default "add all keys" behaviour
376-
sshKeys, diags := newSSHKeysOnCreate(plan.SSHPublicKeys)
381+
sshKeys, diags := newNameOrIdList(plan.SSHPublicKeys)
377382
resp.Diagnostics.Append(diags...)
378383
if resp.Diagnostics.HasError() {
379384
return
380385
}
381386
params.Body.SshPublicKeys = sshKeys
382387

388+
antiAffinityGroupIDs, diags := newNameOrIdList(plan.AntiAffinityGroups)
389+
resp.Diagnostics.Append(diags...)
390+
if resp.Diagnostics.HasError() {
391+
return
392+
}
393+
params.Body.AntiAffinityGroups = antiAffinityGroupIDs
394+
383395
disks, diags := newDiskAttachmentsOnCreate(ctx, r.client, plan.DiskAttachments)
384396
resp.Diagnostics.Append(diags...)
385397
if resp.Diagnostics.HasError() {
@@ -508,6 +520,16 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
508520
state.SSHPublicKeys = keySet
509521
}
510522

523+
antiAffinityGroupSet, diags := newAssociatedAntiAffinityGroupsOnCreateSet(ctx, r.client, state.ID.ValueString())
524+
resp.Diagnostics.Append(diags...)
525+
if resp.Diagnostics.HasError() {
526+
return
527+
}
528+
// Only set the anti-affinity group list if there are any associated groups
529+
if len(antiAffinityGroupSet.Elements()) > 0 {
530+
state.AntiAffinityGroups = antiAffinityGroupSet
531+
}
532+
511533
diskSet, diags := newAttachedDisksSet(ctx, r.client, state.ID.ValueString())
512534
resp.Diagnostics.Append(diags...)
513535
if resp.Diagnostics.HasError() {
@@ -655,6 +677,24 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
655677
return
656678
}
657679

680+
// Update anti-affinity groups
681+
planAntiAffinityGroups := plan.AntiAffinityGroups.Elements()
682+
stateAntiAffinityGroups := state.AntiAffinityGroups.Elements()
683+
684+
// Check plan and if it has an ID that the state doesn't then add it
685+
antiAffinityGroupsToAdd := sliceDiff(planAntiAffinityGroups, stateAntiAffinityGroups)
686+
resp.Diagnostics.Append(addAntiAffinityGroups(ctx, r.client, antiAffinityGroupsToAdd, state.ID.ValueString())...)
687+
if resp.Diagnostics.HasError() {
688+
return
689+
}
690+
691+
// Check state and if it has an ID that the plan doesn't then remove it
692+
antiAffinityGroupsToRemove := sliceDiff(stateAntiAffinityGroups, planAntiAffinityGroups)
693+
resp.Diagnostics.Append(removeAntiAffinityGroups(ctx, r.client, antiAffinityGroupsToRemove, state.ID.ValueString())...)
694+
if resp.Diagnostics.HasError() {
695+
return
696+
}
697+
658698
startParams := oxide.InstanceStartParams{Instance: oxide.NameOrId(state.ID.ValueString())}
659699
_, err = r.client.InstanceStart(ctx, startParams)
660700
if err != nil {
@@ -877,6 +917,36 @@ func newAssociatedSSHKeysOnCreateSet(ctx context.Context, client *oxide.Client,
877917
return keySet, nil
878918
}
879919

920+
func newAssociatedAntiAffinityGroupsOnCreateSet(ctx context.Context, client *oxide.Client, instanceID string) (types.Set, diag.Diagnostics) {
921+
var diags diag.Diagnostics
922+
923+
params := oxide.InstanceAntiAffinityGroupListParams{
924+
Limit: oxide.NewPointer(1000000000),
925+
Instance: oxide.NameOrId(instanceID),
926+
}
927+
groups, err := client.InstanceAntiAffinityGroupList(ctx, params)
928+
if err != nil {
929+
diags.AddError(
930+
"Unable to list associated anti-affinity groups:",
931+
"API error: "+err.Error(),
932+
)
933+
return types.SetNull(types.StringType), diags
934+
}
935+
936+
d := []attr.Value{}
937+
for _, group := range groups.Items {
938+
id := types.StringValue(group.Id)
939+
d = append(d, id)
940+
}
941+
groupSet, diags := types.SetValue(types.StringType, d)
942+
diags.Append(diags...)
943+
if diags.HasError() {
944+
return types.SetNull(types.StringType), diags
945+
}
946+
947+
return groupSet, nil
948+
}
949+
880950
func newNetworkInterfaceAttachment(ctx context.Context, client *oxide.Client, model []instanceResourceNICModel) (
881951
oxide.InstanceNetworkInterfaceAttachment, diag.Diagnostics) {
882952
var diags diag.Diagnostics
@@ -1030,26 +1100,6 @@ func newDiskAttachmentsOnCreate(ctx context.Context, client *oxide.Client, diskI
10301100
return disks, diags
10311101
}
10321102

1033-
func newSSHKeysOnCreate(sshKeyIDs types.Set) ([]oxide.NameOrId, diag.Diagnostics) {
1034-
var diags diag.Diagnostics
1035-
var sshKeys = []oxide.NameOrId{}
1036-
for _, sshKeyID := range sshKeyIDs.Elements() {
1037-
id, err := strconv.Unquote(sshKeyID.String())
1038-
if err != nil {
1039-
diags.AddError(
1040-
"Error retrieving SSH public key ID information",
1041-
"SSH public key ID parse error: "+err.Error(),
1042-
)
1043-
return []oxide.NameOrId{}, diags
1044-
}
1045-
1046-
da := oxide.NameOrId(id)
1047-
sshKeys = append(sshKeys, da)
1048-
}
1049-
1050-
return sshKeys, diags
1051-
}
1052-
10531103
func newExternalIPsOnCreate(externalIPs []instanceResourceExternalIPModel) []oxide.ExternalIpCreate {
10541104
var ips []oxide.ExternalIpCreate
10551105

@@ -1201,6 +1251,70 @@ func detachDisks(ctx context.Context, client *oxide.Client, disks []attr.Value,
12011251
return nil
12021252
}
12031253

1254+
func addAntiAffinityGroups(
1255+
ctx context.Context, client *oxide.Client, groups []attr.Value, instanceID string) diag.Diagnostics {
1256+
var diags diag.Diagnostics
1257+
1258+
for _, v := range groups {
1259+
id, err := strconv.Unquote(v.String())
1260+
if err != nil {
1261+
diags.AddError(
1262+
"Error adding anti-affinity group",
1263+
"anti-affinity group ID parse error: "+err.Error(),
1264+
)
1265+
return diags
1266+
}
1267+
1268+
params := oxide.AntiAffinityGroupMemberInstanceAddParams{
1269+
Instance: oxide.NameOrId(instanceID),
1270+
AntiAffinityGroup: oxide.NameOrId(id),
1271+
}
1272+
_, err = client.AntiAffinityGroupMemberInstanceAdd(ctx, params)
1273+
if err != nil {
1274+
diags.AddError(
1275+
"Error adding anti-affinity group",
1276+
"API error: "+err.Error(),
1277+
)
1278+
return diags
1279+
}
1280+
tflog.Trace(ctx, fmt.Sprintf("added anti-affinity group with ID: %v", v), map[string]any{"success": true})
1281+
}
1282+
1283+
return nil
1284+
}
1285+
1286+
func removeAntiAffinityGroups(
1287+
ctx context.Context, client *oxide.Client, groups []attr.Value, instanceID string) diag.Diagnostics {
1288+
var diags diag.Diagnostics
1289+
1290+
for _, v := range groups {
1291+
id, err := strconv.Unquote(v.String())
1292+
if err != nil {
1293+
diags.AddError(
1294+
"Error removing anti-affinity group",
1295+
"anti-affinity group ID parse error: "+err.Error(),
1296+
)
1297+
return diags
1298+
}
1299+
1300+
params := oxide.AntiAffinityGroupMemberInstanceDeleteParams{
1301+
Instance: oxide.NameOrId(instanceID),
1302+
AntiAffinityGroup: oxide.NameOrId(id),
1303+
}
1304+
err = client.AntiAffinityGroupMemberInstanceDelete(ctx, params)
1305+
if err != nil {
1306+
diags.AddError(
1307+
"Error removing anti-affinity group",
1308+
"API error: "+err.Error(),
1309+
)
1310+
return diags
1311+
}
1312+
tflog.Trace(ctx, fmt.Sprintf("removed anit-affinity group with ID: %v", v), map[string]any{"success": true})
1313+
}
1314+
1315+
return nil
1316+
}
1317+
12041318
func attrValueSliceContains(s []attr.Value, str string) (bool, error) {
12051319
for _, a := range s {
12061320
v, err := strconv.Unquote(a.String())

0 commit comments

Comments
 (0)