You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by pc...@apache.org on 2022/06/20 07:13:45 UTC

[camel-k] branch main updated (f6cea0d63 -> 030f95804)

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

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


    from f6cea0d63 chore(deps): bump github.com/container-tools/spectrum
     new c23af418a feat(cli): kamel promote (or copy) command poc
     new 0cad79746 feat(e2e): promote integration test
     new fa3479b34 chore(cli): polished promote feature
     new 030f95804 feat(cli): promote kameletbinding support

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../files/promote-route.groovy}                    |   6 +-
 e2e/common/cli/promote_test.go                     | 132 +++++++
 pkg/cmd/promote.go                                 | 426 +++++++++++++++++++++
 pkg/cmd/promote_test.go                            |  60 +++
 pkg/cmd/root.go                                    |   1 +
 pkg/cmd/version.go                                 |   1 +
 pkg/trait/kamelets.go                              |  24 +-
 pkg/trait/mount.go                                 |   2 +-
 pkg/util/kamelets/util.go                          |  56 +++
 9 files changed, 685 insertions(+), 23 deletions(-)
 copy e2e/common/{config/files/config-configmap-route.groovy => cli/files/promote-route.groovy} (88%)
 create mode 100644 e2e/common/cli/promote_test.go
 create mode 100644 pkg/cmd/promote.go
 create mode 100644 pkg/cmd/promote_test.go
 create mode 100644 pkg/util/kamelets/util.go


[camel-k] 04/04: feat(cli): promote kameletbinding support

Posted by pc...@apache.org.
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 030f958045c43f8c848503bd45f6c2627dbb2fc1
Author: Pasquale Congiusti <pa...@gmail.com>
AuthorDate: Fri Jun 17 16:17:19 2022 +0200

    feat(cli): promote kameletbinding support
---
 e2e/common/cli/promote_test.go | 28 ++++++++++--
 pkg/cmd/promote.go             | 98 +++++++++++++++++++++++++++++++++++-------
 2 files changed, 107 insertions(+), 19 deletions(-)

diff --git a/e2e/common/cli/promote_test.go b/e2e/common/cli/promote_test.go
index 3f7840165..c2b7182c7 100644
--- a/e2e/common/cli/promote_test.go
+++ b/e2e/common/cli/promote_test.go
@@ -46,7 +46,7 @@ func TestKamelCLIPromote(t *testing.T) {
 		secData["my-secret-key"] = "very top secret development"
 		NewPlainTextSecret(nsDev, "my-sec", secData)
 
-		t.Run("plain integration", func(t *testing.T) {
+		t.Run("plain integration dev", func(t *testing.T) {
 			Expect(Kamel("run", "-n", nsDev, "./files/promote-route.groovy",
 				"--config", "configmap:my-cm",
 				"--config", "secret:my-sec",
@@ -57,13 +57,20 @@ func TestKamelCLIPromote(t *testing.T) {
 			Eventually(IntegrationLogs(nsDev, "promote-route"), TestTimeoutShort).Should(ContainSubstring("very top secret development"))
 		})
 
-		t.Run("kamelet integration", func(t *testing.T) {
+		t.Run("kamelet integration dev", func(t *testing.T) {
 			Expect(CreateTimerKamelet(nsDev, "my-own-timer-source")()).To(Succeed())
 			Expect(Kamel("run", "-n", nsDev, "files/timer-kamelet-usage.groovy").Execute()).To(Succeed())
 			Eventually(IntegrationPodPhase(nsDev, "timer-kamelet-usage"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
 			Eventually(IntegrationLogs(nsDev, "timer-kamelet-usage"), TestTimeoutShort).Should(ContainSubstring("Hello world"))
 		})
 
+		t.Run("kamelet binding dev", func(t *testing.T) {
+			Expect(CreateTimerKamelet(nsDev, "kb-timer-source")()).To(Succeed())
+			Expect(Kamel("bind", "kb-timer-source", "log:info", "-p", "message=my-kamelet-binding-rocks", "-n", nsDev).Execute()).To(Succeed())
+			Eventually(IntegrationPodPhase(nsDev, "kb-timer-source-to-log"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+			Eventually(IntegrationLogs(nsDev, "kb-timer-source-to-log"), TestTimeoutShort).Should(ContainSubstring("my-kamelet-binding-rocks"))
+		})
+
 		// Prod environment namespace
 		WithNewTestNamespace(t, func(nsProd string) {
 			Expect(Kamel("install", "-n", nsProd).Execute()).To(Succeed())
@@ -85,7 +92,7 @@ func TestKamelCLIPromote(t *testing.T) {
 			secData["my-secret-key"] = "very top secret production"
 			NewPlainTextSecret(nsProd, "my-sec", secData)
 
-			t.Run("Production integration", func(t *testing.T) {
+			t.Run("plain integration promotion", func(t *testing.T) {
 				Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).To(Succeed())
 				Eventually(IntegrationPodPhase(nsProd, "promote-route"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
 				Eventually(IntegrationConditionStatus(nsProd, "promote-route", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
@@ -99,7 +106,7 @@ func TestKamelCLIPromote(t *testing.T) {
 				Expect(Kamel("promote", "-n", nsDev, "timer-kamelet-usage", "--to", nsProd).Execute()).NotTo(Succeed())
 			})
 
-			t.Run("kamelet integration", func(t *testing.T) {
+			t.Run("kamelet integration promotion", func(t *testing.T) {
 				Expect(CreateTimerKamelet(nsProd, "my-own-timer-source")()).To(Succeed())
 				Expect(Kamel("promote", "-n", nsDev, "timer-kamelet-usage", "--to", nsProd).Execute()).To(Succeed())
 				Eventually(IntegrationPodPhase(nsProd, "timer-kamelet-usage"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
@@ -107,6 +114,19 @@ func TestKamelCLIPromote(t *testing.T) {
 				// They must use the same image
 				Expect(IntegrationPodImage(nsProd, "timer-kamelet-usage")()).Should(Equal(IntegrationPodImage(nsDev, "timer-kamelet-usage")()))
 			})
+
+			t.Run("no kamelet for kameletbinding in destination", func(t *testing.T) {
+				Expect(Kamel("promote", "-n", nsDev, "kb-timer-source", "--to", nsProd).Execute()).NotTo(Succeed())
+			})
+
+			t.Run("kamelet binding promotion", func(t *testing.T) {
+				Expect(CreateTimerKamelet(nsProd, "kb-timer-source")()).To(Succeed())
+				Expect(Kamel("promote", "-n", nsDev, "kb-timer-source-to-log", "--to", nsProd).Execute()).To(Succeed())
+				Eventually(IntegrationPodPhase(nsProd, "kb-timer-source-to-log"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+				Eventually(IntegrationLogs(nsProd, "kb-timer-source-to-log"), TestTimeoutShort).Should(ContainSubstring("my-kamelet-binding-rocks"))
+				// They must use the same image
+				Expect(IntegrationPodImage(nsProd, "kb-timer-source-to-log")()).Should(Equal(IntegrationPodImage(nsDev, "kb-timer-source-to-log")()))
+			})
 		})
 	})
 }
diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go
index 28be8b28b..6e15c6cc3 100644
--- a/pkg/cmd/promote.go
+++ b/pkg/cmd/promote.go
@@ -20,10 +20,11 @@ package cmd
 import (
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"strings"
 
+	"github.com/pkg/errors"
+
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/client"
@@ -33,6 +34,7 @@ import (
 	"github.com/apache/camel-k/pkg/util/resource"
 	"github.com/spf13/cobra"
 	corev1 "k8s.io/api/core/v1"
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -43,8 +45,8 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO
 	}
 	cmd := cobra.Command{
 		Use:     "promote integration --to [namespace] ...",
-		Short:   "Promote an Integration from an environment to another",
-		Long:    "Promote an Integration from an environment to another, for example from a Development environment to a Production environment",
+		Short:   "Promote an Integration/KameletBinding from an environment to another",
+		Long:    "Promote an Integration/KameletBinding from an environment to another, for example from a Development environment to a Production environment",
 		PreRunE: decode(&options),
 		RunE:    options.run,
 	}
@@ -61,7 +63,7 @@ type promoteCmdOptions struct {
 
 func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error {
 	if len(args) != 1 {
-		return errors.New("promote expects an Integration name argument")
+		return errors.New("promote expects an Integration/KameletBinding name argument")
 	}
 	if o.To == "" {
 		return errors.New("promote expects a destination namespace as --to argument")
@@ -74,39 +76,61 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	it := args[0]
+	name := args[0]
 	c, err := o.GetCmdClient()
 	if err != nil {
-		return err
+		return errors.Wrap(err, "could not retrieve cluster client")
 	}
 
 	opSource, err := operatorInfo(o.Context, c, o.Namespace)
 	if err != nil {
-		return fmt.Errorf("could not retrieve info for Camel K operator source")
+		return errors.Wrap(err, "could not retrieve info for Camel K operator source")
 	}
 	opDest, err := operatorInfo(o.Context, c, o.To)
 	if err != nil {
-		return fmt.Errorf("could not retrieve info for Camel K operator source")
+		return errors.Wrap(err, "could not retrieve info for Camel K operator destination")
 	}
 
 	err = checkOpsCompatibility(cmd, opSource, opDest)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "could not verify operators compatibility")
+	}
+	promoteKameletBinding := false
+	var sourceIntegration *v1.Integration
+	// We first look if a KameletBinding with the name exists
+	sourceKameletBinding, err := o.getKameletBinding(c, name)
+	if err != nil && !k8serrors.IsNotFound(err) {
+		return errors.Wrap(err, "problems looking for KameletBinding "+name)
+	}
+	if sourceKameletBinding != nil {
+		promoteKameletBinding = true
 	}
-	sourceIntegration, err := o.getIntegration(c, it)
+	sourceIntegration, err = o.getIntegration(c, name)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "could not get Integration "+name)
 	}
 	if sourceIntegration.Status.Phase != v1.IntegrationPhaseRunning {
-		return fmt.Errorf("could not promote an integration in %s status", sourceIntegration.Status.Phase)
+		return fmt.Errorf("could not promote an Integration in %s status", sourceIntegration.Status.Phase)
 	}
 	err = o.validateDestResources(c, sourceIntegration)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "could not validate destination resources")
+	}
+	if promoteKameletBinding {
+		// KameletBinding promotion
+		destKameletBinding, err := o.editKameletBinding(sourceKameletBinding, sourceIntegration)
+		if err != nil {
+			return errors.Wrap(err, "could not edit KameletBinding "+name)
+		}
+
+		return c.Create(o.Context, destKameletBinding)
 	}
+	// Plain Integration promotion
 	destIntegration, err := o.editIntegration(sourceIntegration)
 	if err != nil {
-		return err
+		if err != nil {
+			return errors.Wrap(err, "could not edit Integration "+name)
+		}
 	}
 
 	return c.Create(o.Context, destIntegration)
@@ -126,6 +150,19 @@ func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) e
 	return nil
 }
 
+func (o *promoteCmdOptions) getKameletBinding(c client.Client, name string) (*v1alpha1.KameletBinding, error) {
+	it := v1alpha1.NewKameletBinding(o.Namespace, name)
+	key := k8sclient.ObjectKey{
+		Name:      name,
+		Namespace: o.Namespace,
+	}
+	if err := c.Get(o.Context, key, &it); err != nil {
+		return nil, err
+	}
+
+	return &it, nil
+}
+
 func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.Integration, error) {
 	it := v1.NewIntegration(o.Namespace, name)
 	key := k8sclient.ObjectKey{
@@ -133,7 +170,7 @@ func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.In
 		Namespace: o.Namespace,
 	}
 	if err := c.Get(o.Context, key, &it); err != nil {
-		return nil, fmt.Errorf("could not find integration %s in namespace %s", it.Name, o.Namespace)
+		return nil, err
 	}
 
 	return &it, nil
@@ -145,6 +182,9 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr
 	var secrets []string
 	var pvcs []string
 	var kamelets []string
+	if it.Spec.Traits == nil {
+		return nil
+	}
 	// Mount trait
 	mounts := it.Spec.Traits["mount"]
 	if err := json.Unmarshal(mounts.Configuration.RawMessage, &traits); err != nil {
@@ -332,6 +372,34 @@ func (o *promoteCmdOptions) editIntegration(it *v1.Integration) (*v1.Integration
 	return &dst, err
 }
 
+func (o *promoteCmdOptions) editKameletBinding(kb *v1alpha1.KameletBinding, it *v1.Integration) (*v1alpha1.KameletBinding, error) {
+	dst := v1alpha1.NewKameletBinding(o.To, kb.Name)
+	dst.Spec = *kb.Spec.DeepCopy()
+	contImage := it.Status.Image
+	if dst.Spec.Integration == nil {
+		dst.Spec.Integration = &v1.IntegrationSpec{}
+	}
+	if dst.Spec.Integration.Traits == nil {
+		dst.Spec.Integration.Traits = map[string]v1.TraitSpec{}
+	}
+	editedContTrait, err := editContainerImage(dst.Spec.Integration.Traits["container"], contImage)
+	dst.Spec.Integration.Traits["container"] = editedContTrait
+	if dst.Spec.Source.Ref != nil {
+		dst.Spec.Source.Ref.Namespace = o.To
+	}
+	if dst.Spec.Sink.Ref != nil {
+		dst.Spec.Sink.Ref.Namespace = o.To
+	}
+	if dst.Spec.Steps != nil {
+		for _, step := range dst.Spec.Steps {
+			if step.Ref != nil {
+				step.Ref.Namespace = o.To
+			}
+		}
+	}
+	return &dst, err
+}
+
 func editContainerImage(contTrait v1.TraitSpec, image string) (v1.TraitSpec, error) {
 	var editedTrait v1.TraitSpec
 	m := make(map[string]map[string]interface{})


[camel-k] 01/04: feat(cli): kamel promote (or copy) command poc

Posted by pc...@apache.org.
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 c23af418abefd8cbbd7c9a76ff617cc8867007af
Author: Pasquale Congiusti <pa...@gmail.com>
AuthorDate: Wed Jun 1 17:22:26 2022 +0200

    feat(cli): kamel promote (or copy) command poc
    
    * Check compatibility version between source and dest operators
    * Copy the Integration spec from namespace source to ns dest
    * Set container.image trait on destination to reuse image from the source Integration
---
 pkg/cmd/promote.go      | 288 ++++++++++++++++++++++++++++++++++++++++++++++++
 pkg/cmd/promote_test.go |  53 +++++++++
 pkg/cmd/root.go         |   1 +
 pkg/cmd/version.go      |   1 +
 4 files changed, 343 insertions(+)

diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go
new file mode 100644
index 000000000..5c65b82f2
--- /dev/null
+++ b/pkg/cmd/promote.go
@@ -0,0 +1,288 @@
+/*
+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 cmd
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/client"
+	"github.com/apache/camel-k/pkg/metadata"
+	"github.com/apache/camel-k/pkg/util"
+	"github.com/apache/camel-k/pkg/util/camel"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"github.com/apache/camel-k/pkg/util/source"
+	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// newCmdPromote --.
+func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdOptions) {
+	options := promoteCmdOptions{
+		RootCmdOptions: rootCmdOptions,
+	}
+	cmd := cobra.Command{
+		Use:     "promote integration -to [namespace] ...",
+		Short:   "Promote an Integration from an environment to another",
+		Long:    "Promote an Integration from an environment to another, for example from a Development environment to a Production environment",
+		Aliases: []string{"cp", "mv"},
+		Args:    options.validate,
+		PreRunE: decode(&options),
+		RunE:    options.run,
+	}
+
+	cmd.Flags().StringP("to", "", "", "The namespace where to promote the Integration")
+
+	return &cmd, &options
+}
+
+type promoteCmdOptions struct {
+	*RootCmdOptions
+	To string `mapstructure:"to" yaml:",omitempty"`
+}
+
+func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error {
+	if len(args) != 1 {
+		return errors.New("promote expects an integration name argument")
+	}
+
+	return nil
+}
+
+func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error {
+	it := args[0]
+	c, err := o.GetCmdClient()
+	if err != nil {
+		return err
+	}
+
+	opSource, err := operatorInfo(o.Context, c, o.Namespace)
+	if err != nil {
+		return fmt.Errorf("could not retrieve info for Camel K operator source")
+	}
+	opDest, err := operatorInfo(o.Context, c, o.To)
+	if err != nil {
+		return fmt.Errorf("could not retrieve info for Camel K operator source")
+	}
+
+	checkOpsCompatibility(cmd, opSource, opDest)
+
+	sourceIntegration, err := o.getIntegration(c, it)
+	o.validateDestResources(c, sourceIntegration)
+	//destIntegration := o.editIntegration(sourceIntegration)
+
+	//return c.Create(o.Context, destIntegration)
+	return nil
+}
+
+func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) {
+	if !compatibleVersions(source["Version"], dest["Version"], cmd) {
+		panic(fmt.Sprintf("source (%s) and destination (%s) Camel K operator versions are not compatible", source["version"], dest["version"]))
+	}
+	if !compatibleVersions(source["Runtime Version"], dest["Runtime Version"], cmd) {
+		panic(fmt.Sprintf("source (%s) and destination (%s) Camel K runtime versions are not compatible", source["runtime version"], dest["runtime version"]))
+	}
+	if source["Registry Address"] != source["Registry Address"] {
+		panic(fmt.Sprintf("source (%s) and destination (%s) Camel K container images registries are not the same", source["registry address"], dest["registry address"]))
+	}
+}
+
+func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.Integration, error) {
+	it := v1.NewIntegration(o.Namespace, name)
+	key := k8sclient.ObjectKey{
+		Name:      name,
+		Namespace: o.Namespace,
+	}
+	if err := c.Get(o.Context, key, &it); err != nil {
+		return nil, fmt.Errorf("could not find integration %s in namespace %s", it.Name, o.Namespace)
+	}
+
+	return &it, nil
+}
+
+func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integration) {
+	var traits map[string][]string
+	var configmaps []string
+	var secrets []string
+	var pvcs []string
+	var kamelets []string
+	// Mount trait
+	mounts := it.Spec.Traits["mount"]
+	json.Unmarshal(mounts.Configuration.RawMessage, &traits)
+	for t, v := range traits {
+		if t == "configs" || t == "resources" {
+			for _, c := range v {
+				//TODO proper parse resources, now it does not account for complex parsing
+				if strings.HasPrefix(c, "configmap:") {
+					configmaps = append(configmaps, strings.Split(c, ":")[1])
+				}
+				if strings.HasPrefix(c, "secret:") {
+					secrets = append(secrets, strings.Split(c, ":")[1])
+				}
+			}
+		} else if t == "volumes" {
+			for _, c := range v {
+				pvcs = append(pvcs, strings.Split(c, ":")[0])
+			}
+		}
+	}
+	// Openapi trait
+	openapis := it.Spec.Traits["openapi"]
+	json.Unmarshal(openapis.Configuration.RawMessage, &traits)
+	for k, v := range traits {
+		for _, c := range v {
+			if k == "configmaps" {
+				configmaps = append(configmaps, c)
+			}
+		}
+	}
+	// Kamelet trait
+	kamelets = o.listKamelets(c, it)
+
+	anyError := false
+	for _, name := range configmaps {
+		if !existsCm(o.Context, c, name, o.To) {
+			anyError = true
+			fmt.Printf("Configmap %s is missing from %s namespace\n", name, o.To)
+		}
+	}
+	for _, name := range secrets {
+		if !existsSecret(o.Context, c, name, o.To) {
+			anyError = true
+			fmt.Printf("Secret %s is missing from %s namespace\n", name, o.To)
+		}
+	}
+	for _, name := range pvcs {
+		if !existsPv(o.Context, c, name, o.To) {
+			anyError = true
+			fmt.Printf("PersistentVolume %s is missing from %s namespace\n", name, o.To)
+		}
+	}
+	for _, name := range kamelets {
+		if !existsKamelet(o.Context, c, name, o.To) {
+			anyError = true
+			fmt.Printf("Kamelet %s is missing from %s namespace\n", name, o.To)
+		}
+	}
+
+	if anyError {
+		os.Exit(1)
+	}
+}
+
+func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []string {
+	// TODO collect any kamelets which may be coming into the kamelet trait as well
+	var kamelets []string
+
+	sources, _ := kubernetes.ResolveIntegrationSources(o.Context, c, it, &kubernetes.Collection{})
+	catalog, _ := camel.DefaultCatalog()
+	metadata.Each(catalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool {
+		util.StringSliceUniqueConcat(&kamelets, meta.Kamelets)
+		return true
+	})
+
+	// Check if a Kamelet is configured as default error handler URI
+	defaultErrorHandlerURI := it.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri")
+	if defaultErrorHandlerURI != "" {
+		if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") {
+			kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI))
+		}
+	}
+
+	return kamelets
+}
+
+func existsCm(ctx context.Context, c client.Client, name string, namespace string) bool {
+	var obj corev1.ConfigMap
+	key := k8sclient.ObjectKey{
+		Name:      name,
+		Namespace: namespace,
+	}
+	if err := c.Get(ctx, key, &obj); err != nil {
+		return false
+	}
+
+	return true
+}
+
+func existsSecret(ctx context.Context, c client.Client, name string, namespace string) bool {
+	var obj corev1.Secret
+	key := k8sclient.ObjectKey{
+		Name:      name,
+		Namespace: namespace,
+	}
+	if err := c.Get(ctx, key, &obj); err != nil {
+		return false
+	}
+
+	return true
+}
+
+func existsPv(ctx context.Context, c client.Client, name string, namespace string) bool {
+	var obj corev1.PersistentVolume
+	key := k8sclient.ObjectKey{
+		Name:      name,
+		Namespace: namespace,
+	}
+	if err := c.Get(ctx, key, &obj); err != nil {
+		return false
+	}
+
+	return true
+}
+
+func existsKamelet(ctx context.Context, c client.Client, name string, namespace string) bool {
+	var obj v1alpha1.Kamelet
+	key := k8sclient.ObjectKey{
+		Name:      name,
+		Namespace: namespace,
+	}
+	if err := c.Get(ctx, key, &obj); err != nil {
+		return false
+	}
+
+	return true
+}
+
+func (o *promoteCmdOptions) editIntegration(it *v1.Integration) *v1.Integration {
+	dst := v1.NewIntegration(o.To, it.Name)
+	contImage := it.Status.Image
+	dst.Spec = *it.Spec.DeepCopy()
+	dst.Spec.Traits = map[string]v1.TraitSpec{
+		"container": traitSpecFromMap(map[string]interface{}{
+			"image": contImage,
+		}),
+	}
+
+	return &dst
+}
+
+// TODO refactor properly
+func traitSpecFromMap(spec map[string]interface{}) v1.TraitSpec {
+	var trait v1.TraitSpec
+	data, _ := json.Marshal(spec)
+	_ = json.Unmarshal(data, &trait.Configuration)
+	return trait
+}
diff --git a/pkg/cmd/promote_test.go b/pkg/cmd/promote_test.go
new file mode 100644
index 000000000..ef527248a
--- /dev/null
+++ b/pkg/cmd/promote_test.go
@@ -0,0 +1,53 @@
+/*
+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 cmd
+
+import (
+	"testing"
+
+	"github.com/apache/camel-k/pkg/util/test"
+	"github.com/spf13/cobra"
+)
+
+const cmdPromote = "promote"
+
+// nolint: unparam
+func initializePromoteCmdOptions(t *testing.T) (*promoteCmdOptions, *cobra.Command, RootCmdOptions) {
+	t.Helper()
+
+	options, rootCmd := kamelTestPreAddCommandInit()
+	promoteCmdOptions := addTestPromoteCmd(*options, rootCmd)
+	kamelTestPostAddCommandInit(t, rootCmd)
+
+	return promoteCmdOptions, rootCmd, *options
+}
+
+// nolint: unparam
+func addTestPromoteCmd(options RootCmdOptions, rootCmd *cobra.Command) *promoteCmdOptions {
+	// add a testing version of operator Command
+	operatorCmd, promoteOptions := newCmdPromote(&options)
+	operatorCmd.RunE = func(c *cobra.Command, args []string) error {
+		return nil
+	}
+	operatorCmd.PostRunE = func(c *cobra.Command, args []string) error {
+		return nil
+	}
+	operatorCmd.Args = test.ArbitraryArgs
+	rootCmd.AddCommand(operatorCmd)
+	return promoteOptions
+}
diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go
index f148ab7e5..abdde0747 100644
--- a/pkg/cmd/root.go
+++ b/pkg/cmd/root.go
@@ -150,6 +150,7 @@ func addKamelSubcommands(cmd *cobra.Command, options *RootCmdOptions) {
 	cmd.AddCommand(cmdOnly(newCmdDump(options)))
 	cmd.AddCommand(newCmdLocal(options))
 	cmd.AddCommand(cmdOnly(newCmdBind(options)))
+	cmd.AddCommand(cmdOnly(newCmdPromote(options)))
 	cmd.AddCommand(newCmdKamelet(options))
 }
 
diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go
index 011d6eddb..ec0e493bd 100644
--- a/pkg/cmd/version.go
+++ b/pkg/cmd/version.go
@@ -143,6 +143,7 @@ func operatorInfo(ctx context.Context, c client.Client, namespace string) (map[s
 	infos["version"] = platform.Status.Version
 	infos["publishStrategy"] = string(platform.Status.Build.PublishStrategy)
 	infos["runtimeVersion"] = platform.Status.Build.RuntimeVersion
+	infos["registryAddress"] = platform.Status.Build.Registry.Address
 
 	if platform.Status.Info != nil {
 		for k, v := range platform.Status.Info {


[camel-k] 03/04: chore(cli): polished promote feature

Posted by pc...@apache.org.
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 fa3479b34c10728873299356687aeef043207548
Author: Pasquale Congiusti <pa...@gmail.com>
AuthorDate: Thu Jun 16 17:24:30 2022 +0200

    chore(cli): polished promote feature
---
 pkg/cmd/promote.go        | 91 ++++++++++++++++++++++++++++++-----------------
 pkg/trait/kamelets.go     | 24 +++----------
 pkg/trait/mount.go        |  2 +-
 pkg/util/kamelets/util.go | 56 +++++++++++++++++++++++++++++
 4 files changed, 119 insertions(+), 54 deletions(-)

diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go
index b24eafd80..28be8b28b 100644
--- a/pkg/cmd/promote.go
+++ b/pkg/cmd/promote.go
@@ -27,11 +27,10 @@ import (
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/client"
-	"github.com/apache/camel-k/pkg/metadata"
-	"github.com/apache/camel-k/pkg/util"
 	"github.com/apache/camel-k/pkg/util/camel"
+	"github.com/apache/camel-k/pkg/util/kamelets"
 	"github.com/apache/camel-k/pkg/util/kubernetes"
-	"github.com/apache/camel-k/pkg/util/source"
+	"github.com/apache/camel-k/pkg/util/resource"
 	"github.com/spf13/cobra"
 	corev1 "k8s.io/api/core/v1"
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -120,7 +119,7 @@ func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) e
 	if !compatibleVersions(source["Runtime Version"], dest["Runtime Version"], cmd) {
 		return fmt.Errorf("source (%s) and destination (%s) Camel K runtime versions are not compatible", source["Runtime Version"], dest["Runtime Version"])
 	}
-	if source["Registry Address"] != source["Registry Address"] {
+	if source["Registry Address"] != dest["Registry Address"] {
 		return fmt.Errorf("source (%s) and destination (%s) Camel K container images registries are not the same", source["Registry Address"], dest["Registry Address"])
 	}
 
@@ -148,27 +147,52 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr
 	var kamelets []string
 	// Mount trait
 	mounts := it.Spec.Traits["mount"]
-	json.Unmarshal(mounts.Configuration.RawMessage, &traits)
+	if err := json.Unmarshal(mounts.Configuration.RawMessage, &traits); err != nil {
+		return err
+	}
 	for t, v := range traits {
-		if t == "configs" || t == "resources" {
+		switch t {
+		case "configs":
 			for _, c := range v {
-				//TODO proper parse resources, now it does not account for complex parsing
-				if strings.HasPrefix(c, "configmap:") {
-					configmaps = append(configmaps, strings.Split(c, ":")[1])
+				if conf, parseErr := resource.ParseConfig(c); parseErr == nil {
+					if conf.StorageType() == resource.StorageTypeConfigmap {
+						configmaps = append(configmaps, conf.Name())
+					} else if conf.StorageType() == resource.StorageTypeSecret {
+						secrets = append(secrets, conf.Name())
+					}
+				} else {
+					return parseErr
 				}
-				if strings.HasPrefix(c, "secret:") {
-					secrets = append(secrets, strings.Split(c, ":")[1])
+			}
+		case "resources":
+			for _, c := range v {
+				if conf, parseErr := resource.ParseResource(c); parseErr == nil {
+					if conf.StorageType() == resource.StorageTypeConfigmap {
+						configmaps = append(configmaps, conf.Name())
+					} else if conf.StorageType() == resource.StorageTypeSecret {
+						secrets = append(secrets, conf.Name())
+					}
+				} else {
+					return parseErr
 				}
 			}
-		} else if t == "volumes" {
+		case "volumes":
 			for _, c := range v {
-				pvcs = append(pvcs, strings.Split(c, ":")[0])
+				if conf, parseErr := resource.ParseVolume(c); parseErr == nil {
+					if conf.StorageType() == resource.StorageTypePVC {
+						pvcs = append(pvcs, conf.Name())
+					}
+				} else {
+					return parseErr
+				}
 			}
 		}
 	}
 	// Openapi trait
 	openapis := it.Spec.Traits["openapi"]
-	json.Unmarshal(openapis.Configuration.RawMessage, &traits)
+	if err := json.Unmarshal(openapis.Configuration.RawMessage, &traits); err != nil {
+		return err
+	}
 	for k, v := range traits {
 		for _, c := range v {
 			if k == "configmaps" {
@@ -177,7 +201,17 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr
 		}
 	}
 	// Kamelet trait
-	kamelets = o.listKamelets(c, it)
+	kameletTrait := it.Spec.Traits["kamelets"]
+	var kameletListTrait map[string]string
+	if err := json.Unmarshal(kameletTrait.Configuration.RawMessage, &kameletListTrait); err != nil {
+		return err
+	}
+	kamelets = strings.Split(kameletListTrait["list"], ",")
+	sourceKamelets, err := o.listKamelets(c, it)
+	if err != nil {
+		return err
+	}
+	kamelets = append(kamelets, sourceKamelets...)
 
 	anyError := false
 	var errorTrace string
@@ -213,23 +247,14 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr
 	return nil
 }
 
-func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []string {
-	// TODO collect any kamelets which may be coming into the kamelet trait as well
-	var kamelets []string
-
-	sources, _ := kubernetes.ResolveIntegrationSources(o.Context, c, it, &kubernetes.Collection{})
-	catalog, _ := camel.DefaultCatalog()
-	metadata.Each(catalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool {
-		util.StringSliceUniqueConcat(&kamelets, meta.Kamelets)
-		return true
-	})
-
-	// Check if a Kamelet is configured as default error handler URI
-	defaultErrorHandlerURI := it.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri")
-	if defaultErrorHandlerURI != "" {
-		if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") {
-			kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI))
-		}
+func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) ([]string, error) {
+	catalog, err := camel.DefaultCatalog()
+	if err != nil {
+		return nil, err
+	}
+	kamelets, err := kamelets.ExtractKameletFromSources(o.Context, c, catalog, &kubernetes.Collection{}, it)
+	if err != nil {
+		return nil, err
 	}
 
 	// We must remove any default source/sink
@@ -240,7 +265,7 @@ func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []
 		}
 	}
 
-	return filtered
+	return filtered, nil
 }
 
 func existsCm(ctx context.Context, c client.Client, name string, namespace string) bool {
diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go
index a5c1c936b..f8873cd7d 100644
--- a/pkg/trait/kamelets.go
+++ b/pkg/trait/kamelets.go
@@ -31,13 +31,11 @@ import (
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	kameletutils "github.com/apache/camel-k/pkg/kamelet"
 	"github.com/apache/camel-k/pkg/kamelet/repository"
-	"github.com/apache/camel-k/pkg/metadata"
 	"github.com/apache/camel-k/pkg/platform"
 	"github.com/apache/camel-k/pkg/util"
 	"github.com/apache/camel-k/pkg/util/digest"
 	"github.com/apache/camel-k/pkg/util/dsl"
-	"github.com/apache/camel-k/pkg/util/kubernetes"
-	"github.com/apache/camel-k/pkg/util/source"
+	"github.com/apache/camel-k/pkg/util/kamelets"
 )
 
 // The kamelets trait is a platform trait used to inject Kamelets into the integration runtime.
@@ -91,23 +89,9 @@ func (t *kameletsTrait) Configure(e *Environment) (bool, error) {
 	}
 
 	if IsNilOrTrue(t.Auto) {
-		var kamelets []string
-		if t.List == "" {
-			sources, err := kubernetes.ResolveIntegrationSources(e.Ctx, e.Client, e.Integration, e.Resources)
-			if err != nil {
-				return false, err
-			}
-			metadata.Each(e.CamelCatalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool {
-				util.StringSliceUniqueConcat(&kamelets, meta.Kamelets)
-				return true
-			})
-		}
-		// Check if a Kamelet is configured as default error handler URI
-		defaultErrorHandlerURI := e.Integration.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri")
-		if defaultErrorHandlerURI != "" {
-			if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") {
-				kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI))
-			}
+		kamelets, err := kamelets.ExtractKameletFromSources(e.Ctx, e.Client, e.CamelCatalog, e.Resources, e.Integration)
+		if err != nil {
+			return false, err
 		}
 
 		if len(kamelets) > 0 {
diff --git a/pkg/trait/mount.go b/pkg/trait/mount.go
index 6c8976a85..81d58c50f 100644
--- a/pkg/trait/mount.go
+++ b/pkg/trait/mount.go
@@ -43,7 +43,7 @@ type mountTrait struct {
 	// A list of configuration pointing to configmap/secret.
 	// The configuration are expected to be UTF-8 resources as they are processed by runtime Camel Context and tried to be parsed as property files.
 	// They are also made available on the classpath in order to ease their usage directly from the Route.
-	// Syntax: [configmap|secret]:name[key], where name represents the resource name and key optionally represents the resource key to be filtered
+	// Syntax: [configmap|secret]:name[/key], where name represents the resource name and key optionally represents the resource key to be filtered
 	Configs []string `property:"configs" json:"configs,omitempty"`
 	// A list of resources (text or binary content) pointing to configmap/secret.
 	// The resources are expected to be any resource type (text or binary content).
diff --git a/pkg/util/kamelets/util.go b/pkg/util/kamelets/util.go
new file mode 100644
index 000000000..2db03c7fc
--- /dev/null
+++ b/pkg/util/kamelets/util.go
@@ -0,0 +1,56 @@
+/*
+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 kamelets
+
+import (
+	"context"
+	"strings"
+
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/client"
+	"github.com/apache/camel-k/pkg/metadata"
+	"github.com/apache/camel-k/pkg/util"
+	"github.com/apache/camel-k/pkg/util/camel"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"github.com/apache/camel-k/pkg/util/source"
+)
+
+// ExtractKameletFromSources provide a list of Kamelets referred into the Integration sources.
+func ExtractKameletFromSources(context context.Context, c client.Client, catalog *camel.RuntimeCatalog, resources *kubernetes.Collection, it *v1.Integration) ([]string, error) {
+	var kamelets []string
+
+	sources, err := kubernetes.ResolveIntegrationSources(context, c, it, resources)
+	if err != nil {
+		return nil, err
+	}
+	metadata.Each(catalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool {
+		util.StringSliceUniqueConcat(&kamelets, meta.Kamelets)
+		return true
+	})
+
+	// Check if a Kamelet is configured as default error handler URI
+	defaultErrorHandlerURI := it.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri")
+	if defaultErrorHandlerURI != "" {
+		if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") {
+			kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI))
+		}
+	}
+
+	return kamelets, nil
+}


[camel-k] 02/04: feat(e2e): promote integration test

Posted by pc...@apache.org.
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 0cad7974633a54cef58e7f0e0cf9d000d6787d1c
Author: Pasquale Congiusti <pa...@gmail.com>
AuthorDate: Wed Jun 15 09:57:47 2022 +0200

    feat(e2e): promote integration test
---
 e2e/common/cli/files/promote-route.groovy |  25 +++++++
 e2e/common/cli/promote_test.go            | 112 ++++++++++++++++++++++++++++
 pkg/cmd/promote.go                        | 119 ++++++++++++++++++++----------
 pkg/cmd/promote_test.go                   |  53 +++++++------
 4 files changed, 249 insertions(+), 60 deletions(-)

diff --git a/e2e/common/cli/files/promote-route.groovy b/e2e/common/cli/files/promote-route.groovy
new file mode 100644
index 000000000..943a4ab91
--- /dev/null
+++ b/e2e/common/cli/files/promote-route.groovy
@@ -0,0 +1,25 @@
+// camel-k: language=groovy
+/*
+ * 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.
+ */
+
+from('timer:configmap')
+    .setBody()
+        .simple("resource:classpath:my-configmap-key")
+    .log('configmap: ${body}')
+    .setBody()
+        .simple("resource:classpath:my-secret-key")
+    .log('secret: ${body}')
\ No newline at end of file
diff --git a/e2e/common/cli/promote_test.go b/e2e/common/cli/promote_test.go
new file mode 100644
index 000000000..3f7840165
--- /dev/null
+++ b/e2e/common/cli/promote_test.go
@@ -0,0 +1,112 @@
+//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 (
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+
+	. "github.com/onsi/gomega"
+
+	. "github.com/apache/camel-k/e2e/support"
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+)
+
+func TestKamelCLIPromote(t *testing.T) {
+	// Dev environment namespace
+	WithNewTestNamespace(t, func(nsDev string) {
+		Expect(Kamel("install", "-n", nsDev).Execute()).To(Succeed())
+		// Dev content configmap
+		var cmData = make(map[string]string)
+		cmData["my-configmap-key"] = "I am development configmap!"
+		NewPlainTextConfigmap(nsDev, "my-cm", cmData)
+		// Dev secret
+		var secData = make(map[string]string)
+		secData["my-secret-key"] = "very top secret development"
+		NewPlainTextSecret(nsDev, "my-sec", secData)
+
+		t.Run("plain integration", func(t *testing.T) {
+			Expect(Kamel("run", "-n", nsDev, "./files/promote-route.groovy",
+				"--config", "configmap:my-cm",
+				"--config", "secret:my-sec",
+			).Execute()).To(Succeed())
+			Eventually(IntegrationPodPhase(nsDev, "promote-route"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+			Eventually(IntegrationConditionStatus(nsDev, "promote-route", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+			Eventually(IntegrationLogs(nsDev, "promote-route"), TestTimeoutShort).Should(ContainSubstring("I am development configmap!"))
+			Eventually(IntegrationLogs(nsDev, "promote-route"), TestTimeoutShort).Should(ContainSubstring("very top secret development"))
+		})
+
+		t.Run("kamelet integration", func(t *testing.T) {
+			Expect(CreateTimerKamelet(nsDev, "my-own-timer-source")()).To(Succeed())
+			Expect(Kamel("run", "-n", nsDev, "files/timer-kamelet-usage.groovy").Execute()).To(Succeed())
+			Eventually(IntegrationPodPhase(nsDev, "timer-kamelet-usage"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+			Eventually(IntegrationLogs(nsDev, "timer-kamelet-usage"), TestTimeoutShort).Should(ContainSubstring("Hello world"))
+		})
+
+		// Prod environment namespace
+		WithNewTestNamespace(t, func(nsProd string) {
+			Expect(Kamel("install", "-n", nsProd).Execute()).To(Succeed())
+
+			t.Run("no configmap in destination", func(t *testing.T) {
+				Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).NotTo(Succeed())
+			})
+			// Prod content configmap
+			var cmData = make(map[string]string)
+			cmData["my-configmap-key"] = "I am production!"
+			NewPlainTextConfigmap(nsProd, "my-cm", cmData)
+
+			t.Run("no secret in destination", func(t *testing.T) {
+				Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).NotTo(Succeed())
+			})
+
+			// Prod secret
+			var secData = make(map[string]string)
+			secData["my-secret-key"] = "very top secret production"
+			NewPlainTextSecret(nsProd, "my-sec", secData)
+
+			t.Run("Production integration", func(t *testing.T) {
+				Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).To(Succeed())
+				Eventually(IntegrationPodPhase(nsProd, "promote-route"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+				Eventually(IntegrationConditionStatus(nsProd, "promote-route", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+				Eventually(IntegrationLogs(nsProd, "promote-route"), TestTimeoutShort).Should(ContainSubstring("I am production!"))
+				Eventually(IntegrationLogs(nsProd, "promote-route"), TestTimeoutShort).Should(ContainSubstring("very top secret production"))
+				// They must use the same image
+				Expect(IntegrationPodImage(nsProd, "promote-route")()).Should(Equal(IntegrationPodImage(nsDev, "promote-route")()))
+			})
+
+			t.Run("no kamelet in destination", func(t *testing.T) {
+				Expect(Kamel("promote", "-n", nsDev, "timer-kamelet-usage", "--to", nsProd).Execute()).NotTo(Succeed())
+			})
+
+			t.Run("kamelet integration", func(t *testing.T) {
+				Expect(CreateTimerKamelet(nsProd, "my-own-timer-source")()).To(Succeed())
+				Expect(Kamel("promote", "-n", nsDev, "timer-kamelet-usage", "--to", nsProd).Execute()).To(Succeed())
+				Eventually(IntegrationPodPhase(nsProd, "timer-kamelet-usage"), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+				Eventually(IntegrationLogs(nsProd, "timer-kamelet-usage"), TestTimeoutShort).Should(ContainSubstring("Hello world"))
+				// They must use the same image
+				Expect(IntegrationPodImage(nsProd, "timer-kamelet-usage")()).Should(Equal(IntegrationPodImage(nsDev, "timer-kamelet-usage")()))
+			})
+		})
+	})
+}
diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go
index 5c65b82f2..b24eafd80 100644
--- a/pkg/cmd/promote.go
+++ b/pkg/cmd/promote.go
@@ -22,7 +22,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"os"
 	"strings"
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
@@ -44,16 +43,14 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO
 		RootCmdOptions: rootCmdOptions,
 	}
 	cmd := cobra.Command{
-		Use:     "promote integration -to [namespace] ...",
+		Use:     "promote integration --to [namespace] ...",
 		Short:   "Promote an Integration from an environment to another",
 		Long:    "Promote an Integration from an environment to another, for example from a Development environment to a Production environment",
-		Aliases: []string{"cp", "mv"},
-		Args:    options.validate,
 		PreRunE: decode(&options),
 		RunE:    options.run,
 	}
 
-	cmd.Flags().StringP("to", "", "", "The namespace where to promote the Integration")
+	cmd.Flags().String("to", "", "The namespace where to promote the Integration")
 
 	return &cmd, &options
 }
@@ -65,13 +62,19 @@ type promoteCmdOptions struct {
 
 func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error {
 	if len(args) != 1 {
-		return errors.New("promote expects an integration name argument")
+		return errors.New("promote expects an Integration name argument")
+	}
+	if o.To == "" {
+		return errors.New("promote expects a destination namespace as --to argument")
 	}
-
 	return nil
 }
 
 func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error {
+	if err := o.validate(cmd, args); err != nil {
+		return err
+	}
+
 	it := args[0]
 	c, err := o.GetCmdClient()
 	if err != nil {
@@ -87,26 +90,41 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("could not retrieve info for Camel K operator source")
 	}
 
-	checkOpsCompatibility(cmd, opSource, opDest)
-
+	err = checkOpsCompatibility(cmd, opSource, opDest)
+	if err != nil {
+		return err
+	}
 	sourceIntegration, err := o.getIntegration(c, it)
-	o.validateDestResources(c, sourceIntegration)
-	//destIntegration := o.editIntegration(sourceIntegration)
+	if err != nil {
+		return err
+	}
+	if sourceIntegration.Status.Phase != v1.IntegrationPhaseRunning {
+		return fmt.Errorf("could not promote an integration in %s status", sourceIntegration.Status.Phase)
+	}
+	err = o.validateDestResources(c, sourceIntegration)
+	if err != nil {
+		return err
+	}
+	destIntegration, err := o.editIntegration(sourceIntegration)
+	if err != nil {
+		return err
+	}
 
-	//return c.Create(o.Context, destIntegration)
-	return nil
+	return c.Create(o.Context, destIntegration)
 }
 
-func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) {
+func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) error {
 	if !compatibleVersions(source["Version"], dest["Version"], cmd) {
-		panic(fmt.Sprintf("source (%s) and destination (%s) Camel K operator versions are not compatible", source["version"], dest["version"]))
+		return fmt.Errorf("source (%s) and destination (%s) Camel K operator versions are not compatible", source["Version"], dest["Version"])
 	}
 	if !compatibleVersions(source["Runtime Version"], dest["Runtime Version"], cmd) {
-		panic(fmt.Sprintf("source (%s) and destination (%s) Camel K runtime versions are not compatible", source["runtime version"], dest["runtime version"]))
+		return fmt.Errorf("source (%s) and destination (%s) Camel K runtime versions are not compatible", source["Runtime Version"], dest["Runtime Version"])
 	}
 	if source["Registry Address"] != source["Registry Address"] {
-		panic(fmt.Sprintf("source (%s) and destination (%s) Camel K container images registries are not the same", source["registry address"], dest["registry address"]))
+		return fmt.Errorf("source (%s) and destination (%s) Camel K container images registries are not the same", source["Registry Address"], dest["Registry Address"])
 	}
+
+	return nil
 }
 
 func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.Integration, error) {
@@ -122,7 +140,7 @@ func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.In
 	return &it, nil
 }
 
-func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integration) {
+func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integration) error {
 	var traits map[string][]string
 	var configmaps []string
 	var secrets []string
@@ -162,34 +180,37 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr
 	kamelets = o.listKamelets(c, it)
 
 	anyError := false
+	var errorTrace string
 	for _, name := range configmaps {
 		if !existsCm(o.Context, c, name, o.To) {
 			anyError = true
-			fmt.Printf("Configmap %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("Configmap %s is missing from %s namespace\n", name, o.To)
 		}
 	}
 	for _, name := range secrets {
 		if !existsSecret(o.Context, c, name, o.To) {
 			anyError = true
-			fmt.Printf("Secret %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("Secret %s is missing from %s namespace\n", name, o.To)
 		}
 	}
 	for _, name := range pvcs {
 		if !existsPv(o.Context, c, name, o.To) {
 			anyError = true
-			fmt.Printf("PersistentVolume %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("PersistentVolume %s is missing from %s namespace\n", name, o.To)
 		}
 	}
 	for _, name := range kamelets {
 		if !existsKamelet(o.Context, c, name, o.To) {
 			anyError = true
-			fmt.Printf("Kamelet %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("Kamelet %s is missing from %s namespace\n", name, o.To)
 		}
 	}
 
 	if anyError {
-		os.Exit(1)
+		return fmt.Errorf(errorTrace)
 	}
+
+	return nil
 }
 
 func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []string {
@@ -211,7 +232,15 @@ func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []
 		}
 	}
 
-	return kamelets
+	// We must remove any default source/sink
+	var filtered []string
+	for _, k := range kamelets {
+		if k != "source" && k != "sink" {
+			filtered = append(filtered, k)
+		}
+	}
+
+	return filtered
 }
 
 func existsCm(ctx context.Context, c client.Client, name string, namespace string) bool {
@@ -266,23 +295,39 @@ func existsKamelet(ctx context.Context, c client.Client, name string, namespace
 	return true
 }
 
-func (o *promoteCmdOptions) editIntegration(it *v1.Integration) *v1.Integration {
+func (o *promoteCmdOptions) editIntegration(it *v1.Integration) (*v1.Integration, error) {
 	dst := v1.NewIntegration(o.To, it.Name)
 	contImage := it.Status.Image
 	dst.Spec = *it.Spec.DeepCopy()
-	dst.Spec.Traits = map[string]v1.TraitSpec{
-		"container": traitSpecFromMap(map[string]interface{}{
-			"image": contImage,
-		}),
+	if dst.Spec.Traits == nil {
+		dst.Spec.Traits = map[string]v1.TraitSpec{}
 	}
-
-	return &dst
+	editedContTrait, err := editContainerImage(dst.Spec.Traits["container"], contImage)
+	dst.Spec.Traits["container"] = editedContTrait
+	return &dst, err
 }
 
-// TODO refactor properly
-func traitSpecFromMap(spec map[string]interface{}) v1.TraitSpec {
-	var trait v1.TraitSpec
-	data, _ := json.Marshal(spec)
-	_ = json.Unmarshal(data, &trait.Configuration)
-	return trait
+func editContainerImage(contTrait v1.TraitSpec, image string) (v1.TraitSpec, error) {
+	var editedTrait v1.TraitSpec
+	m := make(map[string]map[string]interface{})
+	data, err := json.Marshal(contTrait)
+	if err != nil {
+		return editedTrait, err
+	}
+	err = json.Unmarshal(data, &m)
+	if err != nil {
+		return editedTrait, err
+	}
+	// We must initialize, if it was not initialized so far
+	if m["configuration"] == nil {
+		m["configuration"] = make(map[string]interface{})
+	}
+	m["configuration"]["image"] = image
+	newData, err := json.Marshal(m)
+	if err != nil {
+		return editedTrait, err
+	}
+	err = json.Unmarshal(newData, &editedTrait)
+
+	return editedTrait, err
 }
diff --git a/pkg/cmd/promote_test.go b/pkg/cmd/promote_test.go
index ef527248a..3fe2331b5 100644
--- a/pkg/cmd/promote_test.go
+++ b/pkg/cmd/promote_test.go
@@ -18,36 +18,43 @@ limitations under the License.
 package cmd
 
 import (
+	"encoding/json"
 	"testing"
 
-	"github.com/apache/camel-k/pkg/util/test"
-	"github.com/spf13/cobra"
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/stretchr/testify/assert"
 )
 
-const cmdPromote = "promote"
+func TestEditContainerTrait(t *testing.T) {
+	var containerTrait v1.TraitSpec
+	m := make(map[string]interface{})
+	m["configuration"] = map[string]interface{}{
+		"name":  "myName",
+		"image": "myImage",
+	}
+	data, _ := json.Marshal(m)
+	_ = json.Unmarshal(data, &containerTrait)
 
-// nolint: unparam
-func initializePromoteCmdOptions(t *testing.T) (*promoteCmdOptions, *cobra.Command, RootCmdOptions) {
-	t.Helper()
+	editedContainerTrait, err := editContainerImage(containerTrait, "editedImage")
+	assert.Nil(t, err)
 
-	options, rootCmd := kamelTestPreAddCommandInit()
-	promoteCmdOptions := addTestPromoteCmd(*options, rootCmd)
-	kamelTestPostAddCommandInit(t, rootCmd)
+	mappedTrait := make(map[string]map[string]interface{})
+	newData, _ := json.Marshal(editedContainerTrait)
+	_ = json.Unmarshal(newData, &mappedTrait)
 
-	return promoteCmdOptions, rootCmd, *options
+	assert.Equal(t, "myName", mappedTrait["configuration"]["name"])
+	assert.Equal(t, "editedImage", mappedTrait["configuration"]["image"])
 }
 
-// nolint: unparam
-func addTestPromoteCmd(options RootCmdOptions, rootCmd *cobra.Command) *promoteCmdOptions {
-	// add a testing version of operator Command
-	operatorCmd, promoteOptions := newCmdPromote(&options)
-	operatorCmd.RunE = func(c *cobra.Command, args []string) error {
-		return nil
-	}
-	operatorCmd.PostRunE = func(c *cobra.Command, args []string) error {
-		return nil
-	}
-	operatorCmd.Args = test.ArbitraryArgs
-	rootCmd.AddCommand(operatorCmd)
-	return promoteOptions
+func TestEditMissingContainerTrait(t *testing.T) {
+	var containerTrait v1.TraitSpec
+
+	editedContainerTrait, err := editContainerImage(containerTrait, "editedImage")
+	assert.Nil(t, err)
+
+	mappedTrait := make(map[string]map[string]interface{})
+	newData, _ := json.Marshal(editedContainerTrait)
+	_ = json.Unmarshal(newData, &mappedTrait)
+
+	assert.Equal(t, "editedImage", mappedTrait["configuration"]["image"])
 }