@@ -20,7 +20,6 @@ import (
2020 "context"
2121 "errors"
2222 "fmt"
23- "net"
2423 "os"
2524 "reflect"
2625 "strconv"
@@ -155,8 +154,9 @@ func (l *loadbalancers) EnsureLoadBalancer(ctx context.Context, clusterName stri
155154 if lbPrivate && l .pnID == "" {
156155 return nil , fmt .Errorf ("scaleway-cloud-controller-manager cannot create private load balancers without a private network" )
157156 }
158- if lbPrivate && service .Spec .LoadBalancerIP != "" {
159- return nil , fmt .Errorf ("scaleway-cloud-controller-manager can only handle .spec.LoadBalancerIP for public load balancers. Unsetting the .spec.LoadBalancerIP can result in the loss of the IP" )
157+
158+ if lbPrivate && hasLoadBalancerStaticIPs (service ) {
159+ return nil , fmt .Errorf ("scaleway-cloud-controller-manager can only handle static IPs for public load balancers. Unsetting the static IP can result in the loss of the IP" )
160160 }
161161
162162 lb , err := l .fetchLoadBalancer (ctx , clusterName , service )
@@ -177,7 +177,7 @@ func (l *loadbalancers) EnsureLoadBalancer(ctx context.Context, clusterName stri
177177
178178 if ! lbExternallyManaged {
179179 privateModeMismatch := lbPrivate != (len (lb .IP ) == 0 )
180- reservedIPMismatch := service . Spec . LoadBalancerIP != "" && service . Spec . LoadBalancerIP != lb . IP [ 0 ]. IPAddress
180+ reservedIPMismatch := hasLoadBalancerStaticIPs ( service ) && ! hasEqualLoadBalancerStaticIPs ( service , lb )
181181 if privateModeMismatch || reservedIPMismatch {
182182 err = l .deleteLoadBalancer (ctx , lb , clusterName , service )
183183 if err != nil {
@@ -327,13 +327,10 @@ func (l *loadbalancers) deleteLoadBalancer(ctx context.Context, lb *scwlb.LB, cl
327327 return nil
328328 }
329329
330- // if loadBalancerIP is not set, it implies an ephemeral IP
331- releaseIP := service .Spec .LoadBalancerIP == ""
332-
333330 request := & scwlb.ZonedAPIDeleteLBRequest {
334331 Zone : lb .Zone ,
335332 LBID : lb .ID ,
336- ReleaseIP : releaseIP ,
333+ ReleaseIP : ! hasLoadBalancerStaticIPs ( service ), // if no static IP is set, it implies an ephemeral IP
337334 }
338335
339336 err := l .api .DeleteLB (request )
@@ -471,27 +468,12 @@ func (l *loadbalancers) createLoadBalancer(ctx context.Context, clusterName stri
471468 }
472469
473470 // Attach specific IP if set
474- var ipID * string
475- if ! lbPrivate && service .Spec .LoadBalancerIP != "" {
476- request := scwlb.ZonedAPIListIPsRequest {
477- IPAddress : & service .Spec .LoadBalancerIP ,
478- Zone : getLoadBalancerZone (service ),
479- }
480- ipsResp , err := l .api .ListIPs (& request )
471+ var ipIDs []string
472+ if ! lbPrivate {
473+ ipIDs , err = l .getLoadBalancerStaticIPIDs (service )
481474 if err != nil {
482- klog .Errorf ("error getting ip for service %s/%s: %v" , service .Namespace , service .Name , err )
483- return nil , fmt .Errorf ("createLoadBalancer: error getting ip for service %s: %s" , service .Name , err .Error ())
484- }
485-
486- if len (ipsResp .IPs ) == 0 {
487- return nil , IPAddressNotFound
488- }
489-
490- if ipsResp .IPs [0 ].LBID != nil && * ipsResp .IPs [0 ].LBID != "" {
491- return nil , IPAddressInUse
475+ return nil , err
492476 }
493-
494- ipID = & ipsResp .IPs [0 ].ID
495477 }
496478
497479 lbName := l .GetLoadBalancerName (ctx , clusterName , service )
@@ -511,13 +493,15 @@ func (l *loadbalancers) createLoadBalancer(ctx context.Context, clusterName stri
511493 tags = append (tags , "managed-by-scaleway-cloud-controller-manager" )
512494
513495 request := scwlb.ZonedAPICreateLBRequest {
514- Zone : getLoadBalancerZone (service ),
515- Name : lbName ,
516- Description : "kubernetes service " + service .Name ,
517- Tags : tags ,
518- IPID : ipID ,
519- Type : lbType ,
520- AssignFlexibleIP : scw .BoolPtr (! lbPrivate ),
496+ Zone : getLoadBalancerZone (service ),
497+ Name : lbName ,
498+ Description : "kubernetes service " + service .Name ,
499+ Tags : tags ,
500+ IPIDs : ipIDs ,
501+ Type : lbType ,
502+ // We must only assign a flexible IP if LB is public AND no IP ID is provided.
503+ // If IP IDs are provided, there must be at least one IPv4.
504+ AssignFlexibleIP : scw .BoolPtr (! lbPrivate && len (ipIDs ) == 0 ),
521505 }
522506 lb , err := l .api .CreateLB (& request )
523507 if err != nil {
@@ -533,6 +517,38 @@ func (l *loadbalancers) createLoadBalancer(ctx context.Context, clusterName stri
533517 return lb , nil
534518}
535519
520+ // getLoadBalancerStaticIPIDs returns user-provided static IPs for the LB from annotations.
521+ // If no annotation is found, it uses the LoadBalancerIP field from service spec.
522+ // It returns nil if user provided no static IP. In this case, the CCM must manage a dynamic IP.
523+ func (l * loadbalancers ) getLoadBalancerStaticIPIDs (service * v1.Service ) ([]string , error ) {
524+ if ipIDs := getIPIDs (service ); len (ipIDs ) > 0 {
525+ return ipIDs , nil
526+ }
527+
528+ if service .Spec .LoadBalancerIP != "" {
529+ ipsResp , err := l .api .ListIPs (& scwlb.ZonedAPIListIPsRequest {
530+ IPAddress : & service .Spec .LoadBalancerIP ,
531+ Zone : getLoadBalancerZone (service ),
532+ })
533+ if err != nil {
534+ klog .Errorf ("error getting ip for service %s/%s: %v" , service .Namespace , service .Name , err )
535+ return nil , fmt .Errorf ("createLoadBalancer: error getting ip for service %s: %s" , service .Name , err .Error ())
536+ }
537+
538+ if len (ipsResp .IPs ) == 0 {
539+ return nil , IPAddressNotFound
540+ }
541+
542+ if ipsResp .IPs [0 ].LBID != nil && * ipsResp .IPs [0 ].LBID != "" {
543+ return nil , IPAddressInUse
544+ }
545+
546+ return []string {ipsResp .IPs [0 ].ID }, nil
547+ }
548+
549+ return nil , nil
550+ }
551+
536552// annotateAndPatch adds the loadbalancer id to the service's annotations
537553func (l * loadbalancers ) annotateAndPatch (service * v1.Service , loadbalancer * scwlb.LB ) error {
538554 service = service .DeepCopy ()
@@ -854,11 +870,6 @@ func (l *loadbalancers) createPublicServiceStatus(service *v1.Service, lb *scwlb
854870 status := & v1.LoadBalancerStatus {}
855871 status .Ingress = make ([]v1.LoadBalancerIngress , 0 )
856872 for _ , ip := range lb .IP {
857- // Skip ipv6 entries
858- if i := net .ParseIP (ip .IPAddress ); i .To4 () == nil {
859- continue
860- }
861-
862873 if getUseHostname (service ) {
863874 status .Ingress = append (status .Ingress , v1.LoadBalancerIngress {Hostname : ip .Reverse })
864875 } else {
@@ -867,7 +878,7 @@ func (l *loadbalancers) createPublicServiceStatus(service *v1.Service, lb *scwlb
867878 }
868879
869880 if len (status .Ingress ) == 0 {
870- return nil , fmt .Errorf ("no ipv4 found for lb %s" , lb .Name )
881+ return nil , fmt .Errorf ("no ip found for lb %s" , lb .Name )
871882 }
872883
873884 return status , nil
@@ -1673,3 +1684,46 @@ func ptrBoolToString(b *bool) string {
16731684 }
16741685 return fmt .Sprintf ("%t" , * b )
16751686}
1687+
1688+ // hasLoadBalancerStaticIPs returns true if static IPs are specified for the loadbalancer.
1689+ func hasLoadBalancerStaticIPs (service * v1.Service ) bool {
1690+ if ipIDs := getIPIDs (service ); len (ipIDs ) > 0 {
1691+ return true
1692+ }
1693+
1694+ if service .Spec .LoadBalancerIP != "" {
1695+ return true
1696+ }
1697+
1698+ return false
1699+ }
1700+
1701+ // hasEqualLoadBalancerStaticIPs returns true if the LB has the expected static IPs.
1702+ // This function returns true if no static IP is configured.
1703+ func hasEqualLoadBalancerStaticIPs (service * v1.Service , lb * scwlb.LB ) bool {
1704+ if ipIDs := getIPIDs (service ); len (ipIDs ) > 0 {
1705+ if len (ipIDs ) != len (lb .IP ) {
1706+ return false
1707+ }
1708+
1709+ // Sort IP IDs.
1710+ sortedIPIDs := slices .Clone (ipIDs )
1711+ slices .Sort (sortedIPIDs )
1712+
1713+ // Sort LB IP IDs.
1714+ sortedLBIPIDs := make ([]string , 0 , len (lb .IP ))
1715+ for _ , ip := range lb .IP {
1716+ sortedLBIPIDs = append (sortedLBIPIDs , ip .ID )
1717+ }
1718+ slices .Sort (sortedLBIPIDs )
1719+
1720+ // Compare the sorted list.
1721+ return reflect .DeepEqual (sortedIPIDs , sortedLBIPIDs )
1722+ }
1723+
1724+ if lbIP := service .Spec .LoadBalancerIP ; lbIP != "" {
1725+ return len (lb .IP ) == 1 && lbIP == lb .IP [0 ].IPAddress
1726+ }
1727+
1728+ return true
1729+ }
0 commit comments