From 4250ad0a4264b798d4d73894f342ebdb3fa07ddc Mon Sep 17 00:00:00 2001 From: Juan Larriba Date: Wed, 28 Oct 2026 14:23:56 +0100 Subject: [PATCH] [OSPRH-39942] Fix telemetry route overrides applied to the wrong routes Select Aodh services by their service specific label, matching the approach used for Ceilometer and CloudKitty. This keeps the Aodh expose step from matching CloudKitty services. Default the CloudKitty route HAProxy timeout from its apiTimeout, while preserving manually set timeouts. The logic lives in telemetry-operator's CloudKittySpecCore.SetDefaultRouteAnnotations(), called from the webhook the same way as the Aodh one. Signed-off-by: Juan Larriba --- .../v1beta1/openstackcontrolplane_webhook.go | 2 + internal/openstack/telemetry.go | 2 +- .../openstackoperator_controller_test.go | 131 ++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/api/core/v1beta1/openstackcontrolplane_webhook.go b/api/core/v1beta1/openstackcontrolplane_webhook.go index ab575cff63..f3952e4cf1 100644 --- a/api/core/v1beta1/openstackcontrolplane_webhook.go +++ b/api/core/v1beta1/openstackcontrolplane_webhook.go @@ -1151,6 +1151,8 @@ func (r *OpenStackControlPlane) DefaultServices() { r.Spec.Telemetry.Template.Default() initializeOverrideSpec(&r.Spec.Telemetry.AodhAPIOverride.Route, true) r.Spec.Telemetry.Template.Autoscaling.SetDefaultRouteAnnotations(r.Spec.Telemetry.AodhAPIOverride.Route.Annotations) + initializeOverrideSpec(&r.Spec.Telemetry.CloudKittyAPIOverride.Route, true) + r.Spec.Telemetry.Template.CloudKitty.SetDefaultRouteAnnotations(r.Spec.Telemetry.CloudKittyAPIOverride.Route.Annotations) } // Heat diff --git a/internal/openstack/telemetry.go b/internal/openstack/telemetry.go index b641732a7c..4aad02ea60 100644 --- a/internal/openstack/telemetry.go +++ b/internal/openstack/telemetry.go @@ -306,7 +306,7 @@ func ReconcileTelemetry(ctx context.Context, instance *corev1beta1.OpenStackCont ctx, helper, instance.Namespace, - GetServiceOpenStackOperatorLabel(telemetry.Name), + map[string]string{common.AppSelector: "aodh"}, ) if err != nil { return ctrl.Result{}, err diff --git a/test/functional/ctlplane/openstackoperator_controller_test.go b/test/functional/ctlplane/openstackoperator_controller_test.go index dc0bd501a7..d03a5462b9 100644 --- a/test/functional/ctlplane/openstackoperator_controller_test.go +++ b/test/functional/ctlplane/openstackoperator_controller_test.go @@ -2662,6 +2662,137 @@ var _ = Describe("OpenStackOperator controller", func() { }) }) + When("A telemetry OpenStackControlplane instance is created with cloudkitty enabled and autoscaling disabled", func() { + BeforeEach(func() { + spec := GetDefaultOpenStackControlPlaneSpec() + spec["telemetry"] = map[string]interface{}{ + "enabled": true, + "template": map[string]interface{}{ + "ceilometer": map[string]interface{}{ + "enabled": false, + }, + "metricStorage": map[string]interface{}{ + "enabled": false, + }, + "logging": map[string]interface{}{ + "enabled": false, + }, + "autoscaling": map[string]interface{}{ + "enabled": false, + }, + "cloudkitty": map[string]interface{}{ + "enabled": true, + "apiTimeout": 300, + }, + }, + } + + // create cert secrets for rabbitmq instances + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) + // create cert secrets for memcached instance + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) + // create cert secrets for ovn instance + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNNorthdCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNControllerCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNMetricsCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.InstanceHAMetricsCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.NeutronOVNCertName)) + // create cert secrets for cloudkitty + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(types.NamespacedName{Name: "cert-cloudkitty-public-route", Namespace: namespace})) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(types.NamespacedName{Name: "cert-cloudkitty-public-svc", Namespace: namespace})) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(types.NamespacedName{Name: "cert-cloudkitty-internal-svc", Namespace: namespace})) + + DeferCleanup( + th.DeleteInstance, + CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec), + ) + }) + + It("should default the cloudkitty route timeout from the cloudkitty apiTimeout", func() { + OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + Expect(OSCtlplane.Spec.Telemetry.CloudKittyAPIOverride.Route).Should(Not(BeNil())) + Expect(OSCtlplane.Spec.Telemetry.CloudKittyAPIOverride.Route.Annotations).Should(HaveKeyWithValue("haproxy.router.openshift.io/timeout", "300s")) + Expect(OSCtlplane.Spec.Telemetry.CloudKittyAPIOverride.Route.Annotations).Should(HaveKeyWithValue("api.cloudkitty.openstack.org/timeout", "300s")) + // aodh route defaults are independent of the cloudkitty ones + Expect(OSCtlplane.Spec.Telemetry.AodhAPIOverride.Route).Should(Not(BeNil())) + Expect(OSCtlplane.Spec.Telemetry.AodhAPIOverride.Route.Annotations).Should(HaveKeyWithValue("haproxy.router.openshift.io/timeout", "60s")) + Expect(OSCtlplane.Spec.Telemetry.AodhAPIOverride.Route.Annotations).Should(HaveKeyWithValue("api.aodh.openstack.org/timeout", "60s")) + }) + + It("should apply the cloudKittyApiOverride and not the aodhApiOverride to the cloudkitty route", func() { + cloudKittyPublicSvcName := types.NamespacedName{Name: "cloudkitty-public", Namespace: namespace} + cloudKittyInternalSvcName := types.NamespacedName{Name: "cloudkitty-internal", Namespace: namespace} + // labels as set by the telemetry-operator on the cloudkitty + // services, including the osctlplane-service label the + // openstack-operator injects via the service override + cloudKittySvcLabels := map[string]string{ + "service": "cloudkitty", + "osctlplane-service": "telemetry", + "osctlplane": "", + } + + th.CreateService( + cloudKittyPublicSvcName, + cloudKittySvcLabels, + k8s_corev1.ServiceSpec{ + Ports: []k8s_corev1.ServicePort{ + { + Name: "cloudkitty-public", + Port: int32(8889), + Protocol: k8s_corev1.ProtocolTCP, + }, + }, + }) + cloudKittySvc := th.GetService(cloudKittyPublicSvcName) + if cloudKittySvc.Annotations == nil { + cloudKittySvc.Annotations = map[string]string{} + } + cloudKittySvc.Annotations[service.AnnotationIngressCreateKey] = "true" + cloudKittySvc.Annotations[service.AnnotationEndpointKey] = "public" + Expect(th.K8sClient.Status().Update(th.Ctx, cloudKittySvc)).To(Succeed()) + + th.CreateService( + cloudKittyInternalSvcName, + cloudKittySvcLabels, + k8s_corev1.ServiceSpec{ + Ports: []k8s_corev1.ServicePort{ + { + Name: "cloudkitty-internal", + Port: int32(8889), + Protocol: k8s_corev1.ProtocolTCP, + }, + }, + }) + cloudKittyIntSvc := th.GetService(cloudKittyInternalSvcName) + if cloudKittyIntSvc.Annotations == nil { + cloudKittyIntSvc.Annotations = map[string]string{} + } + cloudKittyIntSvc.Annotations[service.AnnotationIngressCreateKey] = "false" + cloudKittyIntSvc.Annotations[service.AnnotationEndpointKey] = "internal" + Expect(th.K8sClient.Status().Update(th.Ctx, cloudKittyIntSvc)).To(Succeed()) + + Eventually(func(g Gomega) { + cloudKittyRoute := &routev1.Route{} + g.Expect(th.K8sClient.Get(th.Ctx, cloudKittyPublicSvcName, cloudKittyRoute)).Should(Succeed()) + g.Expect(cloudKittyRoute.Annotations).Should(HaveKeyWithValue("haproxy.router.openshift.io/timeout", "300s")) + g.Expect(cloudKittyRoute.Annotations).Should(HaveKeyWithValue("api.cloudkitty.openstack.org/timeout", "300s")) + g.Expect(cloudKittyRoute.Annotations).ShouldNot(HaveKey("api.aodh.openstack.org/timeout")) + }, timeout, interval).Should(Succeed()) + + // the cloudkitty endpoint got exposed, but the aodh service + // overrides must not be populated from the cloudkitty services + Eventually(func(g Gomega) { + telemetry := GetTelemetry(names.TelemetryName) + g.Expect(telemetry.Spec.CloudKitty.CloudKittyAPI.Override.Service[service.EndpointPublic].EndpointURL).Should(Not(BeNil())) + for _, svcOverride := range telemetry.Spec.Autoscaling.Aodh.Override.Service { + g.Expect(svcOverride.EndpointURL).Should(BeNil()) + } + }, timeout, interval).Should(Succeed()) + }) + }) + When("OpenStackControlplane instance is deleted", func() { BeforeEach(func() { DeferCleanup(