@@ -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,60 @@ 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+ //
152+ // To illustrate, here's an example of a load balancer status shown on
153+ // a Kubernetes 1.29 service with default settings:
154+ //
155+ // apiVersion: v1
156+ // kind: Service
157+ // ...
158+ // status:
159+ // loadBalancer:
160+ // ingress:
161+ // - ip: 45.81.71.1
162+ // - ip: 2a06:c00::1
163+ //
164+ // Using the annotation causes the status to use the given value instead:
165+ //
166+ // apiVersion: v1
167+ // kind: Service
168+ // metadata:
169+ // annotations:
170+ // k8s.cloudscale.ch/loadbalancer-force-hostname: example.org
171+ // status:
172+ // loadBalancer:
173+ // ingress:
174+ // - hostname: example.org
175+ //
176+ // If you are not using the `proxy` or `proxyv2` protocol, or if you are
177+ // on Kubernetes 1.30 or newer, you probly do not need this setting.
178+ //
179+ // See `LoadBalancerIPMode` below.
180+ LoadBalancerForceHostname = "k8s.cloudscale.ch/loadbalancer-force-hostname"
181+
182+ // LoadBalancerIPMode defines the IP mode reported to Kubernetes for the
183+ // loadbalancers managed by this CCM. It defaults to "Proxy", where all
184+ // traffic destined to the load balancer is sent through the load balancer,
185+ // even if coming from inside the cluster.
186+ //
187+ // The older behavior, where traffic inside the cluster is directly
188+ // sent to the backend service, can be activated by using "VIP" instead.
189+ LoadBalancerIPMode = "k8s.cloudscale.ch/loadbalancer-ip-mode"
190+
136191 // LoadBalancerHealthMonitorDelayS is the delay between two successive
137192 // checks, in seconds. Defaults to 2.
138193 //
@@ -269,7 +324,13 @@ func (l *loadbalancer) GetLoadBalancer(
269324 return nil , false , nil
270325 }
271326
272- return loadBalancerStatus (instance ), true , nil
327+ result , err := l .loadBalancerStatus (serviceInfo , instance )
328+ if err != nil {
329+ return nil , true , fmt .Errorf (
330+ "unable to get load balancer state for %s: %w" , service .Name , err )
331+ }
332+
333+ return result , true , nil
273334}
274335
275336// GetLoadBalancerName returns the name of the load balancer. Implementations
@@ -361,7 +422,13 @@ func (l *loadbalancer) EnsureLoadBalancer(
361422 "unable to annotate service %s: %w" , service .Name , err )
362423 }
363424
364- return loadBalancerStatus (actual .lb ), nil
425+ result , err := l .loadBalancerStatus (serviceInfo , actual .lb )
426+ if err != nil {
427+ return nil , fmt .Errorf (
428+ "unable to get load balancer state for %s: %w" , service .Name , err )
429+ }
430+
431+ return result , nil
365432}
366433
367434// UpdateLoadBalancer updates hosts under the specified load balancer.
@@ -432,6 +499,53 @@ func (l *loadbalancer) EnsureLoadBalancerDeleted(
432499 })
433500}
434501
502+ // loadBalancerStatus generates the v1.LoadBalancerStatus for the given
503+ // loadbalancer, as required by Kubernetes.
504+ func (l * loadbalancer ) loadBalancerStatus (
505+ serviceInfo * serviceInfo ,
506+ lb * cloudscale.LoadBalancer ,
507+ ) (* v1.LoadBalancerStatus , error ) {
508+
509+ status := v1.LoadBalancerStatus {}
510+
511+ // When forcing the use of a hostname, there's exactly one ingress item
512+ hostname := serviceInfo .annotation (LoadBalancerForceHostname )
513+ if len (hostname ) > 0 {
514+ status .Ingress = []v1.LoadBalancerIngress {{Hostname : hostname }}
515+ return & status , nil
516+ }
517+
518+ // Otherwise there as many items as there are addresses
519+ status .Ingress = make ([]v1.LoadBalancerIngress , len (lb .VIPAddresses ))
520+
521+ var ipmode * v1.LoadBalancerIPMode
522+ switch serviceInfo .annotation (LoadBalancerIPMode ) {
523+ case "Proxy" :
524+ ipmode = ptr .To (v1 .LoadBalancerIPModeProxy )
525+ case "VIP" :
526+ ipmode = ptr .To (v1 .LoadBalancerIPModeVIP )
527+ default :
528+ return nil , fmt .Errorf (
529+ "unsupported IP mode: '%s', must be 'Proxy' or 'VIP'" , * ipmode )
530+ }
531+
532+ // On newer releases, we explicitly configure the IP mode
533+ supportsIPMode , err := kubeutil .IsKubernetesReleaseOrNewer (l .k8s , 1 , 30 )
534+ if err != nil {
535+ return nil , fmt .Errorf ("failed to get load balancer status: %w" , err )
536+ }
537+
538+ for i , address := range lb .VIPAddresses {
539+ status .Ingress [i ].IP = address .Address
540+
541+ if supportsIPMode {
542+ status .Ingress [i ].IPMode = ipmode
543+ }
544+ }
545+
546+ return & status , nil
547+ }
548+
435549// ensureValidConfig ensures that the configuration can be applied at all,
436550// acting as a gate that ensures certain invariants before any changes are
437551// made.
@@ -545,17 +659,3 @@ func (l *loadbalancer) findIPsAssignedElsewhere(
545659
546660 return conflicts , nil
547661}
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