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
10 changes: 10 additions & 0 deletions docs/loadbalancer-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ The possible formats are:
- `<ip-id>`: will attach a single IP to the LB.
- `<ip-id>,<ip-id>`: will attach the two IPs to the LB.

### `service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types`

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

### `service.beta.kubernetes.io/scw-loadbalancer-private-ip-ids`

This is the annotation to statically set the private IPs of the loadbalancer.
Expand Down
14 changes: 12 additions & 2 deletions scaleway/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,11 +480,20 @@ func (l *loadbalancers) createLoadBalancer(ctx context.Context, service *v1.Serv

// Attach specific IP if set
var ipIDs []string
var assignFlexibleIP, assignFlexibleIPv6 bool
if !lbPrivate {
ipIDs, err = l.getLoadBalancerStaticIPIDs(ctx, service)
if err != nil {
return nil, err
}

// We must only assign flexible IP(s) if no IP ID is provided.
if len(ipIDs) == 0 {
assignFlexibleIP, assignFlexibleIPv6, err = getFlexibleIPTypes(service)
if err != nil {
return nil, fmt.Errorf("invalid value for annotation %s: %w", serviceAnnotationLoadBalancerFlexibleIPTypes, err)
}
}
}

lbName := l.GetLoadBalancerName(ctx, "", service)
Expand All @@ -510,9 +519,10 @@ func (l *loadbalancers) createLoadBalancer(ctx context.Context, service *v1.Serv
Tags: tags,
IPIDs: ipIDs,
Type: lbType,
// We must only assign a flexible IP if LB is public AND no IP ID is provided.
// We must only assign flexible IP(s) if LB is public AND no IP ID is provided.
// If IP IDs are provided, there must be at least one IPv4.
AssignFlexibleIP: scw.BoolPtr(!lbPrivate && len(ipIDs) == 0),
AssignFlexibleIP: new(assignFlexibleIP),
AssignFlexibleIPv6: new(assignFlexibleIPv6),
SslCompatibilityLevel: sslCompatibilityLevel,
}
lb, err := l.api.CreateLB(&request, scw.WithContext(ctx))
Expand Down
34 changes: 34 additions & 0 deletions scaleway/loadbalancers_annotations.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scaleway

import (
"errors"
"fmt"
"net/url"
"slices"
Expand Down Expand Up @@ -253,6 +254,13 @@ const (
// - "<pn-id>,<pn-id>": will attach the two Private Networks to the LB.
serviceAnnotationPrivateNetworkIDs = "service.beta.kubernetes.io/scw-loadbalancer-pn-ids"

// serviceAnnotationLoadBalancerFlexibleIPTypes is the annotation to choose which flexible IP(s) to book for the LB
// when it is created and no static IP is provided via service.beta.kubernetes.io/scw-loadbalancer-ip-ids.
// The default value is "ipv4" and the possible values are "ipv4" or "ipv4,ipv6".
// A flexible IPv4 is always required, an IPv6-only LB is not supported.
// This annotation is only used at LB creation and has no effect on an already existing LB.
serviceAnnotationLoadBalancerFlexibleIPTypes = "service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types"

// serviceAnnotationLoadBalancerHealthCheckFromService is the annotation to use healthCheckNodePort from the service
// The possible values are "false", "true" or "*" for all ports or a comma delimited list of the service port
// (for instance "80,443"). When enabled for a port, the health check will use the service's healthCheckNodePort
Expand Down Expand Up @@ -329,6 +337,32 @@ func getIPIDs(service *v1.Service) []string {
return strings.Split(ipIDs, ",")
}

// getFlexibleIPTypes returns whether a flexible IPv4 and/or a flexible IPv6 should be
// booked for the LB, based on the serviceAnnotationLoadBalancerFlexibleIPTypes annotation.
func getFlexibleIPTypes(service *v1.Service) (assignIPv4 bool, assignIPv6 bool, err error) {
flexibleIPTypes := service.Annotations[serviceAnnotationLoadBalancerFlexibleIPTypes]
if flexibleIPTypes == "" {
return true, false, nil
}

for ipType := range strings.SplitSeq(flexibleIPTypes, ",") {
switch strings.ToLower(strings.TrimSpace(ipType)) {
case "ipv4":
assignIPv4 = true
case "ipv6":
assignIPv6 = true
default:
return false, false, fmt.Errorf("unsupported value %q", strings.ToLower(strings.TrimSpace(ipType)))
Comment thread
Tomy2e marked this conversation as resolved.
}
}

if !assignIPv4 {
return false, false, errors.New("an IPv4 is always required")
}

return assignIPv4, assignIPv6, nil
}

func getPrivateIPIDs(service *v1.Service) sets.Set[string] {
privateIPIDs := service.Annotations[serviceAnnotationLoadBalancerPrivateIPIDs]
if privateIPIDs == "" {
Expand Down
101 changes: 101 additions & 0 deletions scaleway/loadbalancers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,107 @@ func TestGetSSLCompatibilityLevel(t *testing.T) {
}
}

func TestGetFlexibleIPTypes(t *testing.T) {
matrix := []struct {
name string
service *v1.Service
wantIPv4 bool
wantIPv6 bool
wantErr bool
}{
{
"with no annotation",
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{},
},
},
true,
false,
false,
},
{
"with ipv4",
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "ipv4",
},
},
},
true,
false,
false,
},
{
"with ipv6 only (unsupported)",
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "ipv6",
},
},
},
false,
false,
true,
},
{
"with ipv4,ipv6",
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "ipv4,ipv6",
},
},
},
true,
true,
false,
},
{
"with spaces and uppercase values",
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "IPV4, IPV6",
},
},
},
true,
true,
false,
},
{
"with invalid value",
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"service.beta.kubernetes.io/scw-loadbalancer-flexible-ip-types": "invalid",
},
},
},
false,
false,
true,
},
}

for _, tt := range matrix {
t.Run(tt.name, func(t *testing.T) {
gotIPv4, gotIPv6, err := getFlexibleIPTypes(tt.service)
if tt.wantErr != (err != nil) {
t.Errorf("got error: %s, expected: %v", err, tt.wantErr)
return
}

if gotIPv4 != tt.wantIPv4 || gotIPv6 != tt.wantIPv6 {
t.Errorf("want: (%v, %v), got: (%v, %v)", tt.wantIPv4, tt.wantIPv6, gotIPv4, gotIPv6)
}
})
}
}

func Test_ipMode(t *testing.T) {
type args struct {
service *v1.Service
Expand Down