This is an automated email from the ASF dual-hosted git repository.

squakez pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git


The following commit(s) were added to refs/heads/main by this push:
     new 8160ae4fd Fix #6912: Add Spring RabbitMQ KEDA scaler mapping (#6913)
8160ae4fd is described below

commit 8160ae4fda823ebafa91b034e9d14cc4ab2f15eb
Author: Bharat Kaurav <[email protected]>
AuthorDate: Fri Nov 13 19:55:56 2026 +0530

    Fix #6912: Add Spring RabbitMQ KEDA scaler mapping (#6913)
    
    * Fix #6912: Add Spring RabbitMQ KEDA scaler mapping
    
    * Fix #6912: Add e2e test for Spring RabbitMQ KEDA scaler
    
    Adapt the scaler to the Map signature that takes integration
    properties, and add a keda workflow test that runs an ephemeral
    RabbitMQ broker, checks the auto-discovered trigger, and verifies
    scale 0 -> 1 -> 0 under producer traffic.
    
    * Fix #6912: Run Spring RabbitMQ KEDA e2e test in a random namespace
    
    * Fix #6912: Discover the RabbitMQ host from the connection factory bean
    
    Read the camel.beans properties of the connection factory referenced by
    the spring-rabbitmq connectionFactory option and set the KEDA host from
    its uri, addresses or host/port/virtualHost. Credentials are not copied
    into the trigger.
    
    Also drop the e2e cloneAndReplaceNamespace copy now that main has one,
    and the replicas==1 check right after Ready, which raced with KEDA
    scaling the idle consumer to 0.
    
    * Fix #6912: Prefer a host endpoint parameter over the discovered broker 
address
    
    * Fix #6912: Drop host discovery from the connection factory bean
    
    Bean-based discovery is fragile and overlaps with #6942. Keep queueName
    from queues and an explicit host endpoint parameter; otherwise host,
    mode and value come from keda.autoMetadata.
---
 docs/modules/traits/pages/keda.adoc                |   1 +
 .../files/keda-it-spring-rabbitmq-to-log-auto.yaml |  55 ++++++++++
 e2e/keda/files/rabbitmq-ephemeral.yaml             |  67 ++++++++++++
 e2e/keda/files/timer-to-spring-rabbitmq.yaml       |  43 ++++++++
 e2e/keda/keda_spring_rabbitmq_test.go              |  87 +++++++++++++++
 pkg/trait/keda/scalers/spring_rabbitmq.go          |  51 +++++++++
 pkg/trait/keda/scalers/spring_rabbitmq_test.go     | 118 +++++++++++++++++++++
 7 files changed, 422 insertions(+)

diff --git a/docs/modules/traits/pages/keda.adoc 
b/docs/modules/traits/pages/keda.adoc
index da08c06c3..ba1e2628f 100644
--- a/docs/modules/traits/pages/keda.adoc
+++ b/docs/modules/traits/pages/keda.adoc
@@ -79,5 +79,6 @@ Automatic discovery currently supports the following Camel 
components:
 * `kafka`: creates an Apache Kafka trigger from the topic, broker, and 
consumer-group values in the endpoint URI. See the 
https://keda.sh/docs/latest/scalers/apache-kafka/[KEDA Apache Kafka scaler] 
documentation for the available trigger metadata.
 * `aws2-sqs`: creates an AWS SQS Queue trigger. `queueURL` comes from the 
`queueUrl` endpoint parameter, or is built from a queue ARN, or from the queue 
name with the `region` and `queueOwnerAWSAccountId` parameters. Otherwise the 
plain queue name is used, which KEDA accepts when it is unambiguous. 
`awsRegion` comes from `region` or the ARN, and `awsEndpoint` from 
`uriEndpointOverride` when `overrideEndpoint=true`. Set other metadata, such as 
`queueLength`, with `keda.autoMetadata`, and  [...]
 * `pulsar`: creates an Apache Pulsar trigger from the topic in the endpoint 
URI and the `subscriptionName` option (Camel's default `subs` when it is not 
set). When the service URL (`serviceUrl` option or 
`camel.component.pulsar.service-url` property) uses the `pulsar://` scheme, 
`adminURL` is set to `http://<host>:8080`; for TLS or a custom admin port, set 
it with `keda.autoMetadata`. See the 
https://keda.sh/docs/latest/scalers/pulsar/[KEDA Apache Pulsar scaler] 
documentation for the ava [...]
+* `spring-rabbitmq`: creates a RabbitMQ trigger whose `queueName` is the 
`queues` value of the endpoint URI, when it names a single queue. The broker 
address is not part of the URI: a `host` endpoint parameter, when present, is 
used as the trigger `host` as is; otherwise set `host`, together with `mode` 
and `value`, with `keda.autoMetadata`. See the 
https://keda.sh/docs/latest/scalers/rabbitmq-queue/[KEDA RabbitMQ Queue scaler] 
documentation for the available trigger metadata.
 
 Unsupported Camel components do not produce an automatically discovered 
trigger. You can still configure their KEDA triggers explicitly with 
`keda.triggers`.
\ No newline at end of file
diff --git a/e2e/keda/files/keda-it-spring-rabbitmq-to-log-auto.yaml 
b/e2e/keda/files/keda-it-spring-rabbitmq-to-log-auto.yaml
new file mode 100644
index 000000000..0cbf83df2
--- /dev/null
+++ b/e2e/keda/files/keda-it-spring-rabbitmq-to-log-auto.yaml
@@ -0,0 +1,55 @@
+# ---------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ---------------------------------------------------------------------------
+
+
+# Spring RabbitMQ trigger is auto-discovered from the consumer URI (queueName 
comes from `queues`).
+# The broker address is not part of the URI, so host, mode and value are 
merged via autoMetadata.
+
+apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+  name: keda-it-spring-rabbitmq-to-log-auto
+spec:
+  flows:
+  - beans:
+    - name: rabbitConnectionFactory
+      type: org.springframework.amqp.rabbit.connection.CachingConnectionFactory
+      properties:
+        host: my-rabbitmq.%%%.svc
+        port: 5672
+        username: admin
+        password: admin
+  - route:
+      from:
+        uri: spring-rabbitmq:my-exchange?queues=my-queue&routingKey=my-key
+        steps:
+        - to: log:info
+  traits:
+    camel:
+      properties:
+        - 
camel.component.spring-rabbitmq.connection-factory=#rabbitConnectionFactory
+    keda:
+      enabled: true
+      minReplicaCount: 0
+      maxReplicaCount: 1
+      cooldownPeriod: 20
+      pollingInterval: 10
+      autoMetadata:
+        rabbitmq:
+          host: amqp://admin:admin@my-rabbitmq.%%%.svc:5672/
+          mode: QueueLength
+          value: "5"
diff --git a/e2e/keda/files/rabbitmq-ephemeral.yaml 
b/e2e/keda/files/rabbitmq-ephemeral.yaml
new file mode 100644
index 000000000..a4cd3b8b8
--- /dev/null
+++ b/e2e/keda/files/rabbitmq-ephemeral.yaml
@@ -0,0 +1,67 @@
+# ---------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ---------------------------------------------------------------------------
+
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: my-rabbitmq
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: my-rabbitmq
+  template:
+    metadata:
+      labels:
+        app: my-rabbitmq
+    spec:
+      containers:
+        - name: rabbitmq
+          image: rabbitmq:4-alpine
+          env:
+            # The default guest user can only connect from localhost
+            - name: RABBITMQ_DEFAULT_USER
+              value: "admin"
+            - name: RABBITMQ_DEFAULT_PASS
+              value: "admin"
+          ports:
+            - containerPort: 5672
+              name: amqp
+          readinessProbe:
+            exec:
+              command: ["rabbitmq-diagnostics", "-q", 
"check_port_connectivity"]
+            initialDelaySeconds: 10
+            periodSeconds: 5
+          resources:
+            requests:
+              memory: "256Mi"
+              cpu: "200m"
+            limits:
+              memory: "512Mi"
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-rabbitmq
+spec:
+  selector:
+    app: my-rabbitmq
+  ports:
+    - name: amqp
+      port: 5672
+      targetPort: 5672
diff --git a/e2e/keda/files/timer-to-spring-rabbitmq.yaml 
b/e2e/keda/files/timer-to-spring-rabbitmq.yaml
new file mode 100644
index 000000000..c47c30c78
--- /dev/null
+++ b/e2e/keda/files/timer-to-spring-rabbitmq.yaml
@@ -0,0 +1,43 @@
+# ---------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ---------------------------------------------------------------------------
+
+
+apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+  name: timer-to-spring-rabbitmq
+spec:
+  flows:
+  - beans:
+    - name: rabbitConnectionFactory
+      type: org.springframework.amqp.rabbit.connection.CachingConnectionFactory
+      properties:
+        host: my-rabbitmq.%%%.svc
+        port: 5672
+        username: admin
+        password: admin
+  - route:
+      from:
+        uri: timer:foo?period=1000
+        steps:
+        - setBody:
+            constant: "Hello RabbitMQ"
+        - to: spring-rabbitmq:my-exchange?routingKey=my-key
+  traits:
+    camel:
+      properties:
+        - 
camel.component.spring-rabbitmq.connection-factory=#rabbitConnectionFactory
diff --git a/e2e/keda/keda_spring_rabbitmq_test.go 
b/e2e/keda/keda_spring_rabbitmq_test.go
new file mode 100644
index 000000000..569da5ab4
--- /dev/null
+++ b/e2e/keda/keda_spring_rabbitmq_test.go
@@ -0,0 +1,87 @@
+//go:build integration
+// +build integration
+
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keda
+
+import (
+       "context"
+       "testing"
+
+       . "github.com/onsi/gomega"
+       . "github.com/onsi/gomega/gstruct"
+       corev1 "k8s.io/api/core/v1"
+
+       . "github.com/apache/camel-k/v2/e2e/support"
+       v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+)
+
+func TestSpringRabbitMQKedaAutoscale(t *testing.T) {
+       WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) {
+               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"files/rabbitmq-ephemeral.yaml", "-n", ns))
+               ExpectExecSucceed(t, g, Kubectl("rollout", "status", 
"deployment", "my-rabbitmq", "-n", ns, "--timeout=180s"))
+
+               t.Run("Serverless Spring RabbitMQ Autoscale", func(t 
*testing.T) {
+                       consumerName := "keda-it-spring-rabbitmq-to-log-auto"
+                       producerName := "timer-to-spring-rabbitmq"
+
+                       consumerFile := cloneAndReplaceNamespace(t, 
"files/keda-it-spring-rabbitmq-to-log-auto.yaml", ns)
+                       ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
consumerFile, "-n", ns))
+
+                       // Trigger is auto-discovered from the consumer URI
+                       g.Eventually(ScaledObject(t, ctx, ns, consumerName), 
TestTimeoutMedium).
+                               ShouldNot(BeNil())
+                       scaledObj := ScaledObject(t, ctx, ns, consumerName)()
+                       g.Expect(scaledObj).NotTo(BeNil())
+                       g.Expect(scaledObj.Spec.Triggers).To(HaveLen(1))
+                       
g.Expect(scaledObj.Spec.Triggers[0].Type).To(Equal("rabbitmq"))
+                       
g.Expect(scaledObj.Spec.Triggers[0].Metadata["queueName"]).To(Equal("my-queue"))
+                       
g.Expect(scaledObj.Spec.Triggers[0].Metadata["mode"]).To(Equal("QueueLength"))
+
+                       // Start consumer; it declares the exchange, queue and 
binding
+                       g.Eventually(IntegrationConditionStatus(t, ctx, ns, 
consumerName, v1.IntegrationConditionReady), TestTimeoutLong).
+                               Should(Equal(corev1.ConditionTrue))
+
+                       // Scale down to 0 when idle
+                       g.Eventually(IntegrationStatusReplicas(t, ctx, ns, 
consumerName), TestTimeoutMedium).
+                               Should(PointTo(BeNumerically("==", 0)))
+
+                       // Start producer to generate traffic
+                       producerFile := cloneAndReplaceNamespace(t, 
"files/timer-to-spring-rabbitmq.yaml", ns)
+                       ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
producerFile, "-n", ns))
+                       g.Eventually(IntegrationConditionStatus(t, ctx, ns, 
producerName, v1.IntegrationConditionReady), TestTimeoutLong).
+                               Should(Equal(corev1.ConditionTrue))
+
+                       // Consumer scales up to 1
+                       g.Eventually(IntegrationStatusReplicas(t, ctx, ns, 
consumerName), TestTimeoutMedium).
+                               Should(PointTo(BeNumerically("==", 1)))
+
+                       // Verify messages are being received and processed
+                       g.Eventually(IntegrationLogs(t, ctx, ns, consumerName), 
TestTimeoutMedium).
+                               Should(ContainSubstring("Hello RabbitMQ"))
+
+                       // Stop producer
+                       ExpectExecSucceed(t, g, Kubectl("delete", "it", 
producerName, "-n", ns))
+
+                       // Scale back to 0
+                       g.Eventually(IntegrationStatusReplicas(t, ctx, ns, 
consumerName), TestTimeoutMedium).
+                               Should(PointTo(BeNumerically("==", 0)))
+               })
+       })
+}
diff --git a/pkg/trait/keda/scalers/spring_rabbitmq.go 
b/pkg/trait/keda/scalers/spring_rabbitmq.go
new file mode 100644
index 000000000..902df21a7
--- /dev/null
+++ b/pkg/trait/keda/scalers/spring_rabbitmq.go
@@ -0,0 +1,51 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scalers
+
+import (
+       "strings"
+
+       keda "github.com/apache/camel-k/v2/pkg/trait/keda"
+)
+
+// SpringRabbitMQScaler maps Camel Spring RabbitMQ consumer endpoints to the 
KEDA rabbitmq scaler.
+type SpringRabbitMQScaler struct{}
+
+func init() {
+       keda.Register(&SpringRabbitMQScaler{})
+}
+
+func (s *SpringRabbitMQScaler) Component() string {
+       return "spring-rabbitmq"
+}
+
+// Map takes the queue from the `queues` parameter. The URI path is the 
exchange name, which the
+// scaler does not use. The broker address is taken from a `host` parameter 
when the user sets one;
+// otherwise it has to be provided with keda.autoMetadata.
+func (s *SpringRabbitMQScaler) Map(pathValue string, params map[string]string, 
props map[string]string) (string, map[string]string) {
+       metadata := make(map[string]string)
+       // A KEDA rabbitmq trigger watches a single queue, so a list of queues 
is left for the user to choose.
+       if v, ok := params["queues"]; ok && v != "" && !strings.Contains(v, 
",") {
+               metadata["queueName"] = v
+       }
+       if v := params["host"]; v != "" {
+               metadata["host"] = v
+       }
+
+       return "rabbitmq", metadata
+}
diff --git a/pkg/trait/keda/scalers/spring_rabbitmq_test.go 
b/pkg/trait/keda/scalers/spring_rabbitmq_test.go
new file mode 100644
index 000000000..90159d0ec
--- /dev/null
+++ b/pkg/trait/keda/scalers/spring_rabbitmq_test.go
@@ -0,0 +1,118 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scalers
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+
+       keda "github.com/apache/camel-k/v2/pkg/trait/keda"
+)
+
+func TestSpringRabbitMQScalerRegistered(t *testing.T) {
+       mapper, found := keda.GetMapper("spring-rabbitmq")
+       require.True(t, found, "spring-rabbitmq scaler should be registered")
+       assert.Equal(t, "spring-rabbitmq", mapper.Component())
+}
+
+func TestSpringRabbitMQScalerMap(t *testing.T) {
+       scaler := &SpringRabbitMQScaler{}
+
+       tests := []struct {
+               name         string
+               pathValue    string
+               params       map[string]string
+               props        map[string]string
+               expectedType string
+               expectedMeta map[string]string
+       }{
+               {
+                       name:         "single queue",
+                       pathValue:    "orders-exchange",
+                       params:       map[string]string{"queues": "orders", 
"routingKey": "new", "autoDeclare": "true"},
+                       expectedType: "rabbitmq",
+                       expectedMeta: map[string]string{
+                               "queueName": "orders",
+                       },
+               },
+               {
+                       name:         "no queue",
+                       pathValue:    "orders-exchange",
+                       params:       map[string]string{},
+                       expectedType: "rabbitmq",
+                       expectedMeta: map[string]string{},
+               },
+               {
+                       name:         "empty queue",
+                       pathValue:    "default",
+                       params:       map[string]string{"queues": ""},
+                       expectedType: "rabbitmq",
+                       expectedMeta: map[string]string{},
+               },
+               {
+                       name:         "several queues",
+                       pathValue:    "orders-exchange",
+                       params:       map[string]string{"queues": 
"orders,invoices"},
+                       expectedType: "rabbitmq",
+                       expectedMeta: map[string]string{},
+               },
+               {
+                       name:         "host parameter",
+                       pathValue:    "orders-exchange",
+                       params:       map[string]string{"queues": "orders", 
"host": "amqp://user:pass@my-rabbitmq:5672/vh"},
+                       expectedType: "rabbitmq",
+                       expectedMeta: map[string]string{
+                               "queueName": "orders",
+                               "host":      
"amqp://user:pass@my-rabbitmq:5672/vh",
+                       },
+               },
+               {
+                       name:      "connection factory bean is not used for 
host",
+                       pathValue: "orders-exchange",
+                       params:    map[string]string{"queues": "orders"},
+                       props: map[string]string{
+                               
"camel.component.spring-rabbitmq.connection-factory": "#cf",
+                               "camel.beans.cf.host":                          
      "my-rabbitmq",
+                       },
+                       expectedType: "rabbitmq",
+                       expectedMeta: map[string]string{
+                               "queueName": "orders",
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       kedaType, metadata := scaler.Map(tt.pathValue, 
tt.params, tt.props)
+                       assert.Equal(t, tt.expectedType, kedaType)
+                       assert.Equal(t, tt.expectedMeta, metadata)
+               })
+       }
+}
+
+func TestMapToKedaTriggerSpringRabbitMQ(t *testing.T) {
+       trigger, err := 
keda.MapToKedaTrigger("spring-rabbitmq:orders-exchange?queues=orders&routingKey=new",
 nil)
+       require.NoError(t, err)
+       require.NotNil(t, trigger)
+       assert.Equal(t, "rabbitmq", trigger.Type)
+       assert.Equal(t, map[string]string{
+               "queueName": "orders",
+       }, trigger.Metadata)
+}

Reply via email to