Skip to content

Commit d80cd59

Browse files
authored
Upgrade Public Gateway when type is updated and it's possible to (#15)
1 parent dfb2344 commit d80cd59

File tree

6 files changed

+189
-17
lines changed

6 files changed

+189
-17
lines changed

docs/scalewaycluster.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,9 @@ If not set, a new IP will be created.
259259
> 🚫📶 Updating existing Public Gateways can lead to a loss of network on the nodes, be
260260
> very careful when updating this field.
261261
>
262-
> 🚮 Updating a Public Gateway will lead to its recreation, which will make its private IP change.
262+
> 🚮 Updating a Public Gateway will lead to its re-creation, which will make its private IP change.
263+
> The only change that won't lead to a re-creation of the Public Gateway is a type upgrade
264+
> (e.g. VPC-GW-S to VPC-GW-M). Downgrading a Public Gateway is only possible through a re-creation.
263265
>
264266
> ⏳ Because the default routes are advertised via DHCP, the DHCP leases of the nodes must
265267
> be renewed for changes to be propagated (~24 hours). You can reboot the nodes or

internal/service/scaleway/client/vpcgw.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,41 @@ func (c *Client) CreateGatewayNetwork(ctx context.Context, zone scw.Zone, gatewa
111111

112112
return nil
113113
}
114+
115+
func (c *Client) ListGatewayTypes(ctx context.Context, zone scw.Zone) ([]string, error) {
116+
if err := c.validateZone(c.vpcgw, zone); err != nil {
117+
return nil, err
118+
}
119+
120+
resp, err := c.vpcgw.ListGatewayTypes(&vpcgw.ListGatewayTypesRequest{
121+
Zone: zone,
122+
}, scw.WithContext(ctx))
123+
if err != nil {
124+
return nil, newCallError("ListGatewayTypes", err)
125+
}
126+
127+
// We assume the API returns the gateway types in the correct order (S -> M -> L, etc.).
128+
types := make([]string, 0, len(resp.Types))
129+
for _, t := range resp.Types {
130+
types = append(types, t.Name)
131+
}
132+
133+
return types, nil
134+
}
135+
136+
func (c *Client) UpgradeGateway(ctx context.Context, zone scw.Zone, gatewayID, newType string) (*vpcgw.Gateway, error) {
137+
if err := c.validateZone(c.vpcgw, zone); err != nil {
138+
return nil, err
139+
}
140+
141+
gateway, err := c.vpcgw.UpgradeGateway(&vpcgw.UpgradeGatewayRequest{
142+
Zone: zone,
143+
GatewayID: gatewayID,
144+
Type: &newType,
145+
}, scw.WithContext(ctx))
146+
if err != nil {
147+
return nil, newCallError("UpgradeGateway", err)
148+
}
149+
150+
return gateway, nil
151+
}

internal/service/scaleway/common/resource_ensurer.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type ResourceReconciler[D, R any] interface {
3131

3232
// ShouldKeepResource returns true if a resource should be kept because it
3333
// matches the desired specs.
34-
ShouldKeepResource(resource R, desired D) bool
34+
ShouldKeepResource(ctx context.Context, resource R, desired D) (bool, error)
3535
}
3636

3737
// ResourceEnsurer is a utility that ensures a list of desired resources
@@ -96,11 +96,16 @@ func (e *ResourceEnsurer[D, R]) ensureExistingResources(
9696
continue
9797
}
9898

99-
if !e.ShouldKeepResource(resource, desiredResource) {
100-
continue
99+
// Writes to the keep variable outside the scope of this for-loop.
100+
keep, err = e.ShouldKeepResource(ctx, resource, desiredResource)
101+
if err != nil {
102+
return nil, err
101103
}
102104

103-
keep = true
105+
// Continue looping to the next resource until we find one to keep.
106+
if !keep {
107+
continue
108+
}
104109

105110
resource, err = e.UpdateResource(ctx, resource, desiredResource)
106111
if err != nil {

internal/service/scaleway/lb/lb.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,23 +289,24 @@ func (d *desiredResourceListManager) GetDesiredZone(desired infrav1.LoadBalancer
289289
}
290290

291291
func (d *desiredResourceListManager) ShouldKeepResource(
292+
_ context.Context,
292293
resource *lb.LB,
293294
desired infrav1.LoadBalancerSpec,
294-
) bool {
295+
) (bool, error) {
295296
// If LB does not have an IP, remove it and recreate it.
296297
if len(resource.IP) == 0 {
297-
return false
298+
return false, nil
298299
}
299300

300301
if desired.IP == nil && !slices.Contains(resource.Tags, capsManagedIPTag) {
301-
return false
302+
return false, nil
302303
}
303304

304305
if desired.IP != nil && resource.IP[0].IPAddress != *desired.IP {
305-
return false
306+
return false, nil
306307
}
307308

308-
return true
309+
return true, nil
309310
}
310311

311312
func (d *desiredResourceListManager) GetDesiredResourceName(i int) string {

internal/service/scaleway/vpcgw/vpcgw.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func (s *Service) ensureGateways(ctx context.Context, delete bool) ([]*vpcgw.Gat
4141
}
4242

4343
drle := &common.ResourceEnsurer[infrav1.PublicGatewaySpec, *vpcgw.Gateway]{
44-
ResourceReconciler: &desiredResourceListManager{s.Cluster},
44+
ResourceReconciler: &desiredResourceListManager{s.Cluster, make(map[scw.Zone][]string)},
4545
}
4646
return drle.Do(ctx, desired)
4747
}
@@ -106,6 +106,8 @@ func (s *Service) Delete(ctx context.Context) error {
106106

107107
type desiredResourceListManager struct {
108108
*scope.Cluster
109+
110+
gatewayTypesCache map[scw.Zone][]string
109111
}
110112

111113
func (d *desiredResourceListManager) ListResources(ctx context.Context) ([]*vpcgw.Gateway, error) {
@@ -132,6 +134,18 @@ func (d *desiredResourceListManager) UpdateResource(
132134
resource *vpcgw.Gateway,
133135
desired infrav1.PublicGatewaySpec,
134136
) (*vpcgw.Gateway, error) {
137+
if desired.Type != nil && *desired.Type != resource.Type {
138+
canUpgradeType, err := d.canUpgradeType(ctx, resource.Zone, resource.Type, *desired.Type)
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
if canUpgradeType {
144+
logf.FromContext(ctx).Info("Upgrading Gateway", "gatewayName", resource.Name, "zone", resource.Zone)
145+
return d.ScalewayClient.UpgradeGateway(ctx, resource.Zone, resource.ID, *desired.Type)
146+
}
147+
}
148+
135149
return resource, nil
136150
}
137151

@@ -148,27 +162,35 @@ func (d *desiredResourceListManager) GetDesiredZone(desired infrav1.PublicGatewa
148162
}
149163

150164
func (d *desiredResourceListManager) ShouldKeepResource(
165+
ctx context.Context,
151166
resource *vpcgw.Gateway,
152167
desired infrav1.PublicGatewaySpec,
153-
) bool {
168+
) (bool, error) {
154169
// Gateway has no IPv4, remove it and recreate it.
155170
if resource.IPv4 == nil {
156-
return false
171+
return false, nil
157172
}
158173

159174
if desired.Type != nil && *desired.Type != resource.Type {
160-
return false
175+
canUpgradeType, err := d.canUpgradeType(ctx, resource.Zone, resource.Type, *desired.Type)
176+
if err != nil {
177+
return false, err
178+
}
179+
180+
if !canUpgradeType {
181+
return false, nil
182+
}
161183
}
162184

163185
if desired.IP == nil && !slices.Contains(resource.Tags, capsManagedIPTag) {
164-
return false
186+
return false, nil
165187
}
166188

167189
if desired.IP != nil && resource.IPv4.Address.String() != *desired.IP {
168-
return false
190+
return false, nil
169191
}
170192

171-
return true
193+
return true, nil
172194
}
173195

174196
func (d *desiredResourceListManager) GetDesiredResourceName(i int) string {
@@ -213,3 +235,25 @@ func (d *desiredResourceListManager) CreateResource(
213235

214236
return gateway, nil
215237
}
238+
239+
func (d *desiredResourceListManager) canUpgradeType(ctx context.Context, zone scw.Zone, current, desired string) (bool, error) {
240+
types, ok := d.gatewayTypesCache[zone]
241+
if !ok {
242+
var err error
243+
types, err = d.ScalewayClient.ListGatewayTypes(ctx, zone)
244+
if err != nil {
245+
return false, err
246+
}
247+
248+
d.gatewayTypesCache[zone] = types
249+
}
250+
251+
return canUpgradeTypes(types, current, desired), nil
252+
}
253+
254+
func canUpgradeTypes(types []string, current, desired string) bool {
255+
desiredIndex := slices.Index(types, desired)
256+
currentIndex := slices.Index(types, current)
257+
258+
return currentIndex != -1 && desiredIndex > currentIndex
259+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package vpcgw
2+
3+
import "testing"
4+
5+
func Test_canUpgradeTypes(t *testing.T) {
6+
t.Parallel()
7+
8+
type args struct {
9+
types []string
10+
current string
11+
desired string
12+
}
13+
tests := []struct {
14+
name string
15+
args args
16+
want bool
17+
}{
18+
{
19+
name: "upgrade from VPC-GW-S to VPC-GW-XL",
20+
args: args{
21+
types: []string{"VPC-GW-S", "VPC-GW-M", "VPC-GW-L", "VPC-GW-XL"},
22+
current: "VPC-GW-S",
23+
desired: "VPC-GW-XL",
24+
},
25+
want: true,
26+
},
27+
{
28+
name: "upgrade from VPC-GW-S to VPC-GW-M",
29+
args: args{
30+
types: []string{"VPC-GW-S", "VPC-GW-M", "VPC-GW-L", "VPC-GW-XL"},
31+
current: "VPC-GW-S",
32+
desired: "VPC-GW-XL",
33+
},
34+
want: true,
35+
},
36+
{
37+
name: "current equals desired, not upgradable",
38+
args: args{
39+
types: []string{"VPC-GW-S", "VPC-GW-M", "VPC-GW-L", "VPC-GW-XL"},
40+
current: "VPC-GW-S",
41+
desired: "VPC-GW-S",
42+
},
43+
want: false,
44+
},
45+
{
46+
name: "unknown current type, not upgradable",
47+
args: args{
48+
types: []string{"VPC-GW-S", "VPC-GW-M", "VPC-GW-L", "VPC-GW-XL"},
49+
current: "UNKNOWN-S",
50+
desired: "VPC-GW-L",
51+
},
52+
want: false,
53+
},
54+
{
55+
name: "unknown current and desired type, not upgradable",
56+
args: args{
57+
types: []string{"VPC-GW-S", "VPC-GW-M", "VPC-GW-L", "VPC-GW-XL"},
58+
current: "UNKNOWN-S",
59+
desired: "UNKNOWN-M",
60+
},
61+
want: false,
62+
},
63+
{
64+
name: "unknown desired type, not upgradable",
65+
args: args{
66+
types: []string{"VPC-GW-S", "VPC-GW-M", "VPC-GW-L", "VPC-GW-XL"},
67+
current: "VPC-GW-S",
68+
desired: "UNKNOWN-M",
69+
},
70+
want: false,
71+
},
72+
}
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
t.Parallel()
76+
77+
if got := canUpgradeTypes(tt.args.types, tt.args.current, tt.args.desired); got != tt.want {
78+
t.Errorf("canUpgradeTypes() = %v, want %v", got, tt.want)
79+
}
80+
})
81+
}
82+
}

0 commit comments

Comments
 (0)