Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions scaleway/acl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package scaleway

import (
"context"
"fmt"
"net"
"slices"
"strings"

scwlb "github.com/scaleway/scaleway-sdk-go/api/lb/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
v1 "k8s.io/api/core/v1"
"k8s.io/klog/v2"
)

const MaxEntriesPerACL = 60

func (l *loadbalancers) reconcileACLs(ctx context.Context, loadbalancer *scwlb.LB, frontend *scwlb.Frontend, service *v1.Service, nodes []*v1.Node) error {
// List ACLs for the frontend
aclName := makeACLPrefix(frontend)
aclsResp, err := l.api.ListACLs(&scwlb.ZonedAPIListACLsRequest{
Zone: loadbalancer.Zone,
FrontendID: frontend.ID,
Name: &aclName,
}, scw.WithAllPages(), scw.WithContext(ctx))
if err != nil {
return fmt.Errorf("failed to list ACLs for frontend: %s port: %d loadbalancer: %s err: %v", frontend.ID, frontend.InboundPort, loadbalancer.ID, err)
}

svcAcls := makeACLSpecs(service, nodes, frontend)
if !aclsEquals(aclsResp.ACLs, svcAcls) {
klog.Infof("set ACLs for frontend: %s port: %d loadbalancer: %s", frontend.ID, frontend.InboundPort, loadbalancer.ID)
if _, err := l.api.SetACLs(&scwlb.ZonedAPISetACLsRequest{
Zone: loadbalancer.Zone,
FrontendID: frontend.ID,
ACLs: svcAcls,
}, scw.WithContext(ctx)); err != nil {
return fmt.Errorf("failed setting ACLs for frontend: %s port: %d loadbalancer: %s err: %v", frontend.ID, frontend.InboundPort, loadbalancer.ID, err)
}
}

return nil
}

// makeACLPrefix returns the ACL prefix for rules
func makeACLPrefix(frontend *scwlb.Frontend) string {
if frontend == nil {
return "lb-source-range"
}
return fmt.Sprintf("%s-lb-source-range", frontend.ID)
}

// makeACLSpecs converts a service frontend definition to acl specifications
func makeACLSpecs(service *v1.Service, nodes []*v1.Node, frontend *scwlb.Frontend) []*scwlb.ACLSpec {
if len(service.Spec.LoadBalancerSourceRanges) == 0 {
return []*scwlb.ACLSpec{}
}

sourceRanges := make([]string, 0, len(service.Spec.LoadBalancerSourceRanges))
for _, sourceRange := range service.Spec.LoadBalancerSourceRanges {
if _, _, err := net.ParseCIDR(sourceRange); err != nil {
klog.Warningf("ignoring invalid CIDR %s in LoadBalancerSourceRanges for service %s/%s: %v", sourceRange, service.Namespace, service.Name, err)
continue
}

if strings.Contains(sourceRange, ":") {
sourceRange = strings.TrimSuffix(sourceRange, "/128")
} else {
sourceRange = strings.TrimSuffix(sourceRange, "/32")
}

sourceRanges = append(sourceRanges, sourceRange)
}

aclPrefix := makeACLPrefix(frontend)
whitelist := extractNodesInternalIps(nodes)
whitelist = append(whitelist, extractNodesExternalIps(nodes)...)
whitelist = append(whitelist, sourceRanges...)

slices.Sort(whitelist)

subnetsChunks := chunkArray(whitelist, MaxEntriesPerACL)
acls := make([]*scwlb.ACLSpec, len(subnetsChunks)+1)

for idx, subnets := range subnetsChunks {
acls[idx] = &scwlb.ACLSpec{
Name: fmt.Sprintf("%s-%d", aclPrefix, idx),
Action: &scwlb.ACLAction{
Type: scwlb.ACLActionTypeAllow,
},
Index: int32(idx),
Match: &scwlb.ACLMatch{
IPSubnet: scw.StringSlicePtr(subnets),
},
}
}

acls[len(acls)-1] = &scwlb.ACLSpec{
Name: fmt.Sprintf("%s-end", aclPrefix),
Action: &scwlb.ACLAction{
Type: scwlb.ACLActionTypeDeny,
},
Index: int32(len(acls) - 1),
Match: &scwlb.ACLMatch{
IPSubnet: scw.StringSlicePtr([]string{"0.0.0.0/0", "::/0"}),
},
}

return acls
}

// aclsEquals returns true if both acl lists are equal
func aclsEquals(got []*scwlb.ACL, want []*scwlb.ACLSpec) bool {
if len(got) != len(want) {
return false
}

slices.SortStableFunc(got, func(a, b *scwlb.ACL) int { return int(a.Index - b.Index) })
slices.SortStableFunc(want, func(a, b *scwlb.ACLSpec) int { return int(a.Index - b.Index) })
for idx := range want {
if want[idx].Name != got[idx].Name {
return false
}
if want[idx].Index != got[idx].Index {
return false
}
if (want[idx].Action == nil) != (got[idx].Action == nil) {
return false
}
if want[idx].Action != nil && want[idx].Action.Type != got[idx].Action.Type {
return false
}
if (want[idx].Match == nil) != (got[idx].Match == nil) {
return false
}
if want[idx].Match != nil && !stringPtrArrayEqual(want[idx].Match.IPSubnet, got[idx].Match.IPSubnet) {
return false
}
if want[idx].Match != nil && want[idx].Match.Invert != got[idx].Match.Invert {
return false
}
}

return true
}
141 changes: 2 additions & 139 deletions scaleway/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"maps"
"net"
"os"
"reflect"
"slices"
Expand All @@ -40,8 +39,6 @@ import (
"github.com/scaleway/scaleway-sdk-go/scw"
)

const MaxEntriesPerACL = 60

type loadbalancers struct {
api LoadBalancerAPI
ipam IPAMAPI
Expand Down Expand Up @@ -741,40 +738,8 @@ func (l *loadbalancers) updateLoadBalancer(ctx context.Context, loadbalancer *sc
frontend = f
}

// List ACLs for the frontend
aclName := makeACLPrefix(frontend)
aclsResp, err := l.api.ListACLs(&scwlb.ZonedAPIListACLsRequest{
Zone: loadbalancer.Zone,
FrontendID: frontend.ID,
Name: &aclName,
}, scw.WithAllPages(), scw.WithContext(ctx))
if err != nil {
return fmt.Errorf("failed to list ACLs for frontend: %s port: %d loadbalancer: %s err: %v", frontend.ID, frontend.InboundPort, loadbalancer.ID, err)
}

svcAcls := makeACLSpecs(service, nodes, frontend)
if !aclsEquals(aclsResp.ACLs, svcAcls) {
// Replace ACLs
klog.Infof("remove all ACLs from frontend: %s port: %d loadbalancer: %s", frontend.ID, frontend.InboundPort, loadbalancer.ID)
for _, acl := range aclsResp.ACLs {
if err := l.api.DeleteACL(&scwlb.ZonedAPIDeleteACLRequest{
Zone: loadbalancer.Zone,
ACLID: acl.ID,
}); err != nil {
return fmt.Errorf("failed removing ACL %s from frontend: %s port: %d loadbalancer: %s err: %v", acl.Name, frontend.ID, frontend.InboundPort, loadbalancer.ID, err)
}
}

klog.Infof("create all ACLs for frontend: %s port: %d loadbalancer: %s", frontend.ID, frontend.InboundPort, loadbalancer.ID)
for _, acl := range svcAcls {
if _, err := l.api.SetACLs(&scwlb.ZonedAPISetACLsRequest{
Zone: loadbalancer.Zone,
FrontendID: frontend.ID,
ACLs: svcAcls,
}); err != nil {
return fmt.Errorf("failed creating ACL %s for frontend: %s port: %d loadbalancer: %s err: %v", acl.Name, frontend.ID, frontend.InboundPort, loadbalancer.ID, err)
}
}
if err := l.reconcileACLs(ctx, loadbalancer, frontend, service, nodes); err != nil {
return err
}
}

Expand Down Expand Up @@ -1692,41 +1657,6 @@ func compareBackends(got []*scwlb.Backend, want map[int32]*scwlb.Backend, filter
}
}

// aclsEquals returns true if both acl lists are equal
func aclsEquals(got []*scwlb.ACL, want []*scwlb.ACLSpec) bool {
if len(got) != len(want) {
return false
}

slices.SortStableFunc(got, func(a, b *scwlb.ACL) int { return int(a.Index - b.Index) })
slices.SortStableFunc(want, func(a, b *scwlb.ACLSpec) int { return int(a.Index - b.Index) })
for idx := range want {
if want[idx].Name != got[idx].Name {
return false
}
if want[idx].Index != got[idx].Index {
return false
}
if (want[idx].Action == nil) != (got[idx].Action == nil) {
return false
}
if want[idx].Action != nil && want[idx].Action.Type != got[idx].Action.Type {
return false
}
if (want[idx].Match == nil) != (got[idx].Match == nil) {
return false
}
if want[idx].Match != nil && !stringPtrArrayEqual(want[idx].Match.IPSubnet, got[idx].Match.IPSubnet) {
return false
}
if want[idx].Match != nil && want[idx].Match.Invert != got[idx].Match.Invert {
return false
}
}

return true
}

// createBackend creates a backend on the load balancer
func (l *loadbalancers) createBackend(ctx context.Context, loadbalancer *scwlb.LB, backend *scwlb.Backend) (*scwlb.Backend, error) {
b, err := l.api.CreateBackend(&scwlb.ZonedAPICreateBackendRequest{
Expand Down Expand Up @@ -1945,73 +1875,6 @@ func chunkArray(array []string, maxChunkSize int) [][]string {
return result
}

// makeACLPrefix returns the ACL prefix for rules
func makeACLPrefix(frontend *scwlb.Frontend) string {
if frontend == nil {
return "lb-source-range"
}
return fmt.Sprintf("%s-lb-source-range", frontend.ID)
}

// makeACLSpecs converts a service frontend definition to acl specifications
func makeACLSpecs(service *v1.Service, nodes []*v1.Node, frontend *scwlb.Frontend) []*scwlb.ACLSpec {
if len(service.Spec.LoadBalancerSourceRanges) == 0 {
return []*scwlb.ACLSpec{}
}

sourceRanges := make([]string, 0, len(service.Spec.LoadBalancerSourceRanges))
for _, sourceRange := range service.Spec.LoadBalancerSourceRanges {
if _, _, err := net.ParseCIDR(sourceRange); err != nil {
klog.Warningf("ignoring invalid CIDR %s in LoadBalancerSourceRanges for service %s/%s: %v", sourceRange, service.Namespace, service.Name, err)
continue
}

if strings.Contains(sourceRange, ":") {
sourceRange = strings.TrimSuffix(sourceRange, "/128")
} else {
sourceRange = strings.TrimSuffix(sourceRange, "/32")
}

sourceRanges = append(sourceRanges, sourceRange)
}

aclPrefix := makeACLPrefix(frontend)
whitelist := extractNodesInternalIps(nodes)
whitelist = append(whitelist, extractNodesExternalIps(nodes)...)
whitelist = append(whitelist, sourceRanges...)

slices.Sort(whitelist)

subnetsChunks := chunkArray(whitelist, MaxEntriesPerACL)
acls := make([]*scwlb.ACLSpec, len(subnetsChunks)+1)

for idx, subnets := range subnetsChunks {
acls[idx] = &scwlb.ACLSpec{
Name: fmt.Sprintf("%s-%d", aclPrefix, idx),
Action: &scwlb.ACLAction{
Type: scwlb.ACLActionTypeAllow,
},
Index: int32(idx),
Match: &scwlb.ACLMatch{
IPSubnet: scw.StringSlicePtr(subnets),
},
}
}

acls[len(acls)-1] = &scwlb.ACLSpec{
Name: fmt.Sprintf("%s-end", aclPrefix),
Action: &scwlb.ACLAction{
Type: scwlb.ACLActionTypeDeny,
},
Index: int32(len(acls) - 1),
Match: &scwlb.ACLMatch{
IPSubnet: scw.StringSlicePtr([]string{"0.0.0.0/0", "::/0"}),
},
}

return acls
}

func ptrInt32ToString(i *int32) string {
if i == nil {
return "<nil>"
Expand Down