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 6037f1df57df124b361682821f4e2453d2ad6597 Author: Pasquale Congiusti <[email protected]> AuthorDate: Sat Oct 18 09:17:43 2025 +0200 feat(trait): kamelets from dependencies * Enable usage of Kamelets coming from classpath dependencies * Deprecate github kamelets repo Closes #5846 --- docs/modules/ROOT/pages/kamelets/distribution.adoc | 36 +++---- e2e/common/misc/kamelet_test.go | 120 --------------------- e2e/common/misc/kamelet_update_test.go | 76 ------------- e2e/common/traits/kamelet_test.go | 18 ++++ pkg/apis/camel/v1/integrationplatform_types.go | 1 + pkg/controller/integrationplatform/monitor.go | 16 +++ pkg/kamelet/repository/empty_repository.go | 1 + pkg/kamelet/repository/github_repository.go | 1 + pkg/trait/kamelets.go | 42 ++++---- pkg/trait/kamelets_test.go | 22 ++-- 10 files changed, 78 insertions(+), 255 deletions(-) diff --git a/docs/modules/ROOT/pages/kamelets/distribution.adoc b/docs/modules/ROOT/pages/kamelets/distribution.adoc index deeadee93..8565c7756 100644 --- a/docs/modules/ROOT/pages/kamelets/distribution.adoc +++ b/docs/modules/ROOT/pages/kamelets/distribution.adoc @@ -1,38 +1,26 @@ [[kamelets-distribution]] = Kamelets Distribution -When you install Camel K, you typically got bundled a series of Kamelets which are coming from the xref:camel-kamelets::index.adoc[Apache Kamelet Catalog]. This is a facility that will let you immediately use a wide set of connector-style resources to interact with any event source and sink. - -NOTE: the version we bundle depends directly on the default Camel version used. For instance, in Camel K version 2, the default runtime we use is Camel K Runtime 2.16.0 which depends on Camel version 3.20.1. The Kamelet version we use is the one distributed as link:https://github.com/apache/camel-kamelets/releases/tag/v3.20.1.1[v3.20.1]. - -As development of Kamelet is very fast, make sure to use some version which is compatible with the Camel runtime you're going to use. - -== Release Kamelets on the cluster - -Since Kamelets are Kubernetes custom resources, the Camel K opinionated way of Kamelets distribution is to expect them available on the cluster. You tipically develop a Kamelet and then release it in the Integration namespace or the operator namespace. Alternatively you can also deliver them into any other namespace. +The Camel K opinionated way of Kamelets distribution is to expect them available on the cluster. You typically develop a Kamelet and then release it in the Integration namespace or the operator namespace. Alternatively you can also deliver them into any other namespace. There is not a prescribed way how to release the Kamelets on the cluster. It can vary based on each company process. What's important for the operator, is that they are available when running the Integrations. -[[kamelets-own-catalog]] -== Provide your own catalog +[[kamelets-catalog]] +== Apache Kamelets catalog -An alternative to the catalog we bundle is to include a repository containing your own catalog. You need to specify it in the IntegrationPlatform `.spec.kamelet.repository`. - -```yaml - spec: - kamelet: - repositories: - - uri: <git-platform>:<owner>/<repo>[/path_to_kamelets_folder][@version] -``` -Mind that `<git-platform>` is the repository of your choice, for instance `github` and `[@version]` is the tag to use (by default it will use `main` branch). +When you install Camel K, you typically got bundled a series of Kamelets which are coming from the xref:camel-kamelets::index.adoc[Apache Kamelet Catalog]. This is a facility that will let you immediately use a wide set of connector-style resources to interact with any event source and sink. -With this approach you can dynamically include any repository where your Kamelets are hosted. They will be lazily initialized as soon as they are required by any of the Integration or Pipes which will make use of them. +NOTE: the version we bundle depends directly on the default Camel version used. To avoid potential breaking compatibility issues, Apache Kamelets catalog bundling is deprecated and may disappear in future releases [[kamelets-as-dependency]] == Kamelets as a dependency -The Camel K has an opinionated way to use Kamelets which is the usage of cluster custom resources. Here the Kamelet spec resource is expected to be available in the cluster. +You may find situations where you want to bundle a Kamelet in a dependency (ie, one or more external catalog containing the Kamelets spec) and use the same approach of distributing Kamelets as done in Camel core. As Kamelets are a Camel thing, then, you can use such dependency and let the runtime use the Kamelets available in the classpath. As an example, you will be able to run: + +```bash +kamel bind my-source log-sink -d github:squakez/acme-kamelets-catalog -d camel:timer +``` -However, you may find situations where you want to bundle a Kamelet in a dependency (ie, some external catalog containing all Kamelets spec). As Kamelets are a Camel thing, then, you can use such dependency and let the runtime use the Kamelets available in the classpath. +If you use this approach you will need to provide the Integration all the dependencies used in your Kamelet spec as the operator is not (yet) able to scan the Kamelet spec. -If you're using such an approach, then, you will need to make sure to skip the Kamelet trait (which is in charge to discover them and get required dependencies), and provide all the dependencies which may be required by your Kamelet. Additionally, you may need to specify a Camel property to tell the runtime where to expect to find the Kamelets, `camel.component.kamelet.location` (default `classpath:/kamelets`). +NOTE: this dependency limitation is likely to disappear in future versions. diff --git a/e2e/common/misc/kamelet_test.go b/e2e/common/misc/kamelet_test.go deleted file mode 100644 index 7dc1e2a20..000000000 --- a/e2e/common/misc/kamelet_test.go +++ /dev/null @@ -1,120 +0,0 @@ -//go:build integration -// +build integration - -// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "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 common - -import ( - "context" - "testing" - - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - - . "github.com/apache/camel-k/v2/e2e/support" -) - -func TestKameletClasspathLoading(t *testing.T) { - t.Parallel() - WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { - // Store a configmap on the cluster - var cmData = make(map[string]string) - cmData["my-timer-source.kamelet.yaml"] = ` -# --------------------------------------------------------------------------- -# 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: Kamelet -metadata: - name: my-timer-source - annotations: - camel.apache.org/kamelet.support.level: "Preview" - camel.apache.org/catalog.version: "0.3.0" - camel.apache.org/kamelet.icon: data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gU3ZnIFZlY3RvciBJY29ucyA6IGh0dHA6Ly93d3cub25saW5ld2ViZm9udHMuY29tL2ljb24gLS0+DQo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm9 [...] - camel.apache.org/provider: "Apache Software Foundation" - camel.apache.org/kamelet.group: "Timer" - labels: - camel.apache.org/kamelet.type: source - camel.apache.org/kamelet.verified: "true" -spec: - definition: - title: Timer Source - description: Produces periodic events with a custom payload. - required: - - message - type: object - properties: - period: - title: Period - description: The interval between two events in milliseconds - type: integer - default: 1000 - message: - title: Message - description: The message to generate - type: string - example: hello world - contentType: - title: Content Type - description: The content type of the message being generated - type: string - default: text/plain - dependencies: - - "camel:core" - - "camel:timer" - - "camel:kamelet" - template: - from: - uri: timer:tick - parameters: - period: "{{period}}" - steps: - - setBody: - constant: "{{message}}" - - setHeader: - name: "Content-Type" - constant: "{{contentType}}" - - to: kamelet:sink -` - CreatePlainTextConfigmap(t, ctx, ns, "my-kamelet-cm", cmData) - - // Basic - t.Run("test basic case", func(t *testing.T) { - g.Expect(KamelRun(t, ctx, ns, "files/TimerKameletIntegration.java", "-t", "kamelets.enabled=false", "--resource", "configmap:my-kamelet-cm@/kamelets", "-p camel.component.kamelet.location=file:/kamelets", "-d", "camel:yaml-dsl", "-d", "camel:timer").Execute()).To(Succeed()) - g.Eventually(IntegrationPodPhase(t, ctx, ns, "timer-kamelet-integration"), TestTimeoutLong).Should(Equal(corev1.PodRunning)) - g.Eventually(IntegrationLogs(t, ctx, ns, "timer-kamelet-integration")).Should(ContainSubstring("important message")) - }) - }) -} diff --git a/e2e/common/misc/kamelet_update_test.go b/e2e/common/misc/kamelet_update_test.go deleted file mode 100644 index f9e45fc00..000000000 --- a/e2e/common/misc/kamelet_update_test.go +++ /dev/null @@ -1,76 +0,0 @@ -//go:build integration -// +build integration - -// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "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 common - -import ( - "context" - "testing" - "time" - - . "github.com/onsi/gomega" - - . "github.com/apache/camel-k/v2/e2e/support" - v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" -) - -const customLabel = "custom-label" - -func TestBundleKameletUpdate(t *testing.T) { - t.Parallel() - WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { - g.Expect(createBundleKamelet(t, ctx, ns, "my-http-sink")()).To(Succeed()) // Going to be replaced - g.Expect(createUserKamelet(t, ctx, ns, "user-sink")()).To(Succeed()) // Left intact by the operator - - g.Eventually(Kamelet(t, ctx, "my-http-sink", ns)). - Should(WithTransform(KameletLabels, HaveKeyWithValue(customLabel, "true"))) - g.Consistently(Kamelet(t, ctx, "user-sink", ns), 5*time.Second, 1*time.Second). - Should(WithTransform(KameletLabels, HaveKeyWithValue(customLabel, "true"))) - }) -} - -func createBundleKamelet(t *testing.T, ctx context.Context, ns string, name string) func() error { - flow := map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "kamelet:source", - }, - } - - labels := map[string]string{ - customLabel: "true", - v1.KameletBundledLabel: "true", - } - return CreateKamelet(t, ctx, ns, name, flow, nil, labels) -} - -func createUserKamelet(t *testing.T, ctx context.Context, ns string, name string) func() error { - flow := map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "kamelet:source", - }, - } - - labels := map[string]string{ - customLabel: "true", - } - return CreateKamelet(t, ctx, ns, name, flow, nil, labels) -} diff --git a/e2e/common/traits/kamelet_test.go b/e2e/common/traits/kamelet_test.go index d96ce6215..1dedf9c7e 100644 --- a/e2e/common/traits/kamelet_test.go +++ b/e2e/common/traits/kamelet_test.go @@ -107,6 +107,24 @@ func TestKameletNamespaced(t *testing.T) { }) } +func TestKameletExternalCatalog(t *testing.T) { + t.Parallel() + WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { + t.Run("external kamelet catalog", func(t *testing.T) { + name := RandomizedSuffixName("external-catalog") + g.Expect(KamelBind(t, ctx, ns, + "my-source", "log-sink", + // The Kamelet spec (my-source) is provided into this catalog + "-d", "github:squakez/acme-kamelets-catalog", + "-d", "camel:timer", + "--name", name).Execute()).To(Succeed()) + g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutMedium).Should(Equal(corev1.ConditionTrue)) + g.Eventually(IntegrationPodPhase(t, ctx, ns, name), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + g.Eventually(IntegrationLogs(t, ctx, ns, name), TestTimeoutShort).Should(ContainSubstring("ACME")) + }) + }) +} + // cloneAndReplaceNamespace clones and replace the content marked as %%% with the namespace passed as parameter. func cloneAndReplaceNamespace(t *testing.T, srcPath, namespace string) string { t.Helper() diff --git a/pkg/apis/camel/v1/integrationplatform_types.go b/pkg/apis/camel/v1/integrationplatform_types.go index 6e5c1ca55..97209eb86 100644 --- a/pkg/apis/camel/v1/integrationplatform_types.go +++ b/pkg/apis/camel/v1/integrationplatform_types.go @@ -143,6 +143,7 @@ type IntegrationPlatformBuildSpec struct { } // IntegrationPlatformKameletSpec define the behavior for all the Kamelets controller by the IntegrationPlatform. +// Deprecated: to be removed in future versions. type IntegrationPlatformKameletSpec struct { // remote repository used to retrieve Kamelet catalog Repositories []KameletRepositorySpec `json:"repositories,omitempty"` diff --git a/pkg/controller/integrationplatform/monitor.go b/pkg/controller/integrationplatform/monitor.go index bd3c245de..3c29f6e99 100644 --- a/pkg/controller/integrationplatform/monitor.go +++ b/pkg/controller/integrationplatform/monitor.go @@ -128,6 +128,7 @@ func (action *monitorAction) Handle(ctx context.Context, platform *v1.Integratio } action.checkTraitAnnotationsDeprecatedNotice(platform) action.checkMavenSettings(platform) + action.checkKameletsCatalogRepoDeprecatedNotice(platform) if err = action.addPlainQuarkusCatalog(ctx, catalog); err != nil { // Only warn the user, we don't want to fail action.L.Infof( @@ -160,6 +161,21 @@ func (action *monitorAction) checkTraitAnnotationsDeprecatedNotice(platform *v1. } } +// Deprecated: to be removed in future versions, when we won't support any longer Kamelets catalog into IntegrationPlatforms. +func (action *monitorAction) checkKameletsCatalogRepoDeprecatedNotice(platform *v1.IntegrationPlatform) { + if platform.Status.Kamelet.Repositories != nil { + platform.Status.SetCondition( + v1.IntegrationPlatformConditionType("KameletsCatalogRepositoryDeprecated"), + corev1.ConditionTrue, + "DeprecationNotice", + "Kamelets Catalog Repository is deprecated and will be removed soon. Bundle Kamelets into a Maven dependency instead.", + ) + action.L.Infof( + "Kamelets Catalog Repository is deprecated and will be removed soon from IntegrationPlatform configuration. " + + "Bundle Kamelets into a Maven dependency instead") + } +} + func (action *monitorAction) checkMavenSettings(platform *v1.IntegrationPlatform) { if platform.Status.Build.Maven.Settings.ConfigMapKeyRef != nil || platform.Status.Build.Maven.Settings.SecretKeyRef != nil { diff --git a/pkg/kamelet/repository/empty_repository.go b/pkg/kamelet/repository/empty_repository.go index d71c88a66..cfb1e3176 100644 --- a/pkg/kamelet/repository/empty_repository.go +++ b/pkg/kamelet/repository/empty_repository.go @@ -23,6 +23,7 @@ import ( v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" ) +// Deprecated: to be removed in the future. type emptyKameletRepository struct { } diff --git a/pkg/kamelet/repository/github_repository.go b/pkg/kamelet/repository/github_repository.go index 0e38e1236..652027b88 100644 --- a/pkg/kamelet/repository/github_repository.go +++ b/pkg/kamelet/repository/github_repository.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" ) +// Deprecated: to be removed in the future. type githubKameletRepository struct { httpClient *http.Client owner string diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go index 091788677..860dd5bd8 100644 --- a/pkg/trait/kamelets.go +++ b/pkg/trait/kamelets.go @@ -18,7 +18,6 @@ limitations under the License. package trait import ( - "errors" "fmt" "net/url" "path/filepath" @@ -151,29 +150,11 @@ func (t *kameletsTrait) collectKamelets(e *Environment) (map[string]*v1.Kamelet, sort.Strings(missingKamelets) sort.Strings(bundledKamelets) - if len(missingKamelets) > 0 { - message := fmt.Sprintf("kamelets [%s] not found in %s repositories", - strings.Join(missingKamelets, ","), - repo.String()) - e.Integration.Status.SetCondition( - v1.IntegrationConditionKameletsAvailable, - corev1.ConditionFalse, - v1.IntegrationConditionKameletsAvailableReason, - message, - ) - - return nil, errors.New(message) - } - - // TODO: // We list the Kamelets coming from a bundle. We want to warn the user - // that in the future we'll use the specification coming from the dependency runtime - // instead of using the one installed in the cluster. - // It may be a good idea in the future to let the user specify the catalog dependency to use - // in order to override the one coming from Apache catalog + // that in the future we won't use any longer bundled Kamelets. if len(bundledKamelets) > 0 { - message := fmt.Sprintf("using bundled kamelets [%s]: make sure the Kamelet specifications is compatible with this Integration runtime."+ - " This feature is deprecated as in the future we will use directly the specification coming from the Kamelet catalog dependency jar.", + message := fmt.Sprintf("using bundled Kamelets [%s]: make sure the Kamelet specifications is compatible with this Integration runtime."+ + " This feature is deprecated as in the future we will use directly the Kamelets provided in the dependencies provided.", strings.Join(bundledKamelets, ",")) e.Integration.Status.SetCondition( v1.IntegrationConditionType("KameletsDeprecationNotice"), @@ -183,11 +164,24 @@ func (t *kameletsTrait) collectKamelets(e *Environment) (map[string]*v1.Kamelet, ) } + kameletsAvailabilityMessage := "" + cond := corev1.ConditionTrue + if len(missingKamelets) > 0 { + kameletsAvailabilityMessage = fmt.Sprintf("Kamelets [%s] not found in cluster. Make sure to include the Kamelets dependency in the Integration.", + strings.Join(missingKamelets, ",")) + cond = corev1.ConditionUnknown + } + if len(availableKamelets) > 0 { + if kameletsAvailabilityMessage != "" { + kameletsAvailabilityMessage += "; " + } + kameletsAvailabilityMessage += fmt.Sprintf("Kamelets [%s] found in cluster", strings.Join(availableKamelets, ",")) + } e.Integration.Status.SetCondition( v1.IntegrationConditionKameletsAvailable, - corev1.ConditionTrue, + cond, v1.IntegrationConditionKameletsAvailableReason, - fmt.Sprintf("kamelets [%s] found in %s repositories", strings.Join(availableKamelets, ","), repo.String()), + kameletsAvailabilityMessage, ) return kamelets, nil diff --git a/pkg/trait/kamelets_test.go b/pkg/trait/kamelets_test.go index 87c24cda8..48dc27fcb 100644 --- a/pkg/trait/kamelets_test.go +++ b/pkg/trait/kamelets_test.go @@ -499,13 +499,13 @@ func TestKameletConditionFalse(t *testing.T) { assert.Nil(t, condition) err = trait.Apply(environment) - require.Error(t, err) + require.NoError(t, err) assert.Len(t, environment.Integration.Status.Conditions, 1) cond := environment.Integration.Status.GetCondition(v1.IntegrationConditionKameletsAvailable) - assert.Equal(t, corev1.ConditionFalse, cond.Status) + assert.Equal(t, corev1.ConditionUnknown, cond.Status) assert.Equal(t, v1.IntegrationConditionKameletsAvailableReason, cond.Reason) - assert.Contains(t, cond.Message, "kamelets [none] not found") + assert.Contains(t, cond.Message, "Kamelets [none] not found") } func TestKameletConditionTrue(t *testing.T) { @@ -834,9 +834,9 @@ func TestKameletMultiNamespace(t *testing.T) { assert.Equal(t, corev1.ConditionTrue, environment.Integration.Status.GetCondition(v1.IntegrationConditionKameletsAvailable).Status) - assert.Equal(t, - "kamelets [extra,timer] found in (Kubernetes[namespace=test], Kubernetes[namespace=ns1], Empty[]) repositories", - environment.Integration.Status.GetCondition(v1.IntegrationConditionKameletsAvailable).Message) + assert.Contains(t, + environment.Integration.Status.GetCondition(v1.IntegrationConditionKameletsAvailable).Message, + "Kamelets [extra,timer] found in cluster") } func TestKameletMultiNamespaceMissing(t *testing.T) { @@ -886,11 +886,11 @@ func TestKameletMultiNamespaceMissing(t *testing.T) { assert.Equal(t, "missing?kameletNamespace=ns1,timer", trait.List) err = trait.Apply(environment) - require.Error(t, err) + require.NoError(t, err) assert.Equal(t, - corev1.ConditionFalse, + corev1.ConditionUnknown, environment.Integration.Status.GetCondition(v1.IntegrationConditionKameletsAvailable).Status) - assert.Equal(t, - "kamelets [missing] not found in (Kubernetes[namespace=test], Kubernetes[namespace=ns1], Empty[]) repositories", - err.Error()) + assert.Contains(t, + environment.Integration.Status.GetCondition(v1.IntegrationConditionKameletsAvailable).Message, + "Kamelets [missing] not found in cluster") }
