@@ -148,7 +148,7 @@ func (l *loadbalancers) shareIPs(ctx context.Context, addrs []string, node *v1.N
148
148
// perform IP sharing (via a specified node selector) have the expected IPs shared
149
149
// in the event that a Node joins the cluster after the LoadBalancer Service already
150
150
// exists
151
- func (l * loadbalancers ) handleIPSharing (ctx context.Context , node * v1.Node ) error {
151
+ func (l * loadbalancers ) handleIPSharing (ctx context.Context , node * v1.Node , ipHolderSuffix string ) error {
152
152
// ignore cases where the provider ID has been set
153
153
if node .Spec .ProviderID == "" {
154
154
klog .Info ("skipping IP while providerID is unset" )
@@ -182,7 +182,7 @@ func (l *loadbalancers) handleIPSharing(ctx context.Context, node *v1.Node) erro
182
182
// if any of the addrs don't exist on the ip-holder (e.g. someone manually deleted it outside the CCM),
183
183
// we need to exclude that from the list
184
184
// TODO: also clean up the CiliumLoadBalancerIPPool for that missing IP if that happens
185
- ipHolder , err := l .getIPHolder (ctx )
185
+ ipHolder , err := l .getIPHolder (ctx , ipHolderSuffix )
186
186
if err != nil {
187
187
return err
188
188
}
@@ -207,8 +207,8 @@ func (l *loadbalancers) handleIPSharing(ctx context.Context, node *v1.Node) erro
207
207
208
208
// createSharedIP requests an additional IP that can be shared on Nodes to support
209
209
// loadbalancing via Cilium LB IPAM + BGP Control Plane.
210
- func (l * loadbalancers ) createSharedIP (ctx context.Context , nodes []* v1.Node ) (string , error ) {
211
- ipHolder , err := l .ensureIPHolder (ctx )
210
+ func (l * loadbalancers ) createSharedIP (ctx context.Context , nodes []* v1.Node , ipHolderSuffix string ) (string , error ) {
211
+ ipHolder , err := l .ensureIPHolder (ctx , ipHolderSuffix )
212
212
if err != nil {
213
213
return "" , err
214
214
}
@@ -276,7 +276,15 @@ func (l *loadbalancers) deleteSharedIP(ctx context.Context, service *v1.Service)
276
276
return err
277
277
}
278
278
bgpNodes := nodeList .Items
279
- ipHolder , err := l .getIPHolder (ctx )
279
+
280
+ serviceNn := getServiceNn (service )
281
+ var ipHolderSuffix string
282
+ if Options .IpHolderSuffix != "" {
283
+ ipHolderSuffix = Options .IpHolderSuffix
284
+ klog .V (3 ).Infof ("using parameter-based IP Holder suffix %s for Service %s" , ipHolderSuffix , serviceNn )
285
+ }
286
+
287
+ ipHolder , err := l .getIPHolder (ctx , ipHolderSuffix )
280
288
if err != nil {
281
289
// return error or nil if not found since no IP holder means there
282
290
// is no IP to reclaim
@@ -310,48 +318,90 @@ func (l *loadbalancers) deleteSharedIP(ctx context.Context, service *v1.Service)
310
318
311
319
// To hold the IP in lieu of a proper IP reservation system, a special Nanode is
312
320
// created but not booted and used to hold all shared IPs.
313
- func (l * loadbalancers ) ensureIPHolder (ctx context.Context ) (* linodego.Instance , error ) {
314
- ipHolder , err := l .getIPHolder (ctx )
321
+ func (l * loadbalancers ) ensureIPHolder (ctx context.Context , suffix string ) (* linodego.Instance , error ) {
322
+ ipHolder , err := l .getIPHolder (ctx , suffix )
315
323
if err != nil {
316
324
return nil , err
317
325
}
318
326
if ipHolder != nil {
319
327
return ipHolder , nil
320
328
}
321
-
329
+ label := generateClusterScopedIPHolderLinodeName ( l . zone , suffix )
322
330
ipHolder , err = l .client .CreateInstance (ctx , linodego.InstanceCreateOptions {
323
331
Region : l .zone ,
324
332
Type : "g6-nanode-1" ,
325
- Label : fmt . Sprintf ( "%s-%s" , ipHolderLabelPrefix , l . zone ) ,
333
+ Label : label ,
326
334
RootPass : uuid .NewString (),
327
335
Image : "linode/ubuntu22.04" ,
328
336
Booted : ptr .To (false ),
329
337
})
330
338
if err != nil {
339
+ if linodego .ErrHasStatus (err , http .StatusBadRequest ) && strings .Contains (err .Error (), "Label must be unique" ) {
340
+ // TODO (rk): should we handle more status codes on error?
341
+ klog .Errorf ("failed to create new IP Holder instance %s since it already exists: %s" , label , err .Error ())
342
+ return nil , err
343
+ }
331
344
return nil , err
332
345
}
346
+ klog .Infof ("created new IP Holder instance %s" , label )
333
347
334
348
return ipHolder , nil
335
349
}
336
350
337
- func (l * loadbalancers ) getIPHolder (ctx context.Context ) (* linodego.Instance , error ) {
351
+ func (l * loadbalancers ) getIPHolder (ctx context.Context , suffix string ) (* linodego.Instance , error ) {
352
+ // even though we have updated the naming convention, leaving this in ensures we have backwards compatibility
338
353
filter := map [string ]string {"label" : fmt .Sprintf ("%s-%s" , ipHolderLabelPrefix , l .zone )}
339
354
rawFilter , err := json .Marshal (filter )
340
355
if err != nil {
341
356
panic ("this should not have failed" )
342
357
}
343
358
var ipHolder * linodego.Instance
359
+ // TODO (rk): should we switch to using GET instead of LIST? we would be able to wrap logic around errors
344
360
linodes , err := l .client .ListInstances (ctx , linodego .NewListOptions (1 , string (rawFilter )))
345
361
if err != nil {
346
362
return nil , err
347
363
}
364
+ if len (linodes ) == 0 {
365
+ // since a list that returns 0 results has a 200/OK status code (no error)
366
+
367
+ // we assume that either
368
+ // a) an ip holder instance does not exist yet
369
+ // or
370
+ // b) another cluster already holds the linode grant to an ip holder using the old naming convention
371
+ filter = map [string ]string {"label" : generateClusterScopedIPHolderLinodeName (l .zone , suffix )}
372
+ rawFilter , err = json .Marshal (filter )
373
+ if err != nil {
374
+ panic ("this should not have failed" )
375
+ }
376
+ linodes , err = l .client .ListInstances (ctx , linodego .NewListOptions (1 , string (rawFilter )))
377
+ if err != nil {
378
+ return nil , err
379
+ }
380
+ }
348
381
if len (linodes ) > 0 {
349
382
ipHolder = & linodes [0 ]
350
383
}
351
-
352
384
return ipHolder , nil
353
385
}
354
386
387
+ // generateClusterScopedIPHolderLinodeName attempts to generate a unique name for the IP Holder
388
+ // instance used alongside Cilium LoadBalancers and Shared IPs for Kubernetes Services.
389
+ // If the `--ip-holder-suffix` arg is passed when running Linode CCM, `suffix` is set to that value.
390
+ func generateClusterScopedIPHolderLinodeName (zone , suffix string ) (label string ) {
391
+ // since Linode CCM consumers are varied, we require a method of providing a
392
+ // suffix that does not rely on the use of a specific product (ex. LKE) to
393
+ // have a specific piece of metadata (ex. annotation(s), label(s) ) present to key off of.
394
+
395
+ if suffix == "" {
396
+ // this avoids a trailing hyphen if suffix is empty (ex. linode-ccm-ip-holder-us-ord-)
397
+ label = fmt .Sprintf ("%s-%s" , ipHolderLabelPrefix , zone )
398
+ } else {
399
+ label = fmt .Sprintf ("%s-%s-%s" , ipHolderLabelPrefix , zone , suffix )
400
+ }
401
+ klog .V (5 ).Infof ("generated IP Holder Linode label: %s" , label )
402
+ return label
403
+ }
404
+
355
405
func (l * loadbalancers ) retrieveCiliumClientset () error {
356
406
if l .ciliumClient != nil {
357
407
return nil
0 commit comments