Skip to content

Commit ea89e68

Browse files
committed
feat(occm/lb): octavia prometheus listener's annotation
1 parent 75b1fbb commit ea89e68

File tree

7 files changed

+388
-16
lines changed

7 files changed

+388
-16
lines changed

docs/openstack-cloud-controller-manager/expose-applications-using-loadbalancer-type-service.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,20 @@ Request Body:
198198

199199
Defines the health monitor retry count for the loadbalancer pool members to be marked down.
200200

201+
- `loadbalancer.openstack.org/metrics-enable`
202+
203+
If 'true', enable the Prometheus listener on the loadbalancer. (default: 'false')
204+
205+
Not supported when `lb-provider=ovn` is configured in openstack-cloud-controller-manager.
206+
207+
- `loadbalancer.openstack.org/metrics-port`
208+
209+
Defines the Prometheus listener's port. If `metric-enable` is 'true', the annotation is automatically added to the service. Default: `9100`
210+
211+
- `loadbalancer.openstack.org/metrics-allow-cidrs`
212+
213+
Defines the Prometheus listener's allowed cirds. __Warning__: [security recommendations](#metric-listener-allowed-cird-security-recommendation). Default: none
214+
201215
- `loadbalancer.openstack.org/flavor-id`
202216

203217
The id of the flavor that is used for creating the loadbalancer.
@@ -236,6 +250,10 @@ Request Body:
236250
This annotation is automatically added and it contains the floating ip address of the load balancer service.
237251
When using `loadbalancer.openstack.org/hostname` annotation it is the only place to see the real address of the load balancer.
238252

253+
- `loadbalancer.openstack.org/load-balancer-vip-address`
254+
255+
This annotation is automatically added and it contains the Octavia's Virtual-IP (VIP).
256+
239257
- `loadbalancer.openstack.org/node-selector`
240258

241259
A set of key=value annotations used to filter nodes for targeting by the load balancer. When defined, only nodes that match all the specified key=value annotations will be targeted. If an annotation includes only a key without a value, the filter will check only for the existence of the key on the node. If the value is not set, the `node-selector` value defined in the OCCM configuration is applied.
@@ -628,3 +646,66 @@ is not yet supported by OCCM.
628646
Internally, OCCM would automatically look for IPv4 or IPv6 subnet to allocate the load balancer
629647
address from based on the service's address family preference. If the subnet with preferred
630648
address family is not available, load balancer can not be created.
649+
650+
### Metric endpoint configuration
651+
652+
Since Octavia v2.25, Octavia proposes to expose an HTTP Prometheus endpoint. Using the annotation `loadbalancer.openstack.org/metrics-enable`, you will be able to configure this endpoint on the LoadBalancer:
653+
654+
```yaml
655+
kind: Service
656+
apiVersion: v1
657+
metadata:
658+
name: service-with-metric
659+
namespace: default
660+
annotations:
661+
loadbalancer.openstack.org/metrics-enable: "true" # Enable the listener endpoint on the Octavia LoadBalancer (default false)
662+
loadbalancer.openstack.org/metrics-port: "9100" # Listener's port (default 9100)
663+
loadbalancer.openstack.org/metrics-allow-cidrs: "10.0.0.0/8, fe80::/10" # Listener's allowed cidrs (default none)
664+
spec:
665+
type: LoadBalancer
666+
```
667+
668+
Then, you can configure a Prometheus scrapper like to get metrics from the LoadBalancer.
669+
670+
e.g. Prometheus Operator configuration:
671+
672+
```yaml
673+
apiVersion: monitoring.coreos.com/v1alpha1
674+
kind: ScrapeConfig
675+
metadata:
676+
name: octavia-sd-config
677+
labels:
678+
release: prometheus # adapt it to your Prometheus deployment configuration
679+
spec:
680+
kubernetesSDConfigs:
681+
- role: Service
682+
relabelings:
683+
- sourceLabels: [__meta_kubernetes_namespace]
684+
targetLabel: namespace
685+
action: replace
686+
- sourceLabels: [__meta_kubernetes_service_name]
687+
targetLabel: job
688+
action: replace
689+
- sourceLabels:
690+
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_load_balancer_vip_address
691+
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_metrics_port
692+
separator: ":"
693+
targetLabel: __address__
694+
action: replace
695+
- sourceLabels:
696+
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_metrics_enable
697+
- __meta_kubernetes_service_annotationpresent_loadbalancer_openstack_org_load_balancer_vip_address
698+
separator: ;
699+
regex: "true;true"
700+
action: keep
701+
```
702+
703+
> This configuration use the `loadbalancer.openstack.org/load-balancer-vip-address` annotation that will use the Octavia's VIP to fetch the metric endpoint. Adapt it to your Octavia deployment.
704+
705+
For more information: https://docs.openstack.org/octavia/latest/user/guides/monitoring.html#monitoring-with-prometheus
706+
707+
Grafana dashboard for Octavia Amphora: https://grafana.com/grafana/dashboards/15828-openstack-octavia-amphora-load-balancer/
708+
709+
#### Metric listener allowed CIRD security recommendation
710+
711+
If the Octavia LoadBalancer is exposed with a public IP, the Prometheus listener is also exposed (at least for Amphora). Even if no critical data are exposed by this endpoint, __it's strongly recommended to apply an allowed cidrs on the listener__ via the annotation `loadbalancer.openstack.org/metrics-allow-cidrs`.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/spf13/viper v1.15.0
2222
github.com/stretchr/testify v1.8.4
2323
go.uber.org/goleak v1.3.0
24+
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
2425
golang.org/x/sys v0.21.0
2526
golang.org/x/term v0.21.0
2627
google.golang.org/grpc v1.58.3
@@ -138,7 +139,6 @@ require (
138139
go.uber.org/multierr v1.11.0 // indirect
139140
go.uber.org/zap v1.26.0 // indirect
140141
golang.org/x/crypto v0.24.0 // indirect
141-
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
142142
golang.org/x/net v0.25.0 // indirect
143143
golang.org/x/oauth2 v0.10.0 // indirect
144144
golang.org/x/sync v0.7.0 // indirect

pkg/openstack/events.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ const (
2323
eventLBAZIgnored = "LoadBalancerAvailabilityZonesIgnored"
2424
eventLBFloatingIPSkipped = "LoadBalancerFloatingIPSkipped"
2525
eventLBRename = "LoadBalancerRename"
26+
eventLBMetricListenerIgnored = "LoadBalancerMetricListenerIgnored"
2627
)

pkg/openstack/loadbalancer.go

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const (
7171
ServiceAnnotationLoadBalancerSubnetID = "loadbalancer.openstack.org/subnet-id"
7272
ServiceAnnotationLoadBalancerNetworkID = "loadbalancer.openstack.org/network-id"
7373
ServiceAnnotationLoadBalancerMemberSubnetID = "loadbalancer.openstack.org/member-subnet-id"
74+
ServiceAnnotationLoadBalancerMetricsEnabled = "loadbalancer.openstack.org/metrics-enable"
75+
ServiceAnnotationLoadBalancerMetricsPort = "loadbalancer.openstack.org/metrics-port"
76+
ServiceAnnotationLoadBalancerMetricsAllowCidrs = "loadbalancer.openstack.org/metrics-allow-cidrs"
7477
ServiceAnnotationLoadBalancerTimeoutClientData = "loadbalancer.openstack.org/timeout-client-data"
7578
ServiceAnnotationLoadBalancerTimeoutMemberConnect = "loadbalancer.openstack.org/timeout-member-connect"
7679
ServiceAnnotationLoadBalancerTimeoutMemberData = "loadbalancer.openstack.org/timeout-member-data"
@@ -87,6 +90,7 @@ const (
8790
ServiceAnnotationLoadBalancerHealthMonitorMaxRetriesDown = "loadbalancer.openstack.org/health-monitor-max-retries-down"
8891
ServiceAnnotationLoadBalancerLoadbalancerHostname = "loadbalancer.openstack.org/hostname"
8992
ServiceAnnotationLoadBalancerAddress = "loadbalancer.openstack.org/load-balancer-address"
93+
ServiceAnnotationLoadBalancerVIPAddress = "loadbalancer.openstack.org/load-balancer-vip-address"
9094
// revive:disable:var-naming
9195
ServiceAnnotationTlsContainerRef = "loadbalancer.openstack.org/default-tls-container-ref"
9296
// revive:enable:var-naming
@@ -95,14 +99,15 @@ const (
9599
ServiceAnnotationLoadBalancerID = "loadbalancer.openstack.org/load-balancer-id"
96100

97101
// Octavia resources name formats
98-
servicePrefix = "kube_service_"
99-
lbFormat = "%s%s_%s_%s"
100-
listenerPrefix = "listener_"
101-
listenerFormat = listenerPrefix + "%d_%s"
102-
poolPrefix = "pool_"
103-
poolFormat = poolPrefix + "%d_%s"
104-
monitorPrefix = "monitor_"
105-
monitorFormat = monitorPrefix + "%d_%s"
102+
servicePrefix = "kube_service_"
103+
lbFormat = "%s%s_%s_%s"
104+
listenerPrefix = "listener_"
105+
listenerFormat = listenerPrefix + "%d_%s"
106+
listenerFormatMetric = listenerPrefix + "metric_%s"
107+
poolPrefix = "pool_"
108+
poolFormat = poolPrefix + "%d_%s"
109+
monitorPrefix = "monitor_"
110+
monitorFormat = monitorPrefix + "%d_%s"
106111
)
107112

108113
// LbaasV2 is a LoadBalancer implementation based on Octavia
@@ -142,6 +147,9 @@ type serviceConfig struct {
142147
healthMonitorTimeout int
143148
healthMonitorMaxRetries int
144149
healthMonitorMaxRetriesDown int
150+
metricAllowedCIDRs []string
151+
metricEnabled bool
152+
metricPort int
145153
preferredIPFamily corev1.IPFamily // preferred (the first) IP family indicated in service's `spec.ipFamilies`
146154
}
147155

@@ -451,6 +459,32 @@ func getIntFromServiceAnnotation(service *corev1.Service, annotationKey string,
451459
return defaultSetting
452460
}
453461

462+
// getStringArrayFromServiceAnnotationSeparatedByComma searches a given v1.Service for a specific annotationKey
463+
// and either returns the annotation's string array value (using comma as separator), or the specified defaultSetting.
464+
// Each value of the array is TrimSpaced. After the trim, if the string is empty, remove it.
465+
func getStringArrayFromServiceAnnotationSeparatedByComma(service *corev1.Service, annotationKey string, defaultSetting []string) []string {
466+
klog.V(4).Infof("getStringArrayFromServiceAnnotationSeparatedByComma(%s/%s, %v, %q)", service.Namespace, service.Name, annotationKey, defaultSetting)
467+
if annotationValue, ok := service.Annotations[annotationKey]; ok {
468+
returnValue := []string{}
469+
splitAnnotation := strings.FieldsFunc( // avoid empty string by using this func
470+
annotationValue, func(r rune) bool {
471+
return r == ','
472+
},
473+
)
474+
for _, value := range splitAnnotation {
475+
trimmedValue := strings.TrimSpace(value)
476+
if len(trimmedValue) == 0 {
477+
continue
478+
}
479+
returnValue = append(returnValue, trimmedValue)
480+
}
481+
klog.V(4).Infof("Found a Service Annotation: %v = %q", annotationKey, returnValue)
482+
return returnValue
483+
}
484+
klog.V(4).Infof("Could not find a Service Annotation; falling back to default setting: %v = %q", annotationKey, defaultSetting)
485+
return defaultSetting
486+
}
487+
454488
// getBoolFromServiceAnnotation searches a given v1.Service for a specific annotationKey and either returns the annotation's boolean value or a specified defaultSetting
455489
// If the annotation is not found or is not a valid boolean ("true" or "false"), it falls back to the defaultSetting and logs a message accordingly.
456490
func getBoolFromServiceAnnotation(service *corev1.Service, annotationKey string, defaultSetting bool) bool {
@@ -1122,6 +1156,58 @@ func (lbaas *LbaasV2) ensureOctaviaListener(lbID string, name string, curListene
11221156
return listener, nil
11231157
}
11241158

1159+
func (lbaas *LbaasV2) ensurePrometheusListener(lbID string, name string, curListenerMapping map[listenerKey]*listeners.Listener, _ corev1.ServicePort, svcConf *serviceConfig, _ *corev1.Service) (*listeners.Listener, error) {
1160+
listener, isPresent := curListenerMapping[listenerKey{
1161+
Protocol: listeners.ProtocolPrometheus,
1162+
Port: svcConf.metricPort,
1163+
}]
1164+
if !isPresent {
1165+
listenerCreateOpt := listeners.CreateOpts{
1166+
Name: name,
1167+
Protocol: listeners.ProtocolPrometheus,
1168+
ProtocolPort: svcConf.metricPort,
1169+
AllowedCIDRs: svcConf.metricAllowedCIDRs,
1170+
LoadbalancerID: lbID,
1171+
Tags: []string{svcConf.lbName},
1172+
}
1173+
1174+
var err error
1175+
listener, err = openstackutil.CreateListener(lbaas.lb, lbID, listenerCreateOpt)
1176+
if err != nil {
1177+
return nil, fmt.Errorf("failed to create metric listener for loadbalancer %s: %v", lbID, err)
1178+
}
1179+
1180+
klog.V(2).Infof("Metric listener %s created for loadbalancer %s", listener.ID, lbID)
1181+
} else {
1182+
listenerChanged := false
1183+
updateOpts := listeners.UpdateOpts{}
1184+
1185+
if svcConf.supportLBTags {
1186+
if !cpoutil.Contains(listener.Tags, svcConf.lbName) {
1187+
var newTags []string
1188+
copy(newTags, listener.Tags)
1189+
newTags = append(newTags, svcConf.lbName)
1190+
updateOpts.Tags = &newTags
1191+
listenerChanged = true
1192+
}
1193+
}
1194+
1195+
if !cpoutil.StringListEqual(svcConf.metricAllowedCIDRs, listener.AllowedCIDRs) {
1196+
updateOpts.AllowedCIDRs = &svcConf.metricAllowedCIDRs
1197+
listenerChanged = true
1198+
}
1199+
1200+
if listenerChanged {
1201+
klog.InfoS("Updating metric listener", "listenerID", listener.ID, "lbID", lbID, "updateOpts", updateOpts)
1202+
if err := openstackutil.UpdateListener(lbaas.lb, lbID, listener.ID, updateOpts); err != nil {
1203+
return nil, fmt.Errorf("failed to update metric listener %s of loadbalancer %s: %v", listener.ID, lbID, err)
1204+
}
1205+
klog.InfoS("Updated metric listener", "listenerID", listener.ID, "lbID", lbID)
1206+
}
1207+
}
1208+
return listener, nil
1209+
}
1210+
11251211
// buildListenerCreateOpt returns listeners.CreateOpts for a specific Service port and configuration
11261212
func (lbaas *LbaasV2) buildListenerCreateOpt(port corev1.ServicePort, svcConf *serviceConfig, name string) listeners.CreateOpts {
11271213
listenerCreateOpt := listeners.CreateOpts{
@@ -1787,6 +1873,25 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName
17871873
curListeners = popListener(curListeners, listener.ID)
17881874
}
17891875

1876+
// Check if we need to expose the metric endpoint
1877+
svcConf.metricEnabled = getBoolFromServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsEnabled, false)
1878+
if svcConf.metricEnabled && openstackutil.IsOctaviaFeatureSupported(lbaas.lb, openstackutil.OctaviaFeaturePrometheusListener, lbaas.opts.LBProvider) {
1879+
// Only a LB owner can add the prometheus listener (to avoid conflict with a shared loadbalancer)
1880+
if isLBOwner {
1881+
svcConf.metricPort = getIntFromServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsPort, 9100)
1882+
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsPort, strconv.Itoa(svcConf.metricPort))
1883+
svcConf.metricAllowedCIDRs = getStringArrayFromServiceAnnotationSeparatedByComma(service, ServiceAnnotationLoadBalancerMetricsAllowCidrs, []string{})
1884+
listener, err := lbaas.ensurePrometheusListener(loadbalancer.ID, cpoutil.Sprintf255(listenerFormatMetric, lbName), curListenerMapping, corev1.ServicePort{}, svcConf, service)
1885+
if err != nil {
1886+
return nil, err
1887+
}
1888+
curListeners = popListener(curListeners, listener.ID)
1889+
} else {
1890+
msg := "Metric Listener cannot be deployed on Service %s, only owner Service can do that"
1891+
lbaas.eventRecorder.Eventf(service, corev1.EventTypeWarning, eventLBMetricListenerIgnored, msg, serviceName)
1892+
klog.Infof(msg, serviceName)
1893+
}
1894+
}
17901895
// Deal with the remaining listeners, delete the listener if it was created by this Service previously.
17911896
if err := lbaas.deleteOctaviaListeners(loadbalancer.ID, curListeners, isLBOwner, lbName); err != nil {
17921897
return nil, err
@@ -1806,8 +1911,9 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName
18061911
}
18071912
}
18081913

1809-
// save address into the annotation
1914+
// save addresses into the annotations
18101915
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerAddress, addr)
1916+
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerVIPAddress, loadbalancer.VipAddress)
18111917

18121918
// add LB name to load balancer tags.
18131919
if svcConf.supportLBTags {

0 commit comments

Comments
 (0)