@@ -12,6 +12,7 @@ import (
1212 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1313 "k8s.io/client-go/kubernetes"
1414 "k8s.io/klog/v2"
15+ "k8s.io/utils/ptr"
1516)
1617
1718// Annotations used by the loadbalancer integration of cloudscale_ccm. Those
@@ -133,6 +134,31 @@ const (
133134 // as all pools have to be recreated.
134135 LoadBalancerPoolProtocol = "k8s.cloudscale.ch/loadbalancer-pool-protocol"
135136
137+ // LoadBalancerForceHostname forces the CCM to report a specific hostname
138+ // to Kubernetes when returning the loadbalancer status, instead of
139+ // reporting the IP address(es).
140+ //
141+ // The hostname used should point to the same IP address that would
142+ // otherwise be reported. This is used as a workaround for clusters that
143+ // do not support status.loadBalancer.ingress.ipMode, and use `proxy` or
144+ // `proxyv2` protocol.
145+ //
146+ // For newer clusters, .status.loadBalancer.ingress.ipMode is automatically
147+ // set to "Proxy", unless LoadBalancerIPMode is set to "VIP"
148+ //
149+ // For more information about this workaround see
150+ // https://kubernetes.io/blog/2023/12/18/kubernetes-1-29-feature-loadbalancer-ip-mode-alpha/
151+ LoadBalancerForceHostname = "k8s.cloudscale.ch/loadbalancer-force-hostname"
152+
153+ // LoadBalancerIPMode defines the IP mode reported to Kubernetes for the
154+ // loadbalancers managed by this CCM. It defaults to "Proxy", where all
155+ // traffic destined to the load balancer is sent through the load balancer,
156+ // even if coming from inside the cluster.
157+ //
158+ // The older behavior, where traffic inside the cluster is directly
159+ // sent to the backend service, can be activated by using "VIP" instead.
160+ LoadBalancerIPMode = "k8s.cloudscale.ch/loadbalancer-ip-mode"
161+
136162 // LoadBalancerHealthMonitorDelayS is the delay between two successive
137163 // checks, in seconds. Defaults to 2.
138164 //
@@ -269,7 +295,13 @@ func (l *loadbalancer) GetLoadBalancer(
269295 return nil , false , nil
270296 }
271297
272- return loadBalancerStatus (instance ), true , nil
298+ result , err := l .loadBalancerStatus (serviceInfo , instance )
299+ if err != nil {
300+ return nil , true , fmt .Errorf (
301+ "unable to get load balancer state for %s: %w" , service .Name , err )
302+ }
303+
304+ return result , true , nil
273305}
274306
275307// GetLoadBalancerName returns the name of the load balancer. Implementations
@@ -361,7 +393,13 @@ func (l *loadbalancer) EnsureLoadBalancer(
361393 "unable to annotate service %s: %w" , service .Name , err )
362394 }
363395
364- return loadBalancerStatus (actual .lb ), nil
396+ result , err := l .loadBalancerStatus (serviceInfo , actual .lb )
397+ if err != nil {
398+ return nil , fmt .Errorf (
399+ "unable to get load balancer state for %s: %w" , service .Name , err )
400+ }
401+
402+ return result , nil
365403}
366404
367405// UpdateLoadBalancer updates hosts under the specified load balancer.
@@ -432,6 +470,53 @@ func (l *loadbalancer) EnsureLoadBalancerDeleted(
432470 })
433471}
434472
473+ // loadBalancerStatus generates the v1.LoadBalancerStatus for the given
474+ // loadbalancer, as required by Kubernetes.
475+ func (l * loadbalancer ) loadBalancerStatus (
476+ serviceInfo * serviceInfo ,
477+ lb * cloudscale.LoadBalancer ,
478+ ) (* v1.LoadBalancerStatus , error ) {
479+
480+ status := v1.LoadBalancerStatus {}
481+
482+ // When forcing the use of a hostname, there's exactly one ingress item
483+ hostname := serviceInfo .annotation (LoadBalancerForceHostname )
484+ if len (hostname ) > 0 {
485+ status .Ingress = []v1.LoadBalancerIngress {{Hostname : hostname }}
486+ return & status , nil
487+ }
488+
489+ // Otherwise there as many items as there are addresses
490+ status .Ingress = make ([]v1.LoadBalancerIngress , len (lb .VIPAddresses ))
491+
492+ var ipmode * v1.LoadBalancerIPMode
493+ switch serviceInfo .annotation (LoadBalancerIPMode ) {
494+ case "Proxy" :
495+ ipmode = ptr .To (v1 .LoadBalancerIPModeProxy )
496+ case "VIP" :
497+ ipmode = ptr .To (v1 .LoadBalancerIPModeVIP )
498+ default :
499+ return nil , fmt .Errorf (
500+ "unsupported IP mode: '%s', must be 'Proxy' or 'VIP'" , * ipmode )
501+ }
502+
503+ // On newer releases, we explicitly configure the IP mode
504+ supportsIPMode , err := kubeutil .IsKubernetesReleaseOrNewer (l .k8s , 1 , 30 )
505+ if err != nil {
506+ return nil , fmt .Errorf ("failed to get load balancer status: %w" , err )
507+ }
508+
509+ for i , address := range lb .VIPAddresses {
510+ status .Ingress [i ].IP = address .Address
511+
512+ if supportsIPMode {
513+ status .Ingress [i ].IPMode = ipmode
514+ }
515+ }
516+
517+ return & status , nil
518+ }
519+
435520// ensureValidConfig ensures that the configuration can be applied at all,
436521// acting as a gate that ensures certain invariants before any changes are
437522// made.
@@ -545,17 +630,3 @@ func (l *loadbalancer) findIPsAssignedElsewhere(
545630
546631 return conflicts , nil
547632}
548-
549- // loadBalancerStatus generates the v1.LoadBalancerStatus for the given
550- // loadbalancer, as required by Kubernetes.
551- func loadBalancerStatus (lb * cloudscale.LoadBalancer ) * v1.LoadBalancerStatus {
552-
553- status := v1.LoadBalancerStatus {}
554- status .Ingress = make ([]v1.LoadBalancerIngress , len (lb .VIPAddresses ))
555-
556- for i , address := range lb .VIPAddresses {
557- status .Ingress [i ].IP = address .Address
558- }
559-
560- return & status
561- }
0 commit comments