This is an automated email from the ASF dual-hosted git repository. pcongiusti pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit a54f806e0a8693953c0651bd586734c4337a299f Author: Pranjul Kalsi <[email protected]> AuthorDate: Sun Dec 14 14:02:52 2025 +0530 feat(trait): Add KEDA auto-discovery for Camel component URIs --- docs/modules/ROOT/partials/apis/camel-k-crds.adoc | 12 +- docs/modules/traits/pages/keda.adoc | 9 +- .../files/keda-it-kafkatopic-to-log-auto.yaml | 17 +++ e2e/kafka/files/keda-kafka-auto-discovery.yaml | 37 +++++ e2e/kafka/kafka_autoscale_keda_test.go | 25 ++++ e2e/support/test_support.go | 18 +++ helm/camel-k/crds/camel-k-crds.yaml | 88 +++++++----- pkg/apis/camel/v1/trait/keda.go | 3 + pkg/apis/camel/v1/trait/zz_generated.deepcopy.go | 5 + .../camel.apache.org_integrationplatforms.yaml | 22 +-- .../camel.apache.org_integrationprofiles.yaml | 22 +-- .../crd/bases/camel.apache.org_integrations.yaml | 22 +-- .../config/crd/bases/camel.apache.org_pipes.yaml | 22 +-- pkg/trait/keda.go | 25 ++++ pkg/trait/keda_mapping.go | 125 ++++++++++++++++ pkg/trait/keda_mapping_test.go | 159 +++++++++++++++++++++ pkg/trait/keda_test.go | 126 ++++++++++++++++ 17 files changed, 669 insertions(+), 68 deletions(-) diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc index ef2d79834..ae0916c91 100644 --- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc +++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc @@ -8066,14 +8066,22 @@ int32 Maximum number of replicas. -|`triggers` + -*xref:#_camel_apache_org_v1_trait_KedaTrigger[[\]KedaTrigger]* +|`auto` + +bool | Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. +Automatically discover KEDA triggers from Camel component URIs. + +|`triggers` + +*xref:#_camel_apache_org_v1_trait_KedaTrigger[[\]KedaTrigger]* +| + + + |=== diff --git a/docs/modules/traits/pages/keda.adoc b/docs/modules/traits/pages/keda.adoc index f10a1987a..dba237792 100644 --- a/docs/modules/traits/pages/keda.adoc +++ b/docs/modules/traits/pages/keda.adoc @@ -47,11 +47,16 @@ The following configuration options are available: | int32 | Maximum number of replicas. -| keda.triggers -| []github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait.KedaTrigger +| keda.auto +| bool | Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. +Automatically discover KEDA triggers from Camel component URIs. + +| keda.triggers +| []github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait.KedaTrigger +| |=== diff --git a/e2e/kafka/files/keda-it-kafkatopic-to-log-auto.yaml b/e2e/kafka/files/keda-it-kafkatopic-to-log-auto.yaml new file mode 100644 index 000000000..018c93ec0 --- /dev/null +++ b/e2e/kafka/files/keda-it-kafkatopic-to-log-auto.yaml @@ -0,0 +1,17 @@ +apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + name: keda-kafka-auto-discovery + namespace: kafka +spec: + sources: + - content: | + from("kafka:my-topic?brokers=my-cluster-kafka-bootstrap.kafka.svc:9092&groupId=auto-group") + .log("${body}"); + name: routes.java + traits: + keda: + enabled: true + minReplicaCount: 0 + maxReplicaCount: 1 + # No triggers - auto-discovery should create kafka scaler \ No newline at end of file diff --git a/e2e/kafka/files/keda-kafka-auto-discovery.yaml b/e2e/kafka/files/keda-kafka-auto-discovery.yaml new file mode 100644 index 000000000..62e3f5661 --- /dev/null +++ b/e2e/kafka/files/keda-kafka-auto-discovery.yaml @@ -0,0 +1,37 @@ +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- +# This integration tests KEDA auto-discovery - no manual triggers are specified, +# the trait should automatically discover the kafka component and create a trigger. +apiVersion: camel.apache.org/v1 +kind: Integration +metadata: + name: keda-kafka-auto-discovery + namespace: kafka +spec: + sources: + - content: | + from("kafka:my-topic?brokers=my-cluster-kafka-bootstrap.kafka.svc:9092&groupId=auto-group") + .log("${body}"); + name: routes.java + traits: + keda: + enabled: true + minReplicaCount: 0 + maxReplicaCount: 1 + cooldownPeriod: 20 + pollingInterval: 10 + # No triggers specified - auto-discovery should create kafka scaler diff --git a/e2e/kafka/kafka_autoscale_keda_test.go b/e2e/kafka/kafka_autoscale_keda_test.go index 8beb38d47..58dca1365 100644 --- a/e2e/kafka/kafka_autoscale_keda_test.go +++ b/e2e/kafka/kafka_autoscale_keda_test.go @@ -73,3 +73,28 @@ func TestKafkaKedaAutoscale(t *testing.T) { }) }) } + +func TestKafkaKedaAutoDiscovery(t *testing.T) { + WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { + t.Run("Auto-discovery Kafka", func(t *testing.T) { + ExpectExecSucceed(t, g, Kubectl("apply", "-f", "files/keda-kafka-auto-discovery.yaml")) + ns := "kafka" + integrationName := "keda-kafka-auto-discovery" + + // Wait for ScaledObject + g.Eventually(ScaledObject(t, ctx, ns, integrationName), TestTimeoutMedium). + ShouldNot(BeNil()) + + // Verify the auto-discovered + scaledObj := ScaledObject(t, ctx, ns, integrationName)() + g.Expect(scaledObj).NotTo(BeNil()) + g.Expect(scaledObj.Spec.Triggers).To(HaveLen(1)) + g.Expect(scaledObj.Spec.Triggers[0].Type).To(Equal("kafka")) + g.Expect(scaledObj.Spec.Triggers[0].Metadata["topic"]).To(Equal("my-topic")) + g.Expect(scaledObj.Spec.Triggers[0].Metadata["bootstrapServers"]).To(Equal("my-cluster-kafka-bootstrap.kafka.svc:9092")) + g.Expect(scaledObj.Spec.Triggers[0].Metadata["consumerGroup"]).To(Equal("auto-group")) + + g.Expect(Kamel(t, ctx, "delete", integrationName, "-n", ns).Execute()).To(Succeed()) + }) + }) +} diff --git a/e2e/support/test_support.go b/e2e/support/test_support.go index dbacf8cd9..021b6b44a 100644 --- a/e2e/support/test_support.go +++ b/e2e/support/test_support.go @@ -75,6 +75,7 @@ import ( "github.com/apache/camel-k/v2/e2e/support/util" v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" + kedav1alpha1 "github.com/apache/camel-k/v2/pkg/apis/duck/keda/v1alpha1" "github.com/apache/camel-k/v2/pkg/client" "github.com/apache/camel-k/v2/pkg/cmd" "github.com/apache/camel-k/v2/pkg/platform" @@ -192,6 +193,7 @@ func init() { client.FastMapperAllowedAPIGroups["operators.coreos.com"] = true client.FastMapperAllowedAPIGroups["config.openshift.io"] = true client.FastMapperAllowedAPIGroups["policy"] = true + client.FastMapperAllowedAPIGroups["keda.sh"] = true var err error @@ -3110,3 +3112,19 @@ func DefaultOperatorSecurityContext() *corev1.SecurityContext { return &sc } + +// ScaledObject retrieves a KEDA ScaledObject by name from a namespace. +func ScaledObject(t *testing.T, ctx context.Context, ns, name string) func() *kedav1alpha1.ScaledObject { + return func() *kedav1alpha1.ScaledObject { + scaledObject := kedav1alpha1.ScaledObject{} + key := ctrl.ObjectKey{ + Namespace: ns, + Name: name, + } + if err := TestClient(t).Get(ctx, key, &scaledObject); err != nil { + log.Error(err, "Error while retrieving ScaledObject "+name) + return nil + } + return &scaledObject + } +} diff --git a/helm/camel-k/crds/camel-k-crds.yaml b/helm/camel-k/crds/camel-k-crds.yaml index 2ceed0549..0ad56a8db 100644 --- a/helm/camel-k/crds/camel-k-crds.yaml +++ b/helm/camel-k/crds/camel-k-crds.yaml @@ -4807,6 +4807,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -4840,10 +4847,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -7216,6 +7219,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -7249,10 +7259,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -9527,6 +9533,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -9560,10 +9573,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -11815,6 +11824,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -11848,10 +11864,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -20937,6 +20949,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -20970,10 +20989,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -23179,6 +23194,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -23212,10 +23234,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -33663,6 +33681,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -33697,10 +33722,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -35837,6 +35858,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -35870,10 +35898,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: diff --git a/pkg/apis/camel/v1/trait/keda.go b/pkg/apis/camel/v1/trait/keda.go index e1bcb8335..2d92a9432 100644 --- a/pkg/apis/camel/v1/trait/keda.go +++ b/pkg/apis/camel/v1/trait/keda.go @@ -36,6 +36,9 @@ type KedaTrait struct { // Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding // to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options // and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + // Automatically discover KEDA triggers from Camel component URIs. + // +kubebuilder:validation:Optional + Auto *bool `json:"auto,omitempty" property:"auto"` Triggers []KedaTrigger `json:"triggers,omitempty" property:"triggers"` } diff --git a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go index 10bb5627b..c194c65e3 100644 --- a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go @@ -731,6 +731,11 @@ func (in *KedaTrait) DeepCopyInto(out *KedaTrait) { *out = new(int32) **out = **in } + if in.Auto != nil { + in, out := &in.Auto, &out.Auto + *out = new(bool) + **out = **in + } if in.Triggers != nil { in, out := &in.Triggers, &out.Triggers *out = make([]KedaTrigger, len(*in)) diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml index 2a8131651..bb9b3ce73 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml @@ -1558,6 +1558,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -1591,10 +1598,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -3967,6 +3970,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -4000,10 +4010,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml index 924a60f7c..b0770c973 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml @@ -1426,6 +1426,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -1459,10 +1466,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -3714,6 +3717,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -3747,10 +3757,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml index 097df5642..3b9a080fd 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml @@ -8240,6 +8240,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -8273,10 +8280,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -10482,6 +10485,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -10515,10 +10525,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: diff --git a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml index 147c06e12..a9bc05167 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml @@ -8296,6 +8296,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -8330,10 +8337,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: @@ -10470,6 +10473,13 @@ spec: keda: description: The configuration of Keda trait properties: + auto: + description: |- + Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding + to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options + and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. + Automatically discover KEDA triggers from Camel component URIs. + type: boolean configuration: description: |- Legacy trait configuration parameters. @@ -10503,10 +10513,6 @@ spec: format: int32 type: integer triggers: - description: |- - Definition of triggers according to the KEDA format. Each trigger must contain `type` field corresponding - to the name of a KEDA autoscaler and a key/value map named `metadata` containing specific trigger options - and optionally a mapping of secrets, used by Keda operator to poll resources according to the autoscaler type. items: properties: metadata: diff --git a/pkg/trait/keda.go b/pkg/trait/keda.go index 9e4d27b41..0eb438019 100644 --- a/pkg/trait/keda.go +++ b/pkg/trait/keda.go @@ -24,6 +24,7 @@ import ( v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" "github.com/apache/camel-k/v2/pkg/apis/duck/keda/v1alpha1" + "github.com/apache/camel-k/v2/pkg/metadata" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -49,6 +50,12 @@ func (t *kedaTrait) Configure(e *Environment) (bool, *TraitCondition, error) { return false, nil, nil } + if ptr.Deref(t.Auto, true) && len(t.Triggers) == 0 { + if err := t.autoDiscoverTriggers(e); err != nil { + return false, nil, err + } + } + return len(t.Triggers) > 0, nil, nil } @@ -148,3 +155,21 @@ func (t *kedaTrait) getScaleTarget(it *v1.Integration) *corev1.ObjectReference { Name: it.Name, } } + +// Auto-discover triggers if Auto is enabled (default) and no manual triggers specified +func (t *kedaTrait) autoDiscoverTriggers(e *Environment) error { + meta, err := metadata.ExtractAll(e.CamelCatalog, e.Integration.AllSources()) + if err != nil { + return err + } + for _, fromURI := range meta.FromURIs { + trigger, err := mapToKedaTrigger(fromURI) + if err != nil { + return err + } + if trigger != nil { + t.Triggers = append(t.Triggers, *trigger) + } + } + return nil +} diff --git a/pkg/trait/keda_mapping.go b/pkg/trait/keda_mapping.go new file mode 100644 index 000000000..cc1c7ac0f --- /dev/null +++ b/pkg/trait/keda_mapping.go @@ -0,0 +1,125 @@ +/* +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 trait + +import ( + "net/url" + "strings" + + traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" + "github.com/apache/camel-k/v2/pkg/util/uri" +) + +type CamelToKedaMapping struct { + KedaScalerType string + // Maps Camel URI Param names to KEDA metadata keys. + ParameterMap map[string]string + // Params that are taken from URI path, (component:path?queryParams) + PathParamName string +} + +// camelToKedaMappings maps Camel component URI parameters to KEDA scaler metadata. +// Only components available in Camel-K catalog (pkg/resources/resources/camel-catalog-*.yaml) +// that have corresponding KEDA scalers are included. +var camelToKedaMappings = map[string]CamelToKedaMapping{ + "kafka": { + KedaScalerType: "kafka", + PathParamName: "topic", + ParameterMap: map[string]string{ + "brokers": "bootstrapServers", + "groupId": "consumerGroup", + }, + }, + "aws2-sqs": { + KedaScalerType: "aws-sqs-queue", + PathParamName: "queueURL", + ParameterMap: map[string]string{ + "region": "awsRegion", + }, + }, + "spring-rabbitmq": { + KedaScalerType: "rabbitmq", + PathParamName: "", + ParameterMap: map[string]string{ + "queues": "queueName", + "addresses": "host", + }, + }, +} + +// parseComponentURI extracts the component scheme, path value, and query parameters from a Camel URI. +func parseComponentURI(rawURI string) (string, string, map[string]string, error) { + scheme := uri.GetComponent(rawURI) + if scheme == "" { + return "", "", nil, nil + } + + params := make(map[string]string) + + // extract path + remainder := strings.TrimPrefix(rawURI, scheme+":") + var pathValue string + if idx := strings.Index(remainder, "?"); idx >= 0 { + pathValue = remainder[:idx] + queryString := remainder[idx+1:] + + values, parseErr := url.ParseQuery(queryString) + if parseErr != nil { + return "", "", nil, parseErr + } + for k, v := range values { + if len(v) > 0 { + params[k] = v[0] + } + } + } else { + pathValue = remainder + } + + return scheme, pathValue, params, nil +} + +// mapToKedaTrigger converts a Camel URI to a KEDA trigger if the component is supported. +func mapToKedaTrigger(rawURI string) (*traitv1.KedaTrigger, error) { + scheme, pathValue, params, err := parseComponentURI(rawURI) + if err != nil { + return nil, err + } + + mapping, found := camelToKedaMappings[scheme] + if scheme == "" || !found { + return nil, nil // no trigger for this URI + } + + metadata := make(map[string]string) + + if mapping.PathParamName != "" && pathValue != "" { + metadata[mapping.PathParamName] = pathValue + } + + for camelParam, kedaParam := range mapping.ParameterMap { + if val, ok := params[camelParam]; ok { + metadata[kedaParam] = val + } + } + + return &traitv1.KedaTrigger{ + Type: mapping.KedaScalerType, + Metadata: metadata, + }, nil +} diff --git a/pkg/trait/keda_mapping_test.go b/pkg/trait/keda_mapping_test.go new file mode 100644 index 000000000..b60956de5 --- /dev/null +++ b/pkg/trait/keda_mapping_test.go @@ -0,0 +1,159 @@ +/* +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 trait + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseComponentURI(t *testing.T) { + tests := []struct { + name string + uri string + scheme string + pathValue string + params map[string]string + }{ + { + name: "kafka with params", + uri: "kafka:orders?brokers=localhost:9092&groupId=myGroup", + scheme: "kafka", + pathValue: "orders", + params: map[string]string{"brokers": "localhost:9092", "groupId": "myGroup"}, + }, + { + name: "aws2-sqs", + uri: "aws2-sqs:myQueue?region=us-east-1", + scheme: "aws2-sqs", + pathValue: "myQueue", + params: map[string]string{"region": "us-east-1"}, + }, + { + name: "spring-rabbitmq", + uri: "spring-rabbitmq:exchange?queues=myQueue&addresses=localhost:5672", + scheme: "spring-rabbitmq", + pathValue: "exchange", + params: map[string]string{"queues": "myQueue", "addresses": "localhost:5672"}, + }, + { + name: "timer unsupported", + uri: "timer:tick?period=1000", + scheme: "timer", + pathValue: "tick", + params: map[string]string{"period": "1000"}, + }, + { + name: "no query params", + uri: "direct:start", + scheme: "direct", + pathValue: "start", + params: map[string]string{}, + }, + { + name: "empty uri", + uri: "", + scheme: "", + pathValue: "", + params: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme, pathValue, params, err := parseComponentURI(tt.uri) + require.NoError(t, err) + assert.Equal(t, tt.scheme, scheme) + assert.Equal(t, tt.pathValue, pathValue) + assert.Equal(t, tt.params, params) + }) + } +} + +func TestMapToKedaTrigger(t *testing.T) { + tests := []struct { + name string + uri string + expectNil bool + expectedType string + expectedMeta map[string]string + }{ + { + name: "kafka trigger", + uri: "kafka:orders?brokers=broker:9092&groupId=grp", + expectNil: false, + expectedType: "kafka", + expectedMeta: map[string]string{ + "topic": "orders", + "bootstrapServers": "broker:9092", + "consumerGroup": "grp", + }, + }, + { + name: "aws2-sqs trigger", + uri: "aws2-sqs:myQueue?region=us-east-1", + expectNil: false, + expectedType: "aws-sqs-queue", + expectedMeta: map[string]string{ + "queueURL": "myQueue", + "awsRegion": "us-east-1", + }, + }, + { + name: "spring-rabbitmq trigger", + uri: "spring-rabbitmq:exchange?queues=myQueue&addresses=localhost:5672", + expectNil: false, + expectedType: "rabbitmq", + expectedMeta: map[string]string{ + "queueName": "myQueue", + "host": "localhost:5672", + }, + }, + { + name: "timer no trigger", + uri: "timer:tick", + expectNil: true, + }, + { + name: "direct no trigger", + uri: "direct:start", + expectNil: true, + }, + { + name: "empty uri no trigger", + uri: "", + expectNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + trigger, err := mapToKedaTrigger(tt.uri) + require.NoError(t, err) + if tt.expectNil { + assert.Nil(t, trigger) + } else { + require.NotNil(t, trigger) + assert.Equal(t, tt.expectedType, trigger.Type) + assert.Equal(t, tt.expectedMeta, trigger.Metadata) + } + }) + } +} diff --git a/pkg/trait/keda_test.go b/pkg/trait/keda_test.go index 20c57a6f9..5def6174c 100644 --- a/pkg/trait/keda_test.go +++ b/pkg/trait/keda_test.go @@ -58,6 +58,66 @@ func TestKeda(t *testing.T) { assert.Equal(t, "10", scaledObject.Spec.Triggers[0].Metadata["lagThreshold"]) } +func TestKedaAutoDiscovery(t *testing.T) { + tests := []struct { + name string + source string + expectedType string + expectedParams map[string]string + }{ + { + name: "kafka", + source: `from("kafka:my-topic?brokers=my-broker:9092&groupId=my-group").log("${body}");`, + expectedType: "kafka", + expectedParams: map[string]string{ + "topic": "my-topic", + "bootstrapServers": "my-broker:9092", + "consumerGroup": "my-group", + }, + }, + { + name: "aws2-sqs", + source: `from("aws2-sqs:my-queue?region=us-east-1").log("${body}");`, + expectedType: "aws-sqs-queue", + expectedParams: map[string]string{ + "queueURL": "my-queue", + "awsRegion": "us-east-1", + }, + }, + { + name: "spring-rabbitmq", + source: `from("spring-rabbitmq:exchange?queues=my-queue&addresses=rabbit:5672").log("${body}");`, + expectedType: "rabbitmq", + expectedParams: map[string]string{ + "queueName": "my-queue", + "host": "rabbit:5672", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + environment := autoDiscoveryEnvWithSource(t, tt.source) + environment.Platform.ResyncStatusFullConfig() + traitCatalog := environment.Catalog + + _, _, err := traitCatalog.apply(&environment) + + require.NoError(t, err) + assert.NotEmpty(t, environment.ExecutedTraits) + assert.NotNil(t, environment.GetTrait("keda")) + + scaledObject := getKedaScaledObject(environment.Resources) + require.NotNil(t, scaledObject) + require.Len(t, scaledObject.Spec.Triggers, 1) + assert.Equal(t, tt.expectedType, scaledObject.Spec.Triggers[0].Type) + for k, v := range tt.expectedParams { + assert.Equal(t, v, scaledObject.Spec.Triggers[0].Metadata[k], "metadata key %s mismatch", k) + } + }) + } +} + func TestKedaAuthentication(t *testing.T) { environment := nominalEnv(t) environment.Integration.Spec.Traits.Keda.Triggers[0].Secrets = []*traitv1.KedaSecret{ @@ -208,3 +268,69 @@ func nominalEnv(t *testing.T) Environment { Resources: kubernetes.NewCollection(), } } + +// autoDiscoveryEnvWithSource creates an environment with the given source but NO manual triggers. +func autoDiscoveryEnvWithSource(t *testing.T, source string) Environment { + t.Helper() + catalog, err := camel.DefaultCatalog() + require.NoError(t, err) + + client, _ := internal.NewFakeClient() + traitCatalog := NewCatalog(nil) + + return Environment{ + CamelCatalog: catalog, + Catalog: traitCatalog, + Client: client, + Integration: &v1.Integration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "ns", + }, + Status: v1.IntegrationStatus{ + Phase: v1.IntegrationPhaseDeploying, + }, + Spec: v1.IntegrationSpec{ + Sources: []v1.SourceSpec{ + { + DataSpec: v1.DataSpec{ + Name: "routes.java", + Content: source, + Compression: false, + }, + Language: v1.LanguageJavaSource, + }, + }, + Traits: v1.Traits{ + Keda: &traitv1.KedaTrait{ + Trait: traitv1.Trait{ + Enabled: ptr.To(true), + }, + // No triggers - auto-discovery should kick in + }, + }, + }, + }, + IntegrationKit: &v1.IntegrationKit{ + Status: v1.IntegrationKitStatus{ + Phase: v1.IntegrationKitPhaseReady, + }, + }, + Platform: &v1.IntegrationPlatform{ + Spec: v1.IntegrationPlatformSpec{ + Cluster: v1.IntegrationPlatformClusterOpenShift, + Build: v1.IntegrationPlatformBuildSpec{ + PublishStrategy: v1.IntegrationPlatformBuildPublishStrategyJib, + Registry: v1.RegistrySpec{Address: "registry"}, + RuntimeVersion: catalog.Runtime.Version, + }, + }, + Status: v1.IntegrationPlatformStatus{ + Phase: v1.IntegrationPlatformPhaseReady, + }, + }, + EnvVars: make([]corev1.EnvVar, 0), + ExecutedTraits: make([]Trait, 0), + Resources: kubernetes.NewCollection(), + } +}
