-
Notifications
You must be signed in to change notification settings - Fork 678
[occm] Tag load balancers with cluster identity to prevent name collisions #3103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
8a0a485
cffd002
c49b652
25e35f2
1bc4118
b7c487e
2e2348c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,3 +96,9 @@ rules: | |
| - list | ||
| - get | ||
| - watch | ||
| - apiGroups: | ||
| - "" | ||
| resources: | ||
| - namespaces | ||
| verbs: | ||
| - get | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,8 +112,25 @@ const ( | |
| poolFormat = poolPrefix + "%d_%s" | ||
| monitorPrefix = "monitor_" | ||
| monitorFormat = monitorPrefix + "%d_%s" | ||
|
|
||
| // clusterIDTagPrefix is the prefix used for the load balancer tag that | ||
| // carries a stable Kubernetes cluster identifier (the kube-system | ||
| // namespace UID). Together with the existing servicePrefix tag it allows | ||
| // OCCM to disambiguate load balancers when multiple Kubernetes clusters | ||
| // share the same OpenStack project and a service name happens to collide. | ||
| clusterIDTagPrefix = "kube_cluster_id_" | ||
| ) | ||
|
|
||
| // clusterIDTag formats the Octavia load balancer tag carrying the cluster | ||
| // identifier. It returns the empty string when uid is empty so callers can | ||
| // safely append the result without a separate nil-check. | ||
| func clusterIDTag(uid string) string { | ||
| if uid == "" { | ||
| return "" | ||
| } | ||
| return clusterIDTagPrefix + uid | ||
| } | ||
|
|
||
| // LbaasV2 is a LoadBalancer implementation based on Octavia | ||
| type LbaasV2 struct { | ||
| LoadBalancer | ||
|
|
@@ -163,8 +180,17 @@ type listenerKey struct { | |
| Port int | ||
| } | ||
|
|
||
| // getLoadbalancerByName get the load balancer which is in valid status by the given name/legacy name. | ||
| func getLoadbalancerByName(ctx context.Context, client *gophercloud.ServiceClient, name string, legacyName string) (*loadbalancers.LoadBalancer, error) { | ||
| // getLoadbalancerByName gets the load balancer which is in valid status by the given name/legacy name. | ||
| // | ||
| // When clusterUID is non-empty, the returned load balancer must either carry | ||
| // the matching clusterIDTagPrefix tag for that UID, or carry no clusterIDTagPrefix | ||
| // tag at all (legacy load balancer that pre-dates the tag). Load balancers | ||
| // whose name matches but that carry a different cluster-id tag belong to | ||
| // another Kubernetes cluster sharing the OpenStack project; they are ignored | ||
| // and the lookup returns ErrNotFound, which causes OCCM to create a new load | ||
| // balancer instead of accidentally adopting (and overwriting) one that is | ||
| // owned by a different cluster. | ||
| func getLoadbalancerByName(ctx context.Context, client *gophercloud.ServiceClient, name string, legacyName string, clusterUID string) (*loadbalancers.LoadBalancer, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes more sense to convert this function to a
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in b7c487e |
||
| var validLBs []loadbalancers.LoadBalancer | ||
|
|
||
| opts := loadbalancers.ListOpts{ | ||
|
|
@@ -197,10 +223,16 @@ func getLoadbalancerByName(ctx context.Context, client *gophercloud.ServiceClien | |
| } | ||
| } | ||
|
|
||
| validLBs, foreignFound := filterLoadBalancersByClusterID(validLBs, clusterUID) | ||
|
|
||
| if len(validLBs) > 1 { | ||
| return nil, cpoerrors.ErrMultipleResults | ||
| } | ||
| if len(validLBs) == 0 { | ||
| if foreignFound { | ||
| klog.Warningf("Found a load balancer named %q in OpenStack but it belongs to a different Kubernetes cluster "+ | ||
| "(no %s%s tag); ignoring it. A new load balancer will be created.", name, clusterIDTagPrefix, clusterUID) | ||
| } | ||
| return nil, cpoerrors.ErrNotFound | ||
| } | ||
|
|
||
|
|
@@ -244,6 +276,55 @@ func withLBNameTag(lbName, annotation string) []string { | |
| return cpoutil.Unique(append([]string{lbName}, userTags...)) | ||
| } | ||
|
|
||
| // filterLoadBalancersByClusterID returns the subset of lbs that may belong to | ||
| // the cluster identified by clusterUID. The selection rules are: | ||
| // | ||
| // - If clusterUID is empty, the filter is a no-op (the caller has no cluster | ||
| // identity to match on). | ||
| // - Load balancers carrying the matching clusterIDTagPrefix+clusterUID tag | ||
| // are kept (strongly owned by this cluster). | ||
| // - If none of the load balancers carries any clusterIDTagPrefix tag, all of | ||
| // them are kept. This preserves the legacy behaviour for load balancers | ||
| // created before the tag was introduced or by tools that don't set it. | ||
| // - Otherwise (every candidate carries a foreign clusterIDTagPrefix tag) all | ||
| // load balancers are dropped. The second return value is true in that case | ||
| // to let the caller emit a more specific log line / event. | ||
| func filterLoadBalancersByClusterID(lbs []loadbalancers.LoadBalancer, clusterUID string) ([]loadbalancers.LoadBalancer, bool) { | ||
| if clusterUID == "" || len(lbs) == 0 { | ||
| return lbs, false | ||
| } | ||
| wantTag := clusterIDTag(clusterUID) | ||
| var owned []loadbalancers.LoadBalancer | ||
| taggedAny := false | ||
| for _, lb := range lbs { | ||
| hasTag := false | ||
| match := false | ||
| for _, tag := range lb.Tags { | ||
| if strings.HasPrefix(tag, clusterIDTagPrefix) { | ||
| hasTag = true | ||
| if tag == wantTag { | ||
| match = true | ||
| } | ||
| } | ||
| } | ||
| if match { | ||
| owned = append(owned, lb) | ||
| } | ||
| if hasTag { | ||
| taggedAny = true | ||
| } | ||
| } | ||
| if len(owned) > 0 { | ||
| return owned, false | ||
| } | ||
| if !taggedAny { | ||
| // Legacy load balancers without any cluster-id tag, behave as before. | ||
| return lbs, false | ||
| } | ||
| // All candidates are tagged for some other cluster. | ||
| return nil, true | ||
| } | ||
|
|
||
| func popListener(existingListeners []listeners.Listener, id string) []listeners.Listener { | ||
| newListeners := []listeners.Listener{} | ||
| for _, existingListener := range existingListeners { | ||
|
|
@@ -283,6 +364,9 @@ func (lbaas *LbaasV2) createOctaviaLoadBalancer(ctx context.Context, name, clust | |
|
|
||
| if svcConf.supportLBTags { | ||
| createOpts.Tags = withLBNameTag(svcConf.lbName, svcConf.lbTags) | ||
| if tag := clusterIDTag(lbaas.clusterUID); tag != "" && !slices.Contains(createOpts.Tags, tag) { | ||
| createOpts.Tags = append(createOpts.Tags, tag) | ||
| } | ||
| } | ||
|
|
||
| if svcConf.flavorID != "" { | ||
|
|
@@ -386,7 +470,7 @@ func (lbaas *LbaasV2) GetLoadBalancer(ctx context.Context, clusterName string, s | |
| if lbID != "" { | ||
| loadbalancer, err = openstackutil.GetLoadbalancerByID(ctx, lbaas.lb, lbID) | ||
| } else { | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, name, legacyName) | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, name, legacyName, lbaas.clusterUID) | ||
| } | ||
| if err != nil && cpoerrors.IsNotFound(err) { | ||
| return nil, false, nil | ||
|
|
@@ -1805,7 +1889,7 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName | |
| } | ||
| } else { | ||
| legacyName := lbaas.getLoadBalancerLegacyName(service) | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, lbName, legacyName) | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, lbName, legacyName, lbaas.clusterUID) | ||
| if err != nil { | ||
| if err != cpoerrors.ErrNotFound { | ||
| return nil, fmt.Errorf("error getting loadbalancer for Service %s: %v", serviceName, err) | ||
|
|
@@ -1894,9 +1978,13 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName | |
| // save address into the annotation | ||
| lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerAddress, addr) | ||
|
|
||
| // Ensure the LB name tag plus any tags from the Service annotation in a single update. | ||
| // Ensure the LB name tag, the cluster-identity tag, plus any tags from the | ||
| // Service annotation in a single update. | ||
| if svcConf.supportLBTags { | ||
| lbTags := withLBNameTag(lbName, svcConf.lbTags) | ||
| if tag := clusterIDTag(lbaas.clusterUID); tag != "" && !slices.Contains(lbTags, tag) { | ||
|
kayrus marked this conversation as resolved.
Outdated
|
||
| lbTags = append(lbTags, tag) | ||
| } | ||
| klog.V(4).Infof("Desired load balancer tags: %v (LB name plus annotation %s)", lbTags, ServiceAnnotationLoadBalancerTags) | ||
| if tags, changed := cpoutil.Merge(loadbalancer.Tags, lbTags); changed { | ||
| klog.InfoS("Updating load balancer tags", "lbID", loadbalancer.ID, "tags", tags) | ||
|
|
@@ -1981,7 +2069,7 @@ func (lbaas *LbaasV2) updateOctaviaLoadBalancer(ctx context.Context, clusterName | |
| // This is a Service created before shared LB is supported. | ||
| name := lbaas.GetLoadBalancerName(ctx, clusterName, service) | ||
| legacyName := lbaas.getLoadBalancerLegacyName(service) | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, name, legacyName) | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, name, legacyName, lbaas.clusterUID) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
@@ -2169,7 +2257,7 @@ func (lbaas *LbaasV2) ensureLoadBalancerDeleted(ctx context.Context, clusterName | |
| loadbalancer, err = openstackutil.GetLoadbalancerByID(ctx, lbaas.lb, svcConf.lbID) | ||
| } else { | ||
| // This may happen when this Service creation was failed previously. | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, lbName, legacyName) | ||
| loadbalancer, err = getLoadbalancerByName(ctx, lbaas.lb, lbName, legacyName, lbaas.clusterUID) | ||
| } | ||
| if err != nil && !cpoerrors.IsNotFound(err) { | ||
| return err | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: avoid linking to issues in documentation. I found only one exception in the repo: this line references an OCCM issue because it describes a workaround for an unfixed bug rather than standard behavior. For typical use cases and resolved issues, documentation should stand alone without issue references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 2e2348c