Skip to content

Commit 2109a4f

Browse files
committed
resource(vpc_firewall_rules): add schema upgrade
Implement the `ResourceWithStateUpgrade` interface on `vpcFirewallRulesResource` to support upgrading existing Terraform states to the new schema implemented in #474. One important note is that this does not allow users to use new versions of the provider with previous versions of the Oxide API. It only updates existing Terraform states to avoid errors when users upgrade their clusters to a version that contains API changes.
1 parent 185b9f3 commit 2109a4f

6 files changed

Lines changed: 535 additions & 2 deletions

File tree

internal/provider/resource_vpc_firewall_rules.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ import (
2929

3030
// Ensure the implementation satisfies the expected interfaces.
3131
var (
32-
_ resource.Resource = (*vpcFirewallRulesResource)(nil)
33-
_ resource.ResourceWithConfigure = (*vpcFirewallRulesResource)(nil)
32+
_ resource.Resource = (*vpcFirewallRulesResource)(nil)
33+
_ resource.ResourceWithConfigure = (*vpcFirewallRulesResource)(nil)
34+
_ resource.ResourceWithUpgradeState = (*vpcFirewallRulesResource)(nil)
3435
)
3536

3637
// NewVPCFirewallRulesResource is a helper function to simplify the provider implementation.
@@ -112,10 +113,41 @@ func (r *vpcFirewallRulesResource) ImportState(ctx context.Context, req resource
112113
resource.ImportStatePassthroughID(ctx, path.Root("vpc_id"), req, resp)
113114
}
114115

116+
// UpgradeState upgrades the Terraform state for the oxide_vpc_firewall_rules
117+
// resource from a previous schema version to the current version.
118+
//
119+
// Schema upgrades are not expected to be applied sequentially, since users are
120+
// allowed to jump to whatever new version they choose. When adding a new
121+
// version, you must ensure that each of the existing StateUpgrader functios
122+
// are also updated to handle the new schema.
123+
func (r *vpcFirewallRulesResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
124+
return map[int64]resource.StateUpgrader{
125+
0: {
126+
PriorSchema: r.schemaV0(ctx),
127+
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
128+
var v0State vpcFirewallRulesResourceModelV0
129+
resp.Diagnostics.Append(req.State.Get(ctx, &v0State)...)
130+
if resp.Diagnostics.HasError() {
131+
return
132+
}
133+
134+
newState, diags := v0State.upgrade(ctx)
135+
resp.Diagnostics.Append(diags...)
136+
if resp.Diagnostics.HasError() {
137+
return
138+
}
139+
140+
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
141+
},
142+
},
143+
}
144+
}
145+
115146
// Schema defines the schema for the resource.
116147
func (r *vpcFirewallRulesResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
117148
// TODO: Make sure users can define a single block per VPC ID, not many, is this even possible?
118149
resp.Schema = schema.Schema{
150+
Version: 1,
119151
Attributes: map[string]schema.Attribute{
120152
"vpc_id": schema.StringAttribute{
121153
Required: true,

internal/provider/resource_vpc_firewall_rules_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ package provider
77
import (
88
"context"
99
"fmt"
10+
"net/http"
11+
"net/http/httptest"
12+
"path/filepath"
13+
"sync/atomic"
1014
"testing"
1115
"time"
1216

@@ -611,3 +615,119 @@ func testAccFirewallRulesDestroy(s *terraform.State) error {
611615

612616
return nil
613617
}
618+
619+
func TestFirewallRules_r15_r16_upgrade(t *testing.T) {
620+
var version atomic.Int32
621+
version.Store(15)
622+
623+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
624+
var respFile string
625+
switch version.Load() {
626+
case 15:
627+
respFile = "r15_get_vpc_firewall_rules.json"
628+
case 16:
629+
respFile = "r16_get_vpc_firewall_rules.json"
630+
}
631+
632+
respPath := filepath.Join("test-fixtures", "resource_vpc_firewall_rules", respFile)
633+
f, err := testFixtures.ReadFile(respPath)
634+
if err != nil {
635+
t.Errorf("failed to read file %q: %v", respPath, err)
636+
http.Error(w, "failed to read file", http.StatusInternalServerError)
637+
return
638+
}
639+
640+
w.WriteHeader(http.StatusOK)
641+
_, _ = w.Write(f)
642+
}))
643+
t.Cleanup(func() {
644+
ts.Close()
645+
})
646+
647+
resource.Test(t, resource.TestCase{
648+
IsUnitTest: true,
649+
Steps: []resource.TestStep{
650+
{
651+
ExternalProviders: map[string]resource.ExternalProvider{
652+
"oxide": {
653+
Source: "oxidecomputer/oxide",
654+
VersionConstraint: "0.12.0",
655+
},
656+
},
657+
//lintignore:AT004
658+
// Configuration must reach local test server.
659+
Config: fmt.Sprintf(`
660+
provider "oxide" {
661+
host = "%s"
662+
token = "fake"
663+
}
664+
665+
resource "oxide_vpc_firewall_rules" "test" {
666+
vpc_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
667+
rules = [
668+
{
669+
action = "allow"
670+
name = "allow-ssh"
671+
description = "SSH rule."
672+
direction = "inbound"
673+
priority = 65535
674+
status = "enabled"
675+
filters = {
676+
ports = ["22"]
677+
protocols = ["TCP"]
678+
}
679+
targets = [
680+
{
681+
type = "subnet"
682+
value = "default"
683+
}
684+
]
685+
}
686+
]
687+
}
688+
`, ts.URL),
689+
},
690+
{
691+
ExternalProviders: map[string]resource.ExternalProvider{},
692+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(),
693+
PreConfig: func() {
694+
version.Store(16)
695+
},
696+
//lintignore:AT004
697+
// Configuration must reach local test server.
698+
Config: fmt.Sprintf(`
699+
provider "oxide" {
700+
host = "%s"
701+
token = "fake"
702+
}
703+
704+
resource "oxide_vpc_firewall_rules" "test" {
705+
vpc_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
706+
rules = [
707+
{
708+
action = "allow"
709+
name = "allow-ssh"
710+
description = "SSH rule."
711+
direction = "inbound"
712+
priority = 65535
713+
status = "enabled"
714+
filters = {
715+
ports = ["22"]
716+
protocols = [
717+
{ type : "tcp" }
718+
]
719+
}
720+
targets = [
721+
{
722+
type = "subnet"
723+
value = "default"
724+
}
725+
]
726+
}
727+
]
728+
}
729+
`, ts.URL),
730+
},
731+
},
732+
})
733+
}

0 commit comments

Comments
 (0)