Skip to content

Commit b14d4a1

Browse files
committed
create ip holder per cluster + tests, update docs
Signed-off-by: Ross Kirkpatrick <[email protected]>
1 parent 66afcae commit b14d4a1

File tree

11 files changed

+528
-132
lines changed

11 files changed

+528
-132
lines changed

README.md

+45-40
Large diffs are not rendered by default.

cloud/linode/cilium_loadbalancers.go

+61-11
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (l *loadbalancers) shareIPs(ctx context.Context, addrs []string, node *v1.N
148148
// perform IP sharing (via a specified node selector) have the expected IPs shared
149149
// in the event that a Node joins the cluster after the LoadBalancer Service already
150150
// 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 {
152152
// ignore cases where the provider ID has been set
153153
if node.Spec.ProviderID == "" {
154154
klog.Info("skipping IP while providerID is unset")
@@ -182,7 +182,7 @@ func (l *loadbalancers) handleIPSharing(ctx context.Context, node *v1.Node) erro
182182
// if any of the addrs don't exist on the ip-holder (e.g. someone manually deleted it outside the CCM),
183183
// we need to exclude that from the list
184184
// 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)
186186
if err != nil {
187187
return err
188188
}
@@ -207,8 +207,8 @@ func (l *loadbalancers) handleIPSharing(ctx context.Context, node *v1.Node) erro
207207

208208
// createSharedIP requests an additional IP that can be shared on Nodes to support
209209
// 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)
212212
if err != nil {
213213
return "", err
214214
}
@@ -276,7 +276,15 @@ func (l *loadbalancers) deleteSharedIP(ctx context.Context, service *v1.Service)
276276
return err
277277
}
278278
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)
280288
if err != nil {
281289
// return error or nil if not found since no IP holder means there
282290
// is no IP to reclaim
@@ -310,48 +318,90 @@ func (l *loadbalancers) deleteSharedIP(ctx context.Context, service *v1.Service)
310318

311319
// To hold the IP in lieu of a proper IP reservation system, a special Nanode is
312320
// 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)
315323
if err != nil {
316324
return nil, err
317325
}
318326
if ipHolder != nil {
319327
return ipHolder, nil
320328
}
321-
329+
label := generateClusterScopedIPHolderLinodeName(l.zone, suffix)
322330
ipHolder, err = l.client.CreateInstance(ctx, linodego.InstanceCreateOptions{
323331
Region: l.zone,
324332
Type: "g6-nanode-1",
325-
Label: fmt.Sprintf("%s-%s", ipHolderLabelPrefix, l.zone),
333+
Label: label,
326334
RootPass: uuid.NewString(),
327335
Image: "linode/ubuntu22.04",
328336
Booted: ptr.To(false),
329337
})
330338
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+
}
331344
return nil, err
332345
}
346+
klog.Infof("created new IP Holder instance %s", label)
333347

334348
return ipHolder, nil
335349
}
336350

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
338353
filter := map[string]string{"label": fmt.Sprintf("%s-%s", ipHolderLabelPrefix, l.zone)}
339354
rawFilter, err := json.Marshal(filter)
340355
if err != nil {
341356
panic("this should not have failed")
342357
}
343358
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
344360
linodes, err := l.client.ListInstances(ctx, linodego.NewListOptions(1, string(rawFilter)))
345361
if err != nil {
346362
return nil, err
347363
}
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+
}
348381
if len(linodes) > 0 {
349382
ipHolder = &linodes[0]
350383
}
351-
352384
return ipHolder, nil
353385
}
354386

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+
355405
func (l *loadbalancers) retrieveCiliumClientset() error {
356406
if l.ciliumClient != nil {
357407
return nil

0 commit comments

Comments
 (0)