Skip to content

Commit b3a0d7c

Browse files
authored
Add external subnet attachment resource. (#617)
----- ### Pull request checklist - [x] Add changelog entry for this change.
1 parent 7efcac8 commit b3a0d7c

7 files changed

Lines changed: 678 additions & 1 deletion

File tree

.changelog/0.18.0.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ description = "`oxide_subnet_pool` [#614](https://github.com/oxidecomputer/terra
4242
title = "New resource"
4343
description = "`oxide_subnet_pool_silo_link` [#615](https://github.com/oxidecomputer/terraform-provider-oxide/pull/615)."
4444

45+
[[features]]
46+
title = "New resource"
47+
description = "`oxide_subnet_pool_attachment` [#617](https://github.com/oxidecomputer/terraform-provider-oxide/pull/617)."
48+
4549
[[features]]
4650
title = "New resource"
4751
description = "`oxide_switch_port_settings` [#475](https://github.com/oxidecomputer/terraform-provider-oxide/pull/475)."
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "oxide_external_subnet_attachment Resource - terraform-provider-oxide"
4+
subcategory: ""
5+
description: |-
6+
This resource manages the attachment of an external subnet to an instance.
7+
---
8+
9+
# oxide_external_subnet_attachment (Resource)
10+
11+
This resource manages the attachment of an external subnet to an instance.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `external_subnet_id` (String) ID of the external subnet to attach.
21+
- `instance_id` (String) ID of the instance to attach the external subnet to.
22+
23+
### Optional
24+
25+
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))
26+
27+
### Read-Only
28+
29+
- `id` (String) Unique identifier for the attachment. Set to the external subnet ID.
30+
31+
<a id="nestedatt--timeouts"></a>
32+
### Nested Schema for `timeouts`
33+
34+
Optional:
35+
36+
- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
37+
- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs.
38+
- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ require (
1919
github.com/hashicorp/terraform-plugin-log v0.10.0
2020
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.2
2121
github.com/hashicorp/terraform-plugin-testing v1.14.0
22-
github.com/oxidecomputer/oxide.go v0.7.1-0.20260203205921-06dacbeebae7
22+
github.com/oxidecomputer/oxide.go v0.7.1-0.20260206035828-b5d31ae54de1
2323
github.com/stretchr/testify v1.11.1
2424
)
2525

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,8 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9
623623
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
624624
github.com/oxidecomputer/oxide.go v0.7.1-0.20260203205921-06dacbeebae7 h1:taKDndmbieTpMvuE/ojkVtfAjXQ8dlLmjx44Z8s6KJs=
625625
github.com/oxidecomputer/oxide.go v0.7.1-0.20260203205921-06dacbeebae7/go.mod h1:ZlG5ri4a6ClA/yhDCbQN6m/F3d/m41XHx5s9WbfFLWc=
626+
github.com/oxidecomputer/oxide.go v0.7.1-0.20260206035828-b5d31ae54de1 h1:arJbgudVNdJnfEeZVcl+NXDSZyWo9uV7f7Jd2pO5vbU=
627+
github.com/oxidecomputer/oxide.go v0.7.1-0.20260206035828-b5d31ae54de1/go.mod h1:ZlG5ri4a6ClA/yhDCbQN6m/F3d/m41XHx5s9WbfFLWc=
626628
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
627629
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
628630
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ func (p *oxideProvider) Resources(_ context.Context) []func() resource.Resource
189189
NewAddressLotResource,
190190
NewAntiAffinityGroupResource,
191191
NewDiskResource,
192+
NewExternalSubnetAttachmentResource,
192193
NewExternalSubnetResource,
193194
NewFloatingIPResource,
194195
NewImageResource,
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
package provider
6+
7+
import (
8+
"context"
9+
"fmt"
10+
11+
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
12+
"github.com/hashicorp/terraform-plugin-framework/path"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
18+
"github.com/hashicorp/terraform-plugin-log/tflog"
19+
"github.com/oxidecomputer/oxide.go/oxide"
20+
)
21+
22+
// Ensure the implementation satisfies the expected interfaces.
23+
var (
24+
_ resource.Resource = (*externalSubnetAttachmentResource)(nil)
25+
_ resource.ResourceWithConfigure = (*externalSubnetAttachmentResource)(nil)
26+
_ resource.ResourceWithImportState = (*externalSubnetAttachmentResource)(nil)
27+
)
28+
29+
// NewExternalSubnetAttachmentResource is a helper function to simplify the provider implementation.
30+
func NewExternalSubnetAttachmentResource() resource.Resource {
31+
return &externalSubnetAttachmentResource{}
32+
}
33+
34+
// externalSubnetAttachmentResource is the resource implementation.
35+
type externalSubnetAttachmentResource struct {
36+
client *oxide.Client
37+
}
38+
39+
type externalSubnetAttachmentResourceModel struct {
40+
ID types.String `tfsdk:"id"`
41+
ExternalSubnetID types.String `tfsdk:"external_subnet_id"`
42+
InstanceID types.String `tfsdk:"instance_id"`
43+
Timeouts timeouts.Value `tfsdk:"timeouts"`
44+
}
45+
46+
// Metadata returns the resource type name.
47+
func (r *externalSubnetAttachmentResource) Metadata(
48+
_ context.Context,
49+
req resource.MetadataRequest,
50+
resp *resource.MetadataResponse,
51+
) {
52+
resp.TypeName = "oxide_external_subnet_attachment"
53+
}
54+
55+
// Configure adds the provider configured client to the resource.
56+
func (r *externalSubnetAttachmentResource) Configure(
57+
_ context.Context,
58+
req resource.ConfigureRequest,
59+
_ *resource.ConfigureResponse,
60+
) {
61+
if req.ProviderData == nil {
62+
return
63+
}
64+
65+
r.client = req.ProviderData.(*oxide.Client)
66+
}
67+
68+
// ImportState imports an external subnet attachment using the external subnet ID.
69+
func (r *externalSubnetAttachmentResource) ImportState(
70+
ctx context.Context,
71+
req resource.ImportStateRequest,
72+
resp *resource.ImportStateResponse,
73+
) {
74+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
75+
}
76+
77+
// Schema defines the schema for the resource.
78+
func (r *externalSubnetAttachmentResource) Schema(
79+
ctx context.Context,
80+
_ resource.SchemaRequest,
81+
resp *resource.SchemaResponse,
82+
) {
83+
resp.Schema = schema.Schema{
84+
MarkdownDescription: "This resource manages the attachment of an external subnet to an instance.",
85+
Attributes: map[string]schema.Attribute{
86+
// External subnet attachments don't have their own IDs, and a given external subnet can
87+
// be attached to at most one instance, so we use the instance ID as the ID of the
88+
// attachment.
89+
"id": schema.StringAttribute{
90+
Computed: true,
91+
Description: "Unique identifier for the attachment. Set to the external subnet ID.",
92+
PlanModifiers: []planmodifier.String{
93+
stringplanmodifier.UseStateForUnknown(),
94+
},
95+
},
96+
"external_subnet_id": schema.StringAttribute{
97+
Required: true,
98+
Description: "ID of the external subnet to attach.",
99+
PlanModifiers: []planmodifier.String{
100+
stringplanmodifier.RequiresReplace(),
101+
},
102+
},
103+
"instance_id": schema.StringAttribute{
104+
Required: true,
105+
Description: "ID of the instance to attach the external subnet to.",
106+
PlanModifiers: []planmodifier.String{
107+
stringplanmodifier.RequiresReplace(),
108+
},
109+
},
110+
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
111+
Create: true,
112+
Read: true,
113+
Delete: true,
114+
}),
115+
},
116+
}
117+
}
118+
119+
// Create creates the resource and sets the initial Terraform state.
120+
func (r *externalSubnetAttachmentResource) Create(
121+
ctx context.Context,
122+
req resource.CreateRequest,
123+
resp *resource.CreateResponse,
124+
) {
125+
var plan externalSubnetAttachmentResourceModel
126+
127+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
128+
if resp.Diagnostics.HasError() {
129+
return
130+
}
131+
132+
createTimeout, diags := plan.Timeouts.Create(ctx, defaultTimeout())
133+
resp.Diagnostics.Append(diags...)
134+
if resp.Diagnostics.HasError() {
135+
return
136+
}
137+
ctx, cancel := context.WithTimeout(ctx, createTimeout)
138+
defer cancel()
139+
140+
params := oxide.ExternalSubnetAttachParams{
141+
ExternalSubnet: oxide.NameOrId(plan.ExternalSubnetID.ValueString()),
142+
Body: &oxide.ExternalSubnetAttach{
143+
Instance: oxide.NameOrId(plan.InstanceID.ValueString()),
144+
},
145+
}
146+
147+
externalSubnet, err := r.client.ExternalSubnetAttach(ctx, params)
148+
if err != nil {
149+
resp.Diagnostics.AddError(
150+
"Error attaching external subnet",
151+
"API error: "+err.Error(),
152+
)
153+
return
154+
}
155+
156+
tflog.Trace(
157+
ctx,
158+
fmt.Sprintf(
159+
"attached external subnet %v to instance %v",
160+
externalSubnet.Id,
161+
externalSubnet.InstanceId,
162+
),
163+
map[string]any{"success": true},
164+
)
165+
166+
plan.ID = types.StringValue(externalSubnet.Id)
167+
plan.ExternalSubnetID = types.StringValue(externalSubnet.Id)
168+
plan.InstanceID = types.StringValue(externalSubnet.InstanceId)
169+
170+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
171+
if resp.Diagnostics.HasError() {
172+
return
173+
}
174+
}
175+
176+
// Read refreshes the Terraform state with the latest data.
177+
func (r *externalSubnetAttachmentResource) Read(
178+
ctx context.Context,
179+
req resource.ReadRequest,
180+
resp *resource.ReadResponse,
181+
) {
182+
var state externalSubnetAttachmentResourceModel
183+
184+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
185+
if resp.Diagnostics.HasError() {
186+
return
187+
}
188+
189+
readTimeout, diags := state.Timeouts.Read(ctx, defaultTimeout())
190+
resp.Diagnostics.Append(diags...)
191+
if resp.Diagnostics.HasError() {
192+
return
193+
}
194+
ctx, cancel := context.WithTimeout(ctx, readTimeout)
195+
defer cancel()
196+
197+
params := oxide.ExternalSubnetViewParams{
198+
ExternalSubnet: oxide.NameOrId(state.ID.ValueString()),
199+
}
200+
201+
externalSubnet, err := r.client.ExternalSubnetView(ctx, params)
202+
if err != nil {
203+
if is404(err) {
204+
resp.State.RemoveResource(ctx)
205+
return
206+
}
207+
resp.Diagnostics.AddError(
208+
"Unable to read external subnet attachment:",
209+
"API error: "+err.Error(),
210+
)
211+
return
212+
}
213+
214+
// If the subnet is no longer attached to any instance, remove from state.
215+
if externalSubnet.InstanceId == "" {
216+
resp.State.RemoveResource(ctx)
217+
return
218+
}
219+
220+
tflog.Trace(
221+
ctx,
222+
fmt.Sprintf("read external subnet attachment with ID: %v", externalSubnet.Id),
223+
map[string]any{"success": true},
224+
)
225+
226+
state.ID = types.StringValue(externalSubnet.Id)
227+
state.ExternalSubnetID = types.StringValue(externalSubnet.Id)
228+
state.InstanceID = types.StringValue(externalSubnet.InstanceId)
229+
230+
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
231+
if resp.Diagnostics.HasError() {
232+
return
233+
}
234+
}
235+
236+
// Update updates the resource and sets the updated Terraform state on success.
237+
// Only timeouts can change in-place; both mutable attributes trigger replacement.
238+
func (r *externalSubnetAttachmentResource) Update(
239+
ctx context.Context,
240+
req resource.UpdateRequest,
241+
resp *resource.UpdateResponse,
242+
) {
243+
var plan externalSubnetAttachmentResourceModel
244+
245+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
246+
if resp.Diagnostics.HasError() {
247+
return
248+
}
249+
250+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
251+
if resp.Diagnostics.HasError() {
252+
return
253+
}
254+
}
255+
256+
// Delete deletes the resource and removes the Terraform state on success.
257+
func (r *externalSubnetAttachmentResource) Delete(
258+
ctx context.Context,
259+
req resource.DeleteRequest,
260+
resp *resource.DeleteResponse,
261+
) {
262+
var state externalSubnetAttachmentResourceModel
263+
264+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
265+
if resp.Diagnostics.HasError() {
266+
return
267+
}
268+
269+
deleteTimeout, diags := state.Timeouts.Delete(ctx, defaultTimeout())
270+
resp.Diagnostics.Append(diags...)
271+
if resp.Diagnostics.HasError() {
272+
return
273+
}
274+
ctx, cancel := context.WithTimeout(ctx, deleteTimeout)
275+
defer cancel()
276+
277+
// Only detach if the subnet is still attached to the expected
278+
// instance. If it's gone, already detached, or re-attached to
279+
// a different instance out of band, there's nothing to do.
280+
viewParams := oxide.ExternalSubnetViewParams{
281+
ExternalSubnet: oxide.NameOrId(state.ID.ValueString()),
282+
}
283+
externalSubnet, err := r.client.ExternalSubnetView(
284+
ctx, viewParams,
285+
)
286+
if err != nil {
287+
if is404(err) {
288+
return
289+
}
290+
resp.Diagnostics.AddError(
291+
"Error reading external subnet during delete:",
292+
"API error: "+err.Error(),
293+
)
294+
return
295+
}
296+
297+
if externalSubnet.InstanceId != state.InstanceID.ValueString() {
298+
return
299+
}
300+
301+
detachParams := oxide.ExternalSubnetDetachParams{
302+
ExternalSubnet: oxide.NameOrId(state.ID.ValueString()),
303+
}
304+
if _, err := r.client.ExternalSubnetDetach(
305+
ctx, detachParams,
306+
); err != nil {
307+
if !is404(err) {
308+
resp.Diagnostics.AddError(
309+
"Error detaching external subnet:",
310+
"API error: "+err.Error(),
311+
)
312+
return
313+
}
314+
}
315+
316+
tflog.Trace(
317+
ctx,
318+
fmt.Sprintf("detached external subnet with ID: %v", state.ID.ValueString()),
319+
map[string]any{"success": true},
320+
)
321+
}

0 commit comments

Comments
 (0)