Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ Request Body:

If this annotation is specified, the other annotations which define the load balancer features will be ignored.

- `loadbalancer.openstack.org/load-balancer-tags`

A comma-separated list of tags to add to the load balancer resource in addition to the tags managed by OCCM. Example: `env=prod,team=network`.

- `loadbalancer.openstack.org/listener-tags`

A comma-separated list of tags to add to the load balancer listener resources in addition to the tags managed by OCCM. Example: `env=prod,team=network`.

- `loadbalancer.openstack.org/pool-tags`

A comma-separated list of tags to add to the load balancer pool resources. Example: `env=prod,team=network`.

- `loadbalancer.openstack.org/hostname`

This annotations explicitly sets a hostname in the status of the load balancer service.
Expand Down
85 changes: 77 additions & 8 deletions pkg/openstack/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ const (
defaultProxyHostnameSuffix = "nip.io"
ServiceAnnotationLoadBalancerID = "loadbalancer.openstack.org/load-balancer-id"

// ServiceAnnotationLoadBalancerTags is used to set additional tags on the loadbalancer resource itself (comma-separated list).
ServiceAnnotationLoadBalancerTags = "loadbalancer.openstack.org/load-balancer-tags"
// ServiceAnnotationListenerTags is used to set additional tags on the loadbalancer listener resources (comma-separated list).
ServiceAnnotationListenerTags = "loadbalancer.openstack.org/listener-tags"
// ServiceAnnotationPoolTags is used to set additional tags on the loadbalancer pool resources (comma-separated list).
ServiceAnnotationPoolTags = "loadbalancer.openstack.org/pool-tags"

// Octavia resources name formats
servicePrefix = "kube_service_"
lbFormat = "%s%s_%s_%s"
Expand Down Expand Up @@ -146,6 +153,9 @@ type serviceConfig struct {
healthMonitorMaxRetries int
healthMonitorMaxRetriesDown int
preferredIPFamily corev1.IPFamily // preferred (the first) IP family indicated in service's `spec.ipFamilies`
lbTags string
listenerTags string
poolTags string
}

type listenerKey struct {
Expand Down Expand Up @@ -197,6 +207,20 @@ func getLoadbalancerByName(ctx context.Context, client *gophercloud.ServiceClien
return &validLBs[0], nil
}

// mergeTags merges existedTags and desiredTags, returns true if all desiredTags are already in existedTags.
func mergeTags(existedTags []string, desiredTags []string) (bool, []string) {
if len(existedTags) == 0 {
return false, desiredTags
}

tagSet := sets.NewString(existedTags...)
if tagSet.HasAll(desiredTags...) {
return true, nil
}

return false, tagSet.Union(sets.NewString(desiredTags...)).List()
}

func popListener(existingListeners []listeners.Listener, id string) []listeners.Listener {
newListeners := []listeners.Listener{}
for _, existingListener := range existingListeners {
Expand Down Expand Up @@ -235,7 +259,8 @@ func (lbaas *LbaasV2) createOctaviaLoadBalancer(ctx context.Context, name, clust
}

if svcConf.supportLBTags {
createOpts.Tags = []string{svcConf.lbName}
desiredTags := cpoutil.SplitTrim(svcConf.lbTags, ',')
Comment thread
alexanderstephan marked this conversation as resolved.
Outdated
createOpts.Tags = append([]string{svcConf.lbName}, desiredTags...)
}

if svcConf.flavorID != "" {
Expand Down Expand Up @@ -923,16 +948,34 @@ func (lbaas *LbaasV2) ensureOctaviaPool(ctx context.Context, lbID string, name s
}
}

desiredTags := cpoutil.SplitTrim(svcConf.poolTags, ',')

if pool == nil {
createOpt := lbaas.buildPoolCreateOpt(listener.Protocol, service, svcConf, name)
createOpt.ListenerID = listener.ID

if svcConf.supportLBTags {
createOpt.Tags = desiredTags
}
klog.InfoS("Creating pool", "listenerID", listener.ID, "protocol", createOpt.Protocol)
klog.V(4).Infof("Pool create options: %+v", createOpt)
pool, err = openstackutil.CreatePool(ctx, lbaas.lb, createOpt, lbID)
if err != nil {
return nil, err
}
klog.V(2).Infof("Pool %s created for listener %s", pool.ID, listener.ID)
} else if svcConf.supportLBTags && len(desiredTags) > 0 {
Comment thread
alexanderstephan marked this conversation as resolved.
Outdated
// Update tags if needed
klog.V(4).Infof("Desired pool tags: %+v from service annotation key: %s", desiredTags, ServiceAnnotationPoolTags)
if ok, tags := mergeTags(pool.Tags, desiredTags); !ok {
klog.V(4).Infof("Will update pool tags, current pool tags: %+v, desired tags: %+v", pool.Tags, tags)
updateOpts := v2pools.UpdateOpts{
Tags: &tags,
}
klog.InfoS("Updating pool tags", "poolID", pool.ID, "listenerID", listener.ID, "lbID", lbID, "tags", tags)
if err := openstackutil.UpdatePool(ctx, lbaas.lb, lbID, pool.ID, updateOpts); err != nil {
klog.Warningf("Error updating LB pool tags: %v", err)
}
}
}

if lbaas.opts.ProviderRequiresSerialAPICalls {
Expand Down Expand Up @@ -1099,11 +1142,12 @@ func (lbaas *LbaasV2) ensureOctaviaListener(ctx context.Context, lbID string, na
updateOpts := listeners.UpdateOpts{}

if svcConf.supportLBTags {
if !slices.Contains(listener.Tags, svcConf.lbName) {
var newTags []string
copy(newTags, listener.Tags)
newTags = append(newTags, svcConf.lbName)
updateOpts.Tags = &newTags
// Ensure the LB name tag plus any desired tags from the Service annotation.
desiredTags := cpoutil.SplitTrim(svcConf.listenerTags, ',')
tagsToEnsure := append([]string{svcConf.lbName}, desiredTags...)
if ok, tags := mergeTags(listener.Tags, tagsToEnsure); !ok {
klog.V(4).Infof("Will update listener tags, current listener tags: %+v, desired tags: %+v", listener.Tags, tags)
updateOpts.Tags = &tags
listenerChanged = true
}
}
Expand Down Expand Up @@ -1177,7 +1221,13 @@ func (lbaas *LbaasV2) buildListenerCreateOpt(ctx context.Context, port corev1.Se
}

if svcConf.supportLBTags {
listenerCreateOpt.Tags = []string{svcConf.lbName}
// The LB name is always tagged, plus any desired tags from the Service annotation.
desiredTags := cpoutil.SplitTrim(svcConf.listenerTags, ',')
tags := []string{svcConf.lbName}
if _, merged := mergeTags(tags, desiredTags); merged != nil {
Comment thread
alexanderstephan marked this conversation as resolved.
Outdated
tags = merged
}
listenerCreateOpt.Tags = tags
}

if openstackutil.IsOctaviaFeatureSupported(ctx, lbaas.lb, openstackutil.OctaviaFeatureTimeout, lbaas.opts.LBProvider) {
Expand Down Expand Up @@ -1365,6 +1415,11 @@ func (lbaas *LbaasV2) checkService(ctx context.Context, service *corev1.Service,
return fmt.Errorf("no service ports provided")
}

annotations := service.GetAnnotations()
svcConf.lbTags = annotations[ServiceAnnotationLoadBalancerTags]
svcConf.listenerTags = annotations[ServiceAnnotationListenerTags]
svcConf.poolTags = annotations[ServiceAnnotationPoolTags]

if len(service.Spec.IPFamilies) > 0 {
// Since OCCM does not support multiple load-balancers per service yet,
// the first IP family will determine the IP family of the load-balancer
Expand Down Expand Up @@ -1756,6 +1811,20 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName
// Make sure LB ID will be saved at this point.
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerID, loadbalancer.ID)

if svcConf.supportLBTags {
Comment thread
alexanderstephan marked this conversation as resolved.
Outdated
desiredTags := cpoutil.SplitTrim(svcConf.lbTags, ',')
// Add the Service annotation tags to the load balancer tags if they don't match.
if len(desiredTags) > 0 {
klog.V(4).Infof("Desired load balancer tags: %v from service annotation: %v", desiredTags, ServiceAnnotationLoadBalancerTags)
if ok, tags := mergeTags(loadbalancer.Tags, desiredTags); !ok {
klog.Infof("Will update load balancer tags, current load balancer tags: %+v, desired tags: %+v", loadbalancer.Tags, tags)
if err := openstackutil.UpdateLoadBalancerTags(ctx, lbaas.lb, loadbalancer.ID, tags); err != nil {
klog.Warningf("failed to update load balancer %s tags: %v", loadbalancer.ID, err)
}
}
}
}

if loadbalancer.ProvisioningStatus != activeStatus {
return nil, fmt.Errorf("load balancer %s is not ACTIVE, current provisioning status: %s", loadbalancer.ID, loadbalancer.ProvisioningStatus)
}
Expand Down
93 changes: 93 additions & 0 deletions pkg/openstack/loadbalancer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,60 @@ type testGetRulesToCreateAndDelete struct {
toDelete []rules.SecGroupRule
}

func TestMergeTags(t *testing.T) {
testCases := []struct {
name string
existedTags []string
desiredTags []string
expectedOK bool
expectedTags []string
}{
{
name: "nil existing tags returns desired tags",
existedTags: nil,
desiredTags: []string{"a", "b"},
expectedOK: false,
expectedTags: []string{"a", "b"},
},
{
name: "empty existing tags returns desired tags",
existedTags: []string{},
desiredTags: []string{"a"},
expectedOK: false,
expectedTags: []string{"a"},
},
{
name: "all desired tags already present",
existedTags: []string{"a", "b", "c"},
desiredTags: []string{"a", "b"},
expectedOK: true,
expectedTags: nil,
},
{
name: "some desired tags missing are merged and sorted",
existedTags: []string{"b", "d"},
desiredTags: []string{"a", "c"},
expectedOK: false,
expectedTags: []string{"a", "b", "c", "d"},
},
{
name: "empty desired tags with existing tags is a no-op",
existedTags: []string{"a"},
desiredTags: []string{},
expectedOK: true,
expectedTags: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ok, tags := mergeTags(tc.existedTags, tc.desiredTags)
assert.Equal(t, tc.expectedOK, ok)
assert.Equal(t, tc.expectedTags, tags)
})
}
}

func TestGetRulesToCreateAndDelete(t *testing.T) {
tests := []testGetRulesToCreateAndDelete{
{
Expand Down Expand Up @@ -2485,6 +2539,45 @@ func TestBuildListenerCreateOpt(t *testing.T) {
Tags: nil,
},
},
{
name: "Test with LB tags support and no listener-tags annotation",
port: corev1.ServicePort{
Protocol: "TCP",
Port: 80,
},
svcConf: &serviceConfig{
connLimit: 100,
lbName: "my-lb",
supportLBTags: true,
},
expectedCreateOpt: listeners.CreateOpts{
Name: "Test with LB tags support and no listener-tags annotation",
Protocol: listeners.ProtocolTCP,
ProtocolPort: 80,
ConnLimit: &svcConf.connLimit,
Tags: []string{"my-lb"},
},
},
{
name: "Test with LB tags support and listener-tags annotation",
port: corev1.ServicePort{
Protocol: "TCP",
Port: 80,
},
svcConf: &serviceConfig{
connLimit: 100,
lbName: "my-lb",
supportLBTags: true,
listenerTags: "foo, bar",
},
expectedCreateOpt: listeners.CreateOpts{
Name: "Test with LB tags support and listener-tags annotation",
Protocol: listeners.ProtocolTCP,
ProtocolPort: 80,
ConnLimit: &svcConf.connLimit,
Tags: []string{"bar", "foo", "my-lb"},
},
},
}

for _, tc := range testCases {
Expand Down