Skip to content

Commit 4ae7bf2

Browse files
authored
feat: add service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types annotation (#256)
1 parent 576186a commit 4ae7bf2

4 files changed

Lines changed: 157 additions & 2 deletions

File tree

docs/loadbalancer-annotations.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,16 @@ The possible formats are:
229229
- `<ip-id>`: will attach a single IP to the LB.
230230
- `<ip-id>,<ip-id>`: will attach the two IPs to the LB.
231231

232+
### `service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types`
233+
234+
This is the annotation to choose which flexible IP(s) to book when the LB is created.
235+
It is a comma delimited list of IP types. The default value is `ipv4` and the possible
236+
values are `ipv4` or `ipv4,ipv6`.
237+
A flexible IPv4 is always required, an IPv6-only LB is not supported.
238+
This annotation is only taken into account at LB creation, and has no effect on an
239+
already existing LB. It is ignored if `service.beta.kubernetes.io/scw-loadbalancer-ip-ids`
240+
is set, since in that case the provided IP(s) are used instead of booking new ones.
241+
232242
### `service.beta.kubernetes.io/scw-loadbalancer-private-ip-ids`
233243

234244
This is the annotation to statically set the private IPs of the loadbalancer.

scaleway/loadbalancers.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,11 +480,20 @@ func (l *loadbalancers) createLoadBalancer(ctx context.Context, service *v1.Serv
480480

481481
// Attach specific IP if set
482482
var ipIDs []string
483+
var assignFlexibleIP, assignFlexibleIPv6 bool
483484
if !lbPrivate {
484485
ipIDs, err = l.getLoadBalancerStaticIPIDs(ctx, service)
485486
if err != nil {
486487
return nil, err
487488
}
489+
490+
// We must only assign flexible IP(s) if no IP ID is provided.
491+
if len(ipIDs) == 0 {
492+
assignFlexibleIP, assignFlexibleIPv6, err = getFlexibleIPTypes(service)
493+
if err != nil {
494+
return nil, fmt.Errorf("invalid value for annotation %s: %w", serviceAnnotationLoadBalancerFlexibleIPTypes, err)
495+
}
496+
}
488497
}
489498

490499
lbName := l.GetLoadBalancerName(ctx, "", service)
@@ -510,9 +519,10 @@ func (l *loadbalancers) createLoadBalancer(ctx context.Context, service *v1.Serv
510519
Tags: tags,
511520
IPIDs: ipIDs,
512521
Type: lbType,
513-
// We must only assign a flexible IP if LB is public AND no IP ID is provided.
522+
// We must only assign flexible IP(s) if LB is public AND no IP ID is provided.
514523
// If IP IDs are provided, there must be at least one IPv4.
515-
AssignFlexibleIP: scw.BoolPtr(!lbPrivate && len(ipIDs) == 0),
524+
AssignFlexibleIP: new(assignFlexibleIP),
525+
AssignFlexibleIPv6: new(assignFlexibleIPv6),
516526
SslCompatibilityLevel: sslCompatibilityLevel,
517527
}
518528
lb, err := l.api.CreateLB(&request, scw.WithContext(ctx))

scaleway/loadbalancers_annotations.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scaleway
22

33
import (
4+
"errors"
45
"fmt"
56
"net/url"
67
"slices"
@@ -253,6 +254,13 @@ const (
253254
// - "<pn-id>,<pn-id>": will attach the two Private Networks to the LB.
254255
serviceAnnotationPrivateNetworkIDs = "service.beta.kubernetes.io/scw-loadbalancer-pn-ids"
255256

257+
// serviceAnnotationLoadBalancerFlexibleIPTypes is the annotation to choose which flexible IP(s) to book for the LB
258+
// when it is created and no static IP is provided via service.beta.kubernetes.io/scw-loadbalancer-ip-ids.
259+
// The default value is "ipv4" and the possible values are "ipv4" or "ipv4,ipv6".
260+
// A flexible IPv4 is always required, an IPv6-only LB is not supported.
261+
// This annotation is only used at LB creation and has no effect on an already existing LB.
262+
serviceAnnotationLoadBalancerFlexibleIPTypes = "service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types"
263+
256264
// serviceAnnotationLoadBalancerHealthCheckFromService is the annotation to use healthCheckNodePort from the service
257265
// The possible values are "false", "true" or "*" for all ports or a comma delimited list of the service port
258266
// (for instance "80,443"). When enabled for a port, the health check will use the service's healthCheckNodePort
@@ -329,6 +337,32 @@ func getIPIDs(service *v1.Service) []string {
329337
return strings.Split(ipIDs, ",")
330338
}
331339

340+
// getFlexibleIPTypes returns whether a flexible IPv4 and/or a flexible IPv6 should be
341+
// booked for the LB, based on the serviceAnnotationLoadBalancerFlexibleIPTypes annotation.
342+
func getFlexibleIPTypes(service *v1.Service) (assignIPv4 bool, assignIPv6 bool, err error) {
343+
flexibleIPTypes := service.Annotations[serviceAnnotationLoadBalancerFlexibleIPTypes]
344+
if flexibleIPTypes == "" {
345+
return true, false, nil
346+
}
347+
348+
for ipType := range strings.SplitSeq(flexibleIPTypes, ",") {
349+
switch strings.ToLower(strings.TrimSpace(ipType)) {
350+
case "ipv4":
351+
assignIPv4 = true
352+
case "ipv6":
353+
assignIPv6 = true
354+
default:
355+
return false, false, fmt.Errorf("unsupported value %q", strings.ToLower(strings.TrimSpace(ipType)))
356+
}
357+
}
358+
359+
if !assignIPv4 {
360+
return false, false, errors.New("an IPv4 is always required")
361+
}
362+
363+
return assignIPv4, assignIPv6, nil
364+
}
365+
332366
func getPrivateIPIDs(service *v1.Service) sets.Set[string] {
333367
privateIPIDs := service.Annotations[serviceAnnotationLoadBalancerPrivateIPIDs]
334368
if privateIPIDs == "" {

scaleway/loadbalancers_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,6 +2185,107 @@ func TestGetSSLCompatibilityLevel(t *testing.T) {
21852185
}
21862186
}
21872187

2188+
func TestGetFlexibleIPTypes(t *testing.T) {
2189+
matrix := []struct {
2190+
name string
2191+
service *v1.Service
2192+
wantIPv4 bool
2193+
wantIPv6 bool
2194+
wantErr bool
2195+
}{
2196+
{
2197+
"with no annotation",
2198+
&v1.Service{
2199+
ObjectMeta: metav1.ObjectMeta{
2200+
Annotations: map[string]string{},
2201+
},
2202+
},
2203+
true,
2204+
false,
2205+
false,
2206+
},
2207+
{
2208+
"with ipv4",
2209+
&v1.Service{
2210+
ObjectMeta: metav1.ObjectMeta{
2211+
Annotations: map[string]string{
2212+
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "ipv4",
2213+
},
2214+
},
2215+
},
2216+
true,
2217+
false,
2218+
false,
2219+
},
2220+
{
2221+
"with ipv6 only (unsupported)",
2222+
&v1.Service{
2223+
ObjectMeta: metav1.ObjectMeta{
2224+
Annotations: map[string]string{
2225+
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "ipv6",
2226+
},
2227+
},
2228+
},
2229+
false,
2230+
false,
2231+
true,
2232+
},
2233+
{
2234+
"with ipv4,ipv6",
2235+
&v1.Service{
2236+
ObjectMeta: metav1.ObjectMeta{
2237+
Annotations: map[string]string{
2238+
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "ipv4,ipv6",
2239+
},
2240+
},
2241+
},
2242+
true,
2243+
true,
2244+
false,
2245+
},
2246+
{
2247+
"with spaces and uppercase values",
2248+
&v1.Service{
2249+
ObjectMeta: metav1.ObjectMeta{
2250+
Annotations: map[string]string{
2251+
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "IPV4, IPV6",
2252+
},
2253+
},
2254+
},
2255+
true,
2256+
true,
2257+
false,
2258+
},
2259+
{
2260+
"with invalid value",
2261+
&v1.Service{
2262+
ObjectMeta: metav1.ObjectMeta{
2263+
Annotations: map[string]string{
2264+
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "invalid",
2265+
},
2266+
},
2267+
},
2268+
false,
2269+
false,
2270+
true,
2271+
},
2272+
}
2273+
2274+
for _, tt := range matrix {
2275+
t.Run(tt.name, func(t *testing.T) {
2276+
gotIPv4, gotIPv6, err := getFlexibleIPTypes(tt.service)
2277+
if tt.wantErr != (err != nil) {
2278+
t.Errorf("got error: %s, expected: %v", err, tt.wantErr)
2279+
return
2280+
}
2281+
2282+
if gotIPv4 != tt.wantIPv4 || gotIPv6 != tt.wantIPv6 {
2283+
t.Errorf("want: (%v, %v), got: (%v, %v)", tt.wantIPv4, tt.wantIPv6, gotIPv4, gotIPv6)
2284+
}
2285+
})
2286+
}
2287+
}
2288+
21882289
func Test_ipMode(t *testing.T) {
21892290
type args struct {
21902291
service *v1.Service

0 commit comments

Comments
 (0)