Skip to content

Commit deb6e49

Browse files
committed
feat(loadbalancers): add feature gate for pn selector label
Signed-off-by: Andreas Wachs <awa@corti.ai>
1 parent eba9221 commit deb6e49

4 files changed

Lines changed: 134 additions & 8 deletions

File tree

docs/loadbalancer-annotations.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,21 @@ The possible formats are:
247247

248248
### `service.beta.kubernetes.io/scw-loadbalancer-pn-names`
249249

250+
> **Feature gate:** This annotation requires the `SCW_ENABLE_LB_PN_NAME_SELECTOR` environment variable
251+
> to be set to `"true"` on the cloud controller manager. When the environment variable is not set or set
252+
> to any other value, this annotation is ignored.
253+
250254
This is the annotation to configure the Private Networks by name instead of ID.
251255
The private network names will be resolved to IDs at runtime. This is useful when
252256
you want to specify private networks without hardcoding their IDs, which can change
253257
when clusters are recreated.
254258

259+
When enabled, IPAM-based node IP resolution is also activated, providing precise IP
260+
lookup for nodes connected to the configured private networks.
261+
255262
**Priority order:**
256263
1. `service.beta.kubernetes.io/scw-loadbalancer-pn-ids` (highest priority)
257-
2. `service.beta.kubernetes.io/scw-loadbalancer-pn-names`
264+
2. `service.beta.kubernetes.io/scw-loadbalancer-pn-names` (requires `SCW_ENABLE_LB_PN_NAME_SELECTOR=true`)
258265
3. `PN_ID` environment variable (fallback)
259266

260267
If both `pn-ids` and `pn-names` are set, `pn-ids` takes precedence and `pn-names` is ignored.

scaleway/cloud.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ const (
5353
loadBalancerDefaultTypeEnv = "LB_DEFAULT_TYPE"
5454

5555
privateNetworkID = "PN_ID"
56+
57+
// enableLBPNNameSelectorEnv enables the selector-style annotation for LB to VPC/PN association.
58+
// When set to "true", the pn-names annotation and IPAM-based node IP resolution are enabled.
59+
// This feature is gated because it can cause issues on the Scaleway backend at scale.
60+
enableLBPNNameSelectorEnv = "SCW_ENABLE_LB_PN_NAME_SELECTOR"
5661
)
5762

5863
type cloud struct {

scaleway/loadbalancers.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ import (
4141

4242
const MaxEntriesPerACL = 60
4343

44+
// lbPNNameSelectorEnabled returns true if the PN name selector feature is enabled
45+
// via the SCW_ENABLE_LB_PN_NAME_SELECTOR environment variable.
46+
func lbPNNameSelectorEnabled() bool {
47+
return strings.EqualFold(os.Getenv(enableLBPNNameSelectorEnv), "true")
48+
}
49+
4450
type loadbalancers struct {
4551
api LoadBalancerAPI
4652
instance LBInstanceAPI
@@ -163,7 +169,7 @@ func (l *loadbalancers) EnsureLoadBalancer(ctx context.Context, clusterName stri
163169
return nil, fmt.Errorf("invalid value for annotation %s: expected boolean", serviceAnnotationLoadBalancerPrivate)
164170
}
165171

166-
if lbPrivate && l.pnID == "" && len(getPrivateNetworkIDs(service)) == 0 && len(getPrivateNetworkNames(service)) == 0 {
172+
if lbPrivate && l.pnID == "" && len(getPrivateNetworkIDs(service)) == 0 && (lbPNNameSelectorEnabled() && len(getPrivateNetworkNames(service)) == 0) {
167173
return nil, fmt.Errorf("scaleway-cloud-controller-manager cannot create private load balancers without a private network")
168174
}
169175

@@ -621,9 +627,13 @@ func (l *loadbalancers) updateLoadBalancer(ctx context.Context, loadbalancer *sc
621627
}
622628

623629
// Discover the project ID from the cluster nodes for accurate VPC/PN lookup
624-
nodeProjectID, err := l.getProjectIDFromNodes(nodes, region)
625-
if err != nil {
626-
klog.V(3).Infof("could not determine project ID from nodes: %v, falling back to default", err)
630+
// (only needed when the PN name selector feature is enabled)
631+
var nodeProjectID string
632+
if lbPNNameSelectorEnabled() {
633+
nodeProjectID, err = l.getProjectIDFromNodes(nodes, region)
634+
if err != nil {
635+
klog.V(3).Infof("could not determine project ID from nodes: %v, falling back to default", err)
636+
}
627637
}
628638

629639
configuredPNIDs, err := l.attachPrivateNetworks(loadbalancer, service, lbExternallyManaged, region, nodeProjectID)
@@ -632,7 +642,7 @@ func (l *loadbalancers) updateLoadBalancer(ctx context.Context, loadbalancer *sc
632642
}
633643

634644
var targetIPs []string
635-
if len(configuredPNIDs) > 0 {
645+
if lbPNNameSelectorEnabled() && len(configuredPNIDs) > 0 {
636646
// Use precise IPAM-based lookup to find node IPs on the configured private networks
637647
targetIPs, err = l.getNodeIPsForPrivateNetworks(nodes, configuredPNIDs, region)
638648
if err != nil {
@@ -868,8 +878,8 @@ func (l *loadbalancers) attachPrivateNetworks(loadbalancer *scwlb.LB, service *v
868878
for _, pnID := range explicitIDs {
869879
pnIDs[pnID] = false
870880
}
871-
} else {
872-
// Priority 2: Names annotation - resolve to IDs
881+
} else if lbPNNameSelectorEnabled() {
882+
// Priority 2: Names annotation - resolve to IDs (requires SCW_ENABLE_LB_PN_NAME_SELECTOR=true)
873883
pnNames := getPrivateNetworkNames(service)
874884
if len(pnNames) > 0 {
875885
resolvedIDs, err := l.resolvePrivateNetworkNames(pnNames, region, projectID)

scaleway/loadbalancers_pn_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,107 @@ import (
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
)
2929

30+
func TestLBPNNameSelectorGate(t *testing.T) {
31+
vpcAPI := &fakeVPCAPI{
32+
vpcs: []*scwvpc.VPC{
33+
{ID: "vpc-default", Name: "default", Region: scw.RegionFrPar, ProjectID: "proj-1"},
34+
},
35+
privateNetworks: []*scwvpc.PrivateNetwork{
36+
{ID: "pn-resolved-1", Name: "my-cluster", VpcID: "vpc-default"},
37+
},
38+
}
39+
40+
testLB := &scwlb.LB{
41+
ID: "lb-1",
42+
Zone: scw.ZoneFrPar2,
43+
}
44+
45+
t.Run("pn-names ignored when gate is disabled", func(t *testing.T) {
46+
// Do NOT set SCW_ENABLE_LB_PN_NAME_SELECTOR (gate off by default)
47+
t.Setenv(enableLBPNNameSelectorEnv, "")
48+
49+
lbAPI := &fakeLBAPI{}
50+
lb := newTestLB(lbAPI, vpcAPI, "pn-from-env")
51+
52+
service := &v1.Service{
53+
ObjectMeta: metav1.ObjectMeta{
54+
Annotations: map[string]string{
55+
"service.beta.kubernetes.io/scw-loadbalancer-pn-names": "default/my-cluster",
56+
},
57+
},
58+
}
59+
60+
gotIDs, err := lb.attachPrivateNetworks(testLB, service, false, scw.RegionFrPar, "proj-1")
61+
if err != nil {
62+
t.Fatalf("unexpected error: %v", err)
63+
}
64+
65+
// pn-names should be ignored; should fall back to env var PN_ID
66+
if len(gotIDs) != 1 || gotIDs[0] != "pn-from-env" {
67+
t.Errorf("expected fallback to env var PN_ID [pn-from-env], got %v", gotIDs)
68+
}
69+
if len(lbAPI.attachCalls) != 1 || lbAPI.attachCalls[0] != "pn-from-env" {
70+
t.Errorf("expected attach call for pn-from-env, got %v", lbAPI.attachCalls)
71+
}
72+
})
73+
74+
t.Run("pn-names used when gate is enabled", func(t *testing.T) {
75+
t.Setenv(enableLBPNNameSelectorEnv, "true")
76+
77+
lbAPI := &fakeLBAPI{}
78+
lb := newTestLB(lbAPI, vpcAPI, "pn-from-env")
79+
80+
service := &v1.Service{
81+
ObjectMeta: metav1.ObjectMeta{
82+
Annotations: map[string]string{
83+
"service.beta.kubernetes.io/scw-loadbalancer-pn-names": "default/my-cluster",
84+
},
85+
},
86+
}
87+
88+
gotIDs, err := lb.attachPrivateNetworks(testLB, service, false, scw.RegionFrPar, "proj-1")
89+
if err != nil {
90+
t.Fatalf("unexpected error: %v", err)
91+
}
92+
93+
// pn-names should resolve, taking precedence over env var
94+
if len(gotIDs) != 1 || gotIDs[0] != "pn-resolved-1" {
95+
t.Errorf("expected resolved PN [pn-resolved-1], got %v", gotIDs)
96+
}
97+
if len(lbAPI.attachCalls) != 1 || lbAPI.attachCalls[0] != "pn-resolved-1" {
98+
t.Errorf("expected attach call for pn-resolved-1, got %v", lbAPI.attachCalls)
99+
}
100+
})
101+
102+
t.Run("pn-names only with no env var returns nil when gate disabled", func(t *testing.T) {
103+
t.Setenv(enableLBPNNameSelectorEnv, "")
104+
105+
lbAPI := &fakeLBAPI{}
106+
lb := newTestLB(lbAPI, vpcAPI, "") // no env var PN_ID
107+
108+
service := &v1.Service{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Annotations: map[string]string{
111+
"service.beta.kubernetes.io/scw-loadbalancer-pn-names": "default/my-cluster",
112+
},
113+
},
114+
}
115+
116+
gotIDs, err := lb.attachPrivateNetworks(testLB, service, false, scw.RegionFrPar, "proj-1")
117+
if err != nil {
118+
t.Fatalf("unexpected error: %v", err)
119+
}
120+
121+
// pn-names ignored + no env var = no PNs configured
122+
if gotIDs != nil {
123+
t.Errorf("expected nil configured IDs when gate disabled and no env var, got %v", gotIDs)
124+
}
125+
if len(lbAPI.attachCalls) != 0 {
126+
t.Errorf("expected no attach calls, got %v", lbAPI.attachCalls)
127+
}
128+
})
129+
}
130+
30131
// fakeLBAPI implements the subset of LoadBalancerAPI needed for attachPrivateNetworks tests.
31132
type fakeLBAPI struct {
32133
privateNetworks []*scwlb.PrivateNetwork
@@ -391,6 +492,9 @@ func TestAttachPrivateNetworks(t *testing.T) {
391492

392493
for _, tt := range tests {
393494
t.Run(tt.name, func(t *testing.T) {
495+
// Enable the PN name selector feature for all tests in this suite
496+
t.Setenv(enableLBPNNameSelectorEnv, "true")
497+
394498
lbAPI := &fakeLBAPI{
395499
privateNetworks: tt.existingPNs,
396500
}

0 commit comments

Comments
 (0)