@@ -12,6 +12,7 @@ import (
12
12
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13
13
"k8s.io/client-go/kubernetes"
14
14
"k8s.io/klog/v2"
15
+ "k8s.io/utils/ptr"
15
16
)
16
17
17
18
// Annotations used by the loadbalancer integration of cloudscale_ccm. Those
@@ -133,6 +134,60 @@ const (
133
134
// as all pools have to be recreated.
134
135
LoadBalancerPoolProtocol = "k8s.cloudscale.ch/loadbalancer-pool-protocol"
135
136
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
+
136
191
// LoadBalancerHealthMonitorDelayS is the delay between two successive
137
192
// checks, in seconds. Defaults to 2.
138
193
//
@@ -269,7 +324,13 @@ func (l *loadbalancer) GetLoadBalancer(
269
324
return nil , false , nil
270
325
}
271
326
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
273
334
}
274
335
275
336
// GetLoadBalancerName returns the name of the load balancer. Implementations
@@ -361,7 +422,13 @@ func (l *loadbalancer) EnsureLoadBalancer(
361
422
"unable to annotate service %s: %w" , service .Name , err )
362
423
}
363
424
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
365
432
}
366
433
367
434
// UpdateLoadBalancer updates hosts under the specified load balancer.
@@ -432,6 +499,53 @@ func (l *loadbalancer) EnsureLoadBalancerDeleted(
432
499
})
433
500
}
434
501
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
+
435
549
// ensureValidConfig ensures that the configuration can be applied at all,
436
550
// acting as a gate that ensures certain invariants before any changes are
437
551
// made.
@@ -545,17 +659,3 @@ func (l *loadbalancer) findIPsAssignedElsewhere(
545
659
546
660
return conflicts , nil
547
661
}
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