Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 40630ee

Browse files
committed
Add controlPlaneLoadBalancer ACLs
1 parent a51d507 commit 40630ee

File tree

9 files changed

+290
-13
lines changed

9 files changed

+290
-13
lines changed

api/v1beta1/scalewaycluster_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ type LoadBalancerSpec struct {
110110
// +kubebuilder:validation:Format=ipv4
111111
// +optional
112112
IP *string `json:"ip,omitempty"`
113+
114+
// AllowedRanges allows to set a list of allowed IP ranges that can access
115+
// the cluster through the load balancer. When unset, all IP ranges are allowed.
116+
// To allow the cluster to work properly, public IPs of nodes and Public
117+
// Gateways will automatically be allowed. However, if this field is set,
118+
// you MUST manually allow IPs of the nodes of your management cluster.
119+
// +optional
120+
AllowedRanges []string `json:"allowedRanges,omitempty"`
113121
}
114122

115123
// ScalewayClusterStatus defines the observed state of ScalewayCluster

api/v1beta1/scalewaycluster_webhook.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,20 @@ func (r *ScalewayCluster) enforceImmutability(old *ScalewayCluster) error {
226226
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "network"), r.Spec.Network, "field is immutable"))
227227
}
228228

229-
// TODO: allow updating load balancer type when it's implemented.
230-
if !reflect.DeepEqual(r.Spec.ControlPlaneLoadBalancer, old.Spec.ControlPlaneLoadBalancer) {
231-
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer"), r.Spec.ControlPlaneLoadBalancer, "field is immutable"))
229+
if old.Spec.ControlPlaneLoadBalancer == nil {
230+
old.Spec.ControlPlaneLoadBalancer = &LoadBalancerSpec{}
231+
}
232+
233+
if r.Spec.ControlPlaneLoadBalancer == nil {
234+
r.Spec.ControlPlaneLoadBalancer = &LoadBalancerSpec{}
235+
}
236+
237+
if !reflect.DeepEqual(old.Spec.ControlPlaneLoadBalancer.Zone, r.Spec.ControlPlaneLoadBalancer.Zone) {
238+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "zone"), r.Spec.ControlPlaneLoadBalancer.Zone, "field is immutable"))
239+
}
240+
241+
if !reflect.DeepEqual(old.Spec.ControlPlaneLoadBalancer.IP, r.Spec.ControlPlaneLoadBalancer.IP) {
242+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "ip"), r.Spec.ControlPlaneLoadBalancer.IP, "field is immutable"))
232243
}
233244

234245
if allErrs == nil {

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/infrastructure.cluster.x-k8s.io_scalewayclusters.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ spec:
5252
controlPlaneLoadBalancer:
5353
description: ControlPlaneLoadBalancer contains loadbalancer options.
5454
properties:
55+
allowedRanges:
56+
description: AllowedRanges allows to set a list of allowed IP
57+
ranges that can access the cluster through the load balancer.
58+
When unset, all IP ranges are allowed. To allow the cluster
59+
to work properly, public IPs of nodes and Public Gateways will
60+
automatically be allowed. However, if this field is set, you
61+
MUST manually allow IPs of the nodes of your management cluster.
62+
items:
63+
type: string
64+
type: array
5565
ip:
5666
description: IP to use when creating a loadbalancer.
5767
format: ipv4

config/crd/bases/infrastructure.cluster.x-k8s.io_scalewayclustertemplates.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ spec:
6868
description: ControlPlaneLoadBalancer contains loadbalancer
6969
options.
7070
properties:
71+
allowedRanges:
72+
description: AllowedRanges allows to set a list of allowed
73+
IP ranges that can access the cluster through the load
74+
balancer. When unset, all IP ranges are allowed. To
75+
allow the cluster to work properly, public IPs of nodes
76+
and Public Gateways will automatically be allowed. However,
77+
if this field is set, you MUST manually allow IPs of
78+
the nodes of your management cluster.
79+
items:
80+
type: string
81+
type: array
7182
ip:
7283
description: IP to use when creating a loadbalancer.
7384
format: ipv4

pkg/service/scaleway/client/loadbalancer.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,30 @@ func (c *Client) FindLoadBalancerBackendByNames(ctx context.Context, zone scw.Zo
5050
return nil, ErrNoItemFound
5151
}
5252

53+
func (c *Client) FindLoadBalancerFrontendByNames(ctx context.Context, zone scw.Zone, lbName, frontendName string) (*lb.Frontend, error) {
54+
loadbalancer, err := c.FindLoadBalancerByName(ctx, zone, lbName)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
frontends, err := c.LoadBalancer.ListFrontends(&lb.ZonedAPIListFrontendsRequest{
60+
Zone: zone,
61+
LBID: loadbalancer.ID,
62+
Name: scw.StringPtr(frontendName),
63+
}, scw.WithAllPages(), scw.WithContext(ctx))
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
for _, frontend := range frontends.Frontends {
69+
if frontend.Name == frontendName {
70+
return frontend, nil
71+
}
72+
}
73+
74+
return nil, ErrNoItemFound
75+
}
76+
5377
func (c *Client) FindLoadBalancerIP(ctx context.Context, zone scw.Zone, ip string) (*lb.IP, error) {
5478
ips, err := c.LoadBalancer.ListIPs(&lb.ZonedAPIListIPsRequest{
5579
Zone: zone,
@@ -68,3 +92,22 @@ func (c *Client) FindLoadBalancerIP(ctx context.Context, zone scw.Zone, ip strin
6892

6993
return nil, ErrNoItemFound
7094
}
95+
96+
func (c *Client) FindLoadBalancerACLByName(ctx context.Context, zone scw.Zone, frontendID, name string) (*lb.ACL, error) {
97+
acls, err := c.LoadBalancer.ListACLs(&lb.ZonedAPIListACLsRequest{
98+
Name: scw.StringPtr(name),
99+
Zone: zone,
100+
FrontendID: frontendID,
101+
}, scw.WithContext(ctx), scw.WithAllPages())
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to list ACLs: %w", err)
104+
}
105+
106+
for _, acl := range acls.ACLs {
107+
if acl.Name == name {
108+
return acl, nil
109+
}
110+
}
111+
112+
return nil, ErrNoItemFound
113+
}

pkg/service/scaleway/client/vpcgw.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,17 @@ func (c *Client) FindGatewayIPByTags(ctx context.Context, zone scw.Zone, tags []
6666

6767
return ips.IPs[0], nil
6868
}
69+
70+
func (c *Client) FindGatewaysByPrivateNetworkID(ctx context.Context, zones []scw.Zone, privateNetworkID string) ([]*vpcgw.Gateway, error) {
71+
resp, err := c.PublicGateway.ListGateways(&vpcgw.ListGatewaysRequest{
72+
PrivateNetworkID: scw.StringPtr(privateNetworkID),
73+
Zone: scw.ZoneFrPar1,
74+
}, scw.WithContext(ctx), scw.WithAllPages(), scw.WithZones(zones...))
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to list public gateways by private network ID: %w", err)
77+
}
78+
79+
gws := make([]*vpcgw.Gateway, 0, len(resp.Gateways))
80+
gws = append(gws, resp.Gateways...)
81+
return gws, nil
82+
}

pkg/service/scaleway/instance/instance.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/scaleway/scaleway-sdk-go/api/lb/v1"
1616
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v1"
1717
"github.com/scaleway/scaleway-sdk-go/scw"
18-
"k8s.io/utils/strings/slices"
18+
"golang.org/x/exp/slices"
1919
"sigs.k8s.io/cluster-api/api/v1beta1"
2020
"sigs.k8s.io/cluster-api/util"
2121
)
@@ -271,6 +271,66 @@ func (s *Service) ensureServerStarted(ctx context.Context, server *instance.Serv
271271
return nil
272272
}
273273

274+
func (s *Service) ensureLoadBalancerACL(ctx context.Context, publicIP *string) error {
275+
frontend, err := s.ScalewayClient.FindLoadBalancerFrontendByNames(
276+
ctx,
277+
s.Cluster.LoadBalancerZone(),
278+
s.Cluster.Name(),
279+
loadbalancer.ControlPlaneFrontendName,
280+
)
281+
if err != nil {
282+
return fmt.Errorf("failed to find load balancer frontend: %w", err)
283+
}
284+
285+
acl, err := s.ScalewayClient.FindLoadBalancerACLByName(ctx, s.LoadBalancerZone(), frontend.ID, s.Name())
286+
if err != nil && !errors.Is(err, client.ErrNoItemFound) {
287+
return fmt.Errorf("failed to find load balancer ACL: %w", err)
288+
}
289+
290+
if publicIP == nil {
291+
if acl != nil {
292+
if err := s.ScalewayClient.LoadBalancer.DeleteACL(&lb.ZonedAPIDeleteACLRequest{
293+
Zone: s.LoadBalancerZone(),
294+
ACLID: acl.ID,
295+
}, scw.WithContext(ctx)); err != nil {
296+
return fmt.Errorf("failed to delete load balancer ACL: %w", err)
297+
}
298+
}
299+
300+
return nil
301+
}
302+
303+
match := scw.StringSlicePtr([]string{*publicIP})
304+
305+
if acl == nil {
306+
_, err := s.ScalewayClient.LoadBalancer.CreateACL(&lb.ZonedAPICreateACLRequest{
307+
Zone: s.LoadBalancerZone(),
308+
FrontendID: frontend.ID,
309+
Name: s.Name(),
310+
Action: &lb.ACLAction{Type: lb.ACLActionTypeAllow},
311+
Match: &lb.ACLMatch{IPSubnet: match},
312+
Index: 3,
313+
}, scw.WithContext(ctx))
314+
315+
return err
316+
}
317+
318+
if acl.Match == nil || !slices.Equal(match, acl.Match.IPSubnet) {
319+
_, err := s.ScalewayClient.LoadBalancer.UpdateACL(&lb.ZonedAPIUpdateACLRequest{
320+
Zone: s.LoadBalancerZone(),
321+
ACLID: acl.ID,
322+
Name: s.Name(),
323+
Action: &lb.ACLAction{Type: lb.ACLActionTypeAllow},
324+
Match: &lb.ACLMatch{IPSubnet: match},
325+
Index: 3,
326+
}, scw.WithContext(ctx))
327+
328+
return err
329+
}
330+
331+
return nil
332+
}
333+
274334
func (s *Service) ensureControlPlaneLoadBalancer(ctx context.Context, server *instance.Server, pnic *instance.PrivateNIC, deletion bool) (*machineIPs, error) {
275335
if !util.IsControlPlaneMachine(s.Machine.Machine) {
276336
return nil, nil
@@ -336,6 +396,10 @@ func (s *Service) Reconcile(ctx context.Context) error {
336396
return err
337397
}
338398

399+
if err := s.ensureLoadBalancerACL(ctx, machineIPs.External); err != nil {
400+
return err
401+
}
402+
339403
if err := s.ensureCloudInit(ctx, server, machineIPs); err != nil {
340404
return err
341405
}
@@ -375,6 +439,11 @@ func (s *Service) Delete(ctx context.Context) error {
375439
return err
376440
}
377441

442+
// Set publicIP to nil to force deletion.
443+
if err := s.ensureLoadBalancerACL(ctx, nil); err != nil && !errors.Is(err, client.ErrNoItemFound) {
444+
return err
445+
}
446+
378447
// Remove this control-plane from the loadbalancer.
379448
if util.IsControlPlaneMachine(s.Machine.Machine) {
380449
var pnic *instance.PrivateNIC

0 commit comments

Comments
 (0)