Repository: apache/camel-k
Pull request #3468 (2022-07-20 17:07:50)

Provides Makefile with ability to generate a test bundle index image
<!-- Description -->
Allows building and testing of an OLM catalog containing the newly built bundle and operator images as well as the existing released version chain



<!--
Enter your extended release note in the below block. If the PR requires
additional action from users switching to the new release, include the string
"action required". If no release note is required, write "NONE". 

You can (optionally) mark this PR with labels "kind/bug" or "kind/feature" to make sure
the text is added to the right section of the release notes. 
-->

**Release Note**
```release-note
NONE
```


tadayosi: @phantomjinx `upgrade` is constantly failing. Could it be related to the changes made with the fix?
```
home/runner/go/bin/operator-sdk bundle validate ./bundle
time="2022-07-21T03:37:16Z" level=info msg="All validation tests have completed successfully"
cd bundle && docker build -f Dockerfile -t kind-registry:5000/apache/camel-k-bundle:1.10.0-SNAPSHOT:1.10.0 .
invalid argument "kind-registry:5000/apache/camel-k-bundle:1.10.0-SNAPSHOT:1.10.0" for "-t, --tag" flag: invalid reference format
See 'docker build --help'.
make: *** [Makefile:606: bundle-build] Error 125
Error: Process completed with exit code 2.
```

Patch:
diff --git a/.github/actions/kamel-build-bundle/build-bundle-image.sh b/.github/actions/kamel-build-bundle/build-bundle-image.sh
index 217c0cdf9..0bd0edb68 100755
--- a/.github/actions/kamel-build-bundle/build-bundle-image.sh
+++ b/.github/actions/kamel-build-bundle/build-bundle-image.sh
@@ -102,26 +102,28 @@ popd
 #
 # Build with the PUSH host to ensure the correct image:tag
 # for docker to push the image.
+# Note: the CUSTOM_IMAGE must still reference the pull host
+# to allow the operator image to be found
 #
-export LOCAL_IMAGE_BUNDLE=${REGISTRY_PUSH_HOST}/${IMAGE_NAMESPACE}/camel-k-bundle:${IMAGE_VERSION}
+BUNDLE_IMAGE_BASE_NAME=$(basename $(make get-bundle-image))
+export PUSH_BUNDLE_LOCAL_IMAGE=${REGISTRY_PUSH_HOST}/${IMAGE_NAMESPACE}/${BUNDLE_IMAGE_BASE_NAME}
 export CUSTOM_IMAGE=${IMAGE_NAME}
+export CUSTOM_VERSION=${IMAGE_VERSION}
 
 export PREV_XY_CHANNEL="stable-$(make get-last-released-version | grep -Po '\d+\.\d+')"
 echo "PREV_XY_CHANNEL=${PREV_XY_CHANNEL}" >> $GITHUB_ENV
 export NEW_XY_CHANNEL=stable-dev-$(make get-version | grep -Po "\d+\.\d+")
 echo "NEW_XY_CHANNEL=${NEW_XY_CHANNEL}" >> $GITHUB_ENV
 
-make bundle-build \
-  BUNDLE_IMAGE_NAME=${LOCAL_IMAGE_BUNDLE} \
+make bundle-push \
+  BUNDLE_IMAGE_NAME="${PUSH_BUNDLE_LOCAL_IMAGE}" \
   DEFAULT_CHANNEL="${NEW_XY_CHANNEL}" \
   CHANNELS="${NEW_XY_CHANNEL}"
 
-docker push ${LOCAL_IMAGE_BUNDLE}
-
 #
 # Use the PULL host to ensure the correct image:tag
 # is passed into the tests for the deployment to pull from
 #
-BUILD_BUNDLE_LOCAL_IMAGE="${REGISTRY_PULL_HOST}/${IMAGE_NAMESPACE}/camel-k-bundle:${IMAGE_VERSION}"
-echo "Setting build-bundle-local-image to ${BUILD_BUNDLE_LOCAL_IMAGE}"
-echo "::set-output name=build-bundle-local-image::${BUILD_BUNDLE_LOCAL_IMAGE}"
+PULL_BUNDLE_LOCAL_IMAGE="${REGISTRY_PULL_HOST}/${IMAGE_NAMESPACE}/${BUNDLE_IMAGE_BASE_NAME}:${IMAGE_VERSION}"
+echo "Setting build-bundle-local-image to ${PULL_BUNDLE_LOCAL_IMAGE}"
+echo "::set-output name=build-bundle-local-image::${PULL_BUNDLE_LOCAL_IMAGE}"
diff --git a/.github/actions/kamel-build-bundle/build-index-image.sh b/.github/actions/kamel-build-bundle/build-index-image.sh
index 0078928fc..c8e61963e 100755
--- a/.github/actions/kamel-build-bundle/build-index-image.sh
+++ b/.github/actions/kamel-build-bundle/build-index-image.sh
@@ -172,8 +172,11 @@ if [ "${PULL_REGISTRY}" != "${PUSH_REGISTRY}" ]; then
   # Tag the bundle image
   #
   echo "Tagging bundle image ..."
+  IMAGE_BASE_NAME=$(basename ${IMAGE_NAME})
+  export CUSTOM_IMAGE=${PUSH_REGISTRY}/${IMAGE_NAMESPACE}/${IMAGE_BASE_NAME}
+  export CUSTOM_VERSION=${IMAGE_VERSION}
   docker tag \
-    ${PUSH_REGISTRY}/${IMAGE_NAMESPACE}/camel-k-bundle:${IMAGE_VERSION} \
+    $(make get-bundle-image):${CUSTOM_VERSION} \
     ${BUNDLE_IMAGE}
 
   # Push the bundle image to the registry
diff --git a/.gitignore b/.gitignore
index 3c195619b..75e386502 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,9 +23,9 @@
 # Bundle directory
 /bundle
 
-# Catalog directory
-/catalog
-catalog.Dockerfile
+# Bundle Index directory
+/index
+index.Dockerfile
 
 # IDEs
 .idea
diff --git a/go.mod b/go.mod
index df0000a5c..9af171225 100644
--- a/go.mod
+++ b/go.mod
@@ -57,7 +57,7 @@ require (
 	k8s.io/gengo v0.0.0-20211129171323-c02415ce4185
 	k8s.io/klog/v2 v2.40.1
 	k8s.io/kubectl v0.22.5
-	k8s.io/utils v0.0.0-20211208161948-7d6a63dca704
+	k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e
 	knative.dev/eventing v0.30.3
 	knative.dev/pkg v0.0.0-20220301181942-2fdd5f232e77
 	knative.dev/serving v0.30.0
@@ -111,7 +111,7 @@ require (
 	github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
 	github.com/hashicorp/golang-lru v0.5.4 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kelseyhightower/envconfig v1.4.0 // indirect
diff --git a/go.sum b/go.sum
index 3a4c4fa9f..ea70353e5 100644
--- a/go.sum
+++ b/go.sum
@@ -799,8 +799,9 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
 github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/tdigest v0.0.0-20180711151920-a7d76c6f093a/go.mod h1:9GkyshztGufsdPQWjH+ifgnIr3xNUL5syI70g2dzU1o=
@@ -2137,8 +2138,9 @@ k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
 k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 h1:ZKMMxTvduyf5WUtREOqg5LiXaN1KO/+0oOQPRFrClpo=
 k8s.io/utils v0.0.0-20211208161948-7d6a63dca704/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e h1:W1yba+Bpkwb5BatGKZALQ1yylhwnuD6CkYmrTibyLDM=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 knative.dev/caching v0.0.0-20220302153644-5a6403ef2de5/go.mod h1:52//2yvw0iO3Nu2duAMI5BzfQKHWiXZuvRvzaypoGkU=
 knative.dev/eventing v0.30.3 h1:phw3Uw/iSQGo3BJokCbfb0+zpbwwbYdg2w+UKE46lk0=
 knative.dev/eventing v0.30.3/go.mod h1:JCqC8evTls1WiBTmYhPEGME6OQD8HFnLQjFPImwOEOg=
diff --git a/pkg/apis/camel/go.mod b/pkg/apis/camel/go.mod
index d825b8b33..8b6e3d2a3 100644
--- a/pkg/apis/camel/go.mod
+++ b/pkg/apis/camel/go.mod
@@ -3,9 +3,11 @@ module github.com/apache/camel-k/pkg/apis/camel
 go 1.17
 
 require (
+	github.com/imdario/mergo v0.3.13
 	github.com/stretchr/testify v1.7.0
 	k8s.io/api v0.22.5
 	k8s.io/apimachinery v0.22.5
+	k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e
 )
 
 require (
@@ -22,7 +24,7 @@ require (
 	golang.org/x/text v0.3.6 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.0 // indirect
 	k8s.io/klog/v2 v2.9.0 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
 )
diff --git a/pkg/apis/camel/go.sum b/pkg/apis/camel/go.sum
index 4c4d03587..20a0319db 100644
--- a/pkg/apis/camel/go.sum
+++ b/pkg/apis/camel/go.sum
@@ -54,6 +54,8 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -91,6 +93,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -201,8 +204,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 k8s.io/api v0.22.5 h1:xk7C+rMjF/EGELiD560jdmwzrB788mfcHiNbMQLIVI8=
@@ -214,6 +218,8 @@ k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM=
 k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
 k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e h1:W1yba+Bpkwb5BatGKZALQ1yylhwnuD6CkYmrTibyLDM=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
diff --git a/pkg/apis/camel/v1/common_types_support.go b/pkg/apis/camel/v1/common_types_support.go
index 7f071a834..1726b5c4b 100644
--- a/pkg/apis/camel/v1/common_types_support.go
+++ b/pkg/apis/camel/v1/common_types_support.go
@@ -24,6 +24,8 @@ import (
 	"strings"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/imdario/mergo"
 )
 
 func (in *Artifact) String() string {
@@ -59,6 +61,44 @@ func (p TraitProfile) Equal(other TraitProfile) bool {
 	return strings.EqualFold(string(p), string(other))
 }
 
+// Merge merges the given Traits into the receiver.
+func (t *Traits) Merge(other Traits) error {
+	// marshal both
+	data1, err := json.Marshal(t)
+	if err != nil {
+		return err
+	}
+	data2, err := json.Marshal(other)
+	if err != nil {
+		return err
+	}
+
+	// merge them
+	map1 := make(map[string]interface{})
+	if err := json.Unmarshal(data1, &map1); err != nil {
+		return err
+	}
+	map2 := make(map[string]interface{})
+	if err := json.Unmarshal(data2, &map2); err != nil {
+		return err
+	}
+	// values from merged trait take precedence over the original ones
+	if err := mergo.Merge(&map1, map2, mergo.WithOverride); err != nil {
+		return err
+	}
+
+	// unmarshal it
+	data, err := json.Marshal(map1)
+	if err != nil {
+		return err
+	}
+	if err = json.Unmarshal(data, &t); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // MarshalJSON returns m as the JSON encoding of m.
 func (m RawMessage) MarshalJSON() ([]byte, error) {
 	if m == nil {
diff --git a/pkg/client/camel/go.mod b/pkg/client/camel/go.mod
index 9e5ed510a..2217e517e 100644
--- a/pkg/client/camel/go.mod
+++ b/pkg/client/camel/go.mod
@@ -18,6 +18,7 @@ require (
 	github.com/google/go-cmp v0.5.5 // indirect
 	github.com/google/gofuzz v1.1.0 // indirect
 	github.com/googleapis/gnostic v0.5.5 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/json-iterator/go v1.1.11 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.1 // indirect
@@ -32,10 +33,10 @@ require (
 	google.golang.org/protobuf v1.26.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.0 // indirect
 	k8s.io/klog/v2 v2.9.0 // indirect
 	k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
-	k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
+	k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
 	sigs.k8s.io/yaml v1.2.0 // indirect
 )
diff --git a/pkg/client/camel/go.sum b/pkg/client/camel/go.sum
index 5042fd6e3..64887de67 100644
--- a/pkg/client/camel/go.sum
+++ b/pkg/client/camel/go.sum
@@ -125,6 +125,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -419,8 +421,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -439,8 +442,9 @@ k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM=
 k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
 k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c h1:jvamsI1tn9V0S8jicyX82qaFC0H/NKxv2e5mbqsgR80=
 k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
-k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g=
 k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e h1:W1yba+Bpkwb5BatGKZALQ1yylhwnuD6CkYmrTibyLDM=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/pkg/cmd/modeline.go b/pkg/cmd/modeline.go
index 1b7ed9e1f..07e88155b 100644
--- a/pkg/cmd/modeline.go
+++ b/pkg/cmd/modeline.go
@@ -196,7 +196,7 @@ func extractModelineOptions(ctx context.Context, sources []string, cmd *cobra.Co
 
 	resolvedSources, err := ResolveSources(ctx, sources, false, cmd)
 	if err != nil {
-		return opts, errors.Wrap(err, "cannot read sources")
+		return opts, errors.Wrap(err, "failed to resolve sources")
 	}
 
 	for _, resolvedSource := range resolvedSources {
@@ -217,7 +217,7 @@ func extractModelineOptions(ctx context.Context, sources []string, cmd *cobra.Co
 }
 
 func extractModelineOptionsFromSource(resolvedSource Source) ([]modeline.Option, error) {
-	ops, err := modeline.Parse(resolvedSource.Location, resolvedSource.Content)
+	ops, err := modeline.Parse(resolvedSource.Name, resolvedSource.Content)
 	if err != nil {
 		return ops, errors.Wrapf(err, "cannot process file %s", resolvedSource.Location)
 	}
diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go
index 4eb216c89..bf81e3e81 100644
--- a/pkg/cmd/promote.go
+++ b/pkg/cmd/promote.go
@@ -20,6 +20,7 @@ package cmd
 import (
 	"context"
 	"fmt"
+	"reflect"
 	"regexp"
 	"strings"
 
@@ -194,43 +195,61 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr
 	for t, v := range mount {
 		switch t {
 		case "configs":
-			if list, ok := v.([]string); ok {
-				for _, cn := range list {
-					if conf, parseErr := resource.ParseConfig(cn); 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
+			list, ok := v.([]interface{})
+			if !ok {
+				return fmt.Errorf("invalid %s type: %s, value: %s", t, reflect.TypeOf(v), v)
+			}
+			for _, cn := range list {
+				s, ok := cn.(string)
+				if !ok {
+					return fmt.Errorf("invalid %s type: %s, value: %s", t, reflect.TypeOf(cn), cn)
+				}
+				if conf, parseErr := resource.ParseConfig(s); 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
 				}
 			}
 		case "resources":
-			if list, ok := v.([]string); ok {
-				for _, cn := range list {
-					if conf, parseErr := resource.ParseResource(cn); 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
+			list, ok := v.([]interface{})
+			if !ok {
+				return fmt.Errorf("invalid %s type: %s, value: %s", t, reflect.TypeOf(v), v)
+			}
+			for _, cn := range list {
+				s, ok := cn.(string)
+				if !ok {
+					return fmt.Errorf("invalid %s type: %s, value: %s", t, reflect.TypeOf(cn), cn)
+				}
+				if conf, parseErr := resource.ParseResource(s); 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
 				}
 			}
 		case "volumes":
-			if list, ok := v.([]string); ok {
-				for _, cn := range list {
-					if conf, parseErr := resource.ParseVolume(cn); parseErr == nil {
-						if conf.StorageType() == resource.StorageTypePVC {
-							pvcs = append(pvcs, conf.Name())
-						}
-					} else {
-						return parseErr
+			list, ok := v.([]interface{})
+			if !ok {
+				return fmt.Errorf("invalid %s type: %s, value: %s", t, reflect.TypeOf(v), v)
+			}
+			for _, cn := range list {
+				s, ok := cn.(string)
+				if !ok {
+					return fmt.Errorf("invalid %s type: %s, value: %s", t, reflect.TypeOf(cn), cn)
+				}
+				if conf, parseErr := resource.ParseVolume(s); parseErr == nil {
+					if conf.StorageType() == resource.StorageTypePVC {
+						pvcs = append(pvcs, conf.Name())
 					}
+				} else {
+					return parseErr
 				}
 			}
 		}
@@ -270,25 +289,25 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr
 	for _, name := range configmaps {
 		if !existsCm(o.Context, c, name, o.To) {
 			anyError = true
-			errorTrace += fmt.Sprintf("Configmap %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("\n\tConfigmap %s is missing from %s namespace", name, o.To)
 		}
 	}
 	for _, name := range secrets {
 		if !existsSecret(o.Context, c, name, o.To) {
 			anyError = true
-			errorTrace += fmt.Sprintf("Secret %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("\n\tSecret %s is missing from %s namespace", name, o.To)
 		}
 	}
 	for _, name := range pvcs {
 		if !existsPv(o.Context, c, name, o.To) {
 			anyError = true
-			errorTrace += fmt.Sprintf("PersistentVolume %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("\n\tPersistentVolume %s is missing from %s namespace", name, o.To)
 		}
 	}
 	for _, name := range kamelets {
 		if !existsKamelet(o.Context, c, name, o.To) {
 			anyError = true
-			errorTrace += fmt.Sprintf("Kamelet %s is missing from %s namespace\n", name, o.To)
+			errorTrace += fmt.Sprintf("\n\tKamelet %s is missing from %s namespace", name, o.To)
 		}
 	}
 
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index 79aab3856..e84a628e6 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -920,9 +920,8 @@ func getMountPath(targetPath string, dirName string, path string) (string, error
 	return filepath.Join(targetPath, localRelativePath), nil
 }
 
-// nolint:errcheck
 func (o *runCmdOptions) uploadPomFromJar(gav maven.Dependency, path string, platform *v1.IntegrationPlatform, ns string, options spectrum.Options, cmd *cobra.Command) maven.Dependency {
-	util.WithTempDir("camel-k", func(tmpDir string) error {
+	_ = util.WithTempDir("camel-k", func(tmpDir string) error {
 		pomPath := filepath.Join(tmpDir, "pom.xml")
 		jar, err := zip.OpenReader(path)
 		if err != nil {
@@ -954,7 +953,7 @@ func (o *runCmdOptions) uploadPomFromJar(gav maven.Dependency, path string, plat
 			} else {
 				gav.Type = "pom"
 				// Swallow error as this is not a mandatory step
-				o.uploadAsMavenArtifact(gav, pomPath, platform, ns, options, cmd)
+				_ = o.uploadAsMavenArtifact(gav, pomPath, platform, ns, options, cmd)
 			}
 		}
 		return nil
diff --git a/pkg/cmd/util_sources.go b/pkg/cmd/util_sources.go
index 991bf9eab..b15e66912 100644
--- a/pkg/cmd/util_sources.go
+++ b/pkg/cmd/util_sources.go
@@ -35,7 +35,7 @@ import (
 	"github.com/pkg/errors"
 )
 
-// Source ---.
+// Source represents the source file of an Integration.
 type Source struct {
 	Origin   string
 	Location string
@@ -60,7 +60,7 @@ func (s *Source) setContent(content []byte) error {
 	return nil
 }
 
-// ResolveSources ---.
+// ResolveSources resolves sources from a variety of locations including local and remote.
 func ResolveSources(ctx context.Context, locations []string, compress bool, cmd *cobra.Command) ([]Source, error) {
 	sources := make([]Source, 0, len(locations))
 
@@ -71,7 +71,7 @@ func ResolveSources(ctx context.Context, locations []string, compress bool, cmd
 		}
 
 		if ok {
-			answer, err := ResolveLocalSource(location, compress)
+			answer, err := resolveLocalSource(location, compress)
 			if err != nil {
 				return sources, err
 			}
@@ -85,101 +85,26 @@ func ResolveSources(ctx context.Context, locations []string, compress bool, cmd
 
 			switch {
 			case u.Scheme == gistScheme || strings.HasPrefix(location, "https://gist.github.com/"):
-				var tc *http.Client
-
-				if token, ok := os.LookupEnv("GITHUB_TOKEN"); ok {
-					ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
-					tc = oauth2.NewClient(ctx, ts)
-
-					fmt.Fprintln(cmd.OutOrStdout(), "GITHUB_TOKEN env var detected, using it for GitHub APIs authentication")
-				}
-
-				gc := github.NewClient(tc)
-				gistID := ""
-
-				if strings.HasPrefix(location, "https://gist.github.com/") {
-					names := util.FindNamedMatches(`^https://gist.github.com/(([a-zA-Z0-9]*)/)?(?P<gistid>[a-zA-Z0-9]*)$`, location)
-					if value, ok := names["gistid"]; ok {
-						gistID = value
-					}
-				} else {
-					gistID = u.Opaque
-				}
-
-				if gistID == "" {
-					return sources, fmt.Errorf("unable to determining gist id from %s", location)
-				}
-
-				gists, _, err := gc.Gists.Get(ctx, gistID)
+				answer, err := resolveGistSource(ctx, location, compress, cmd, u)
 				if err != nil {
 					return sources, err
 				}
-
-				for _, v := range gists.Files {
-					if v.Filename == nil || v.Content == nil {
-						continue
-					}
-
-					answer := Source{
-						Name:     *v.Filename,
-						Compress: compress,
-						Origin:   location,
-					}
-					if v.RawURL != nil {
-						answer.Location = *v.RawURL
-					}
-					if err := answer.setContent([]byte(*v.Content)); err != nil {
-						return sources, err
-					}
-					sources = append(sources, answer)
-				}
+				sources = append(sources, answer...)
 			case u.Scheme == githubScheme:
-				answer := Source{
-					Name:     path.Base(location),
-					Origin:   location,
-					Location: location,
-					Compress: compress,
-				}
-
-				content, err := loadContentGitHub(ctx, u)
-				if err != nil {
-					return sources, err
-				}
-				if err := answer.setContent(content); err != nil {
-					return sources, err
-				}
-				sources = append(sources, answer)
-			case u.Scheme == httpScheme:
-				answer := Source{
-					Name:     path.Base(location),
-					Origin:   location,
-					Location: location,
-					Compress: compress,
-				}
-
-				content, err := loadContentHTTP(ctx, u)
+				answer, err := resolveSource(location, compress, func() ([]byte, error) {
+					return loadContentGitHub(ctx, u)
+				})
 				if err != nil {
 					return sources, err
 				}
-				if err := answer.setContent(content); err != nil {
-					return sources, err
-				}
 				sources = append(sources, answer)
-			case u.Scheme == httpsScheme:
-				answer := Source{
-					Name:     path.Base(location),
-					Origin:   location,
-					Location: location,
-					Compress: compress,
-				}
-
-				content, err := loadContentHTTP(ctx, u)
+			case u.Scheme == httpScheme || u.Scheme == httpsScheme:
+				answer, err := resolveSource(location, compress, func() ([]byte, error) {
+					return loadContentHTTP(ctx, u)
+				})
 				if err != nil {
 					return sources, err
 				}
-				if err := answer.setContent(content); err != nil {
-					return sources, err
-				}
 				sources = append(sources, answer)
 			default:
 				return sources, fmt.Errorf("missing file or unsupported scheme in %s", location)
@@ -190,23 +115,95 @@ func ResolveSources(ctx context.Context, locations []string, compress bool, cmd
 	return sources, nil
 }
 
-// ResolveLocalSource --.
-func ResolveLocalSource(location string, compress bool) (Source, error) {
+// resolveGistSource resolves sources from a Gist.
+func resolveGistSource(ctx context.Context, location string, compress bool, cmd *cobra.Command, u *url.URL) ([]Source, error) {
+	var hc *http.Client
+
+	if token, ok := os.LookupEnv("GITHUB_TOKEN"); ok {
+		ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
+		hc = oauth2.NewClient(ctx, ts)
+
+		fmt.Fprintln(cmd.OutOrStdout(), "GITHUB_TOKEN env var detected, using it for GitHub APIs authentication")
+	}
+
+	gc := github.NewClient(hc)
+	gistID := ""
+
+	if strings.HasPrefix(location, "https://gist.github.com/") {
+		names := util.FindNamedMatches(`^https://gist.github.com/(([a-zA-Z0-9]*)/)?(?P<gistid>[a-zA-Z0-9]*)$`, location)
+		if value, ok := names["gistid"]; ok {
+			gistID = value
+		}
+	} else {
+		gistID = u.Opaque
+	}
+
+	if gistID == "" {
+		return []Source{}, fmt.Errorf("unable to determining gist id from %s", location)
+	}
+
+	gists, _, err := gc.Gists.Get(ctx, gistID)
+	if err != nil {
+		return []Source{}, err
+	}
+
+	sources := make([]Source, 0, len(gists.Files))
+	for _, v := range gists.Files {
+		if v.Filename == nil || v.Content == nil {
+			continue
+		}
+
+		answer := Source{
+			Name:     *v.Filename,
+			Compress: compress,
+			Origin:   location,
+		}
+		if v.RawURL != nil {
+			answer.Location = *v.RawURL
+		}
+		if err := answer.setContent([]byte(*v.Content)); err != nil {
+			return sources, err
+		}
+		sources = append(sources, answer)
+	}
+
+	return sources, nil
+}
+
+// resolveLocalSource resolves a source from the local file system.
+func resolveLocalSource(location string, compress bool) (Source, error) {
 	if _, err := os.Stat(location); err != nil && os.IsNotExist(err) {
 		return Source{}, errors.Wrapf(err, "file %s does not exist", location)
 	} else if err != nil {
 		return Source{}, errors.Wrapf(err, "error while accessing file %s", location)
 	}
 
+	answer, err := resolveSource(location, compress, func() ([]byte, error) {
+		return util.ReadFile(location)
+	})
+	if err != nil {
+		return Source{}, err
+	}
+	answer.Local = true
+
+	return answer, nil
+}
+
+// resolveSource resolves a source using the content provider function.
+func resolveSource(location string, compress bool, loadContent func() ([]byte, error)) (Source, error) {
+	// strip query part from location if any
+	locPath := util.SubstringBefore(location, "?")
+	if locPath == "" {
+		locPath = location
+	}
 	answer := Source{
-		Name:     path.Base(location),
+		Name:     path.Base(locPath),
 		Origin:   location,
 		Location: location,
 		Compress: compress,
-		Local:    true,
 	}
 
-	content, err := util.ReadFile(location)
+	content, err := loadContent()
 	if err != nil {
 		return Source{}, err
 	}
diff --git a/pkg/controller/kameletbinding/integration.go b/pkg/controller/kameletbinding/integration.go
index cde6da195..3f988d595 100644
--- a/pkg/controller/kameletbinding/integration.go
+++ b/pkg/controller/kameletbinding/integration.go
@@ -213,7 +213,9 @@ func configureBinding(integration *v1.Integration, bindings ...*bindings.Binding
 		if b == nil {
 			continue
 		}
-		integration.Spec.Traits = b.Traits
+		if err := integration.Spec.Traits.Merge(b.Traits); err != nil {
+			return err
+		}
 		for k, v := range b.ApplicationProperties {
 			entry, err := property.EncodePropertyFileEntry(k, v)
 			if err != nil {
diff --git a/pkg/kamelet/repository/go.mod b/pkg/kamelet/repository/go.mod
index 1c7f75d57..05ff34bed 100644
--- a/pkg/kamelet/repository/go.mod
+++ b/pkg/kamelet/repository/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/google/gofuzz v1.1.0 // indirect
 	github.com/googleapis/gnostic v0.5.5 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/json-iterator/go v1.1.11 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.1 // indirect
@@ -37,12 +38,12 @@ require (
 	google.golang.org/protobuf v1.26.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.0 // indirect
 	k8s.io/api v0.22.5 // indirect
 	k8s.io/client-go v0.22.5 // indirect
 	k8s.io/klog/v2 v2.9.0 // indirect
 	k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
-	k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
+	k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
 	sigs.k8s.io/yaml v1.2.0 // indirect
 )
diff --git a/pkg/kamelet/repository/go.sum b/pkg/kamelet/repository/go.sum
index f81275cbe..3f115878d 100644
--- a/pkg/kamelet/repository/go.sum
+++ b/pkg/kamelet/repository/go.sum
@@ -129,6 +129,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -424,8 +426,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -444,8 +447,9 @@ k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM=
 k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
 k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c h1:jvamsI1tn9V0S8jicyX82qaFC0H/NKxv2e5mbqsgR80=
 k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
-k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g=
 k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e h1:W1yba+Bpkwb5BatGKZALQ1yylhwnuD6CkYmrTibyLDM=
+k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 55e688fd7..c68f27717 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -199,6 +199,15 @@ func SubstringFrom(s string, substr string) string {
 	return ""
 }
 
+func SubstringBefore(s string, substr string) string {
+	index := strings.LastIndex(s, substr)
+	if index != -1 {
+		return s[:index]
+	}
+
+	return ""
+}
+
 const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 
 const (
diff --git a/script/Makefile b/script/Makefile
index c5cd2327b..878e90025 100644
--- a/script/Makefile
+++ b/script/Makefile
@@ -15,7 +15,6 @@
 
 VERSIONFILE := pkg/util/defaults/defaults.go
 VERSION ?= 1.10.0-SNAPSHOT
-OPERATOR_VERSION := $(subst -SNAPSHOT,,$(VERSION))
 LAST_RELEASED_IMAGE_NAME := camel-k-operator
 LAST_RELEASED_VERSION := 1.9.2
 RUNTIME_VERSION := 1.13.0
@@ -40,6 +39,7 @@ IMAGE_NAME := docker.io/apache/camel-k
 #
 CUSTOM_IMAGE ?= $(IMAGE_NAME)
 CUSTOM_VERSION ?= $(VERSION)
+OPERATOR_VERSION := $(subst -SNAPSHOT,,$(CUSTOM_VERSION))
 
 METADATA_IMAGE_NAME := $(CUSTOM_IMAGE)-metadata
 BUNDLE_IMAGE_NAME ?= $(CUSTOM_IMAGE)-bundle
@@ -50,20 +50,27 @@ LINT_DEADLINE := 10m
 
 
 # olm bundle vars
-CHANNELS ?= $(OPERATOR_VERSION),candidate,stable,latest
-DEFAULT_CHANNEL ?= $(OPERATOR_VERSION)
+MANAGER := config/manager
+MANIFESTS := config/manifests
+CHANNELS ?= $(shell v=$(OPERATOR_VERSION) && echo "stable-$${v%\.[0-9]}"),candidate,latest
+DEFAULT_CHANNEL ?= $(shell v=$(OPERATOR_VERSION) && echo "stable-$${v%\.[0-9]}")
 PACKAGE := camel-k
-CSV_VERSION := $(CUSTOM_VERSION:-SNAPSHOT=)
+CSV_VERSION := $(OPERATOR_VERSION)
 CSV_NAME := $(PACKAGE).v$(CSV_VERSION)
 # Final CSV name that replaces the name required by the operator-sdk
 # Has to be replaced after the bundle has been generated
-CSV_PRODUCTION_NAME := $(PACKAGE)-operator.v$(CSV_VERSION)
+CSV_PRODUCTION_NAME := $(LAST_RELEASED_IMAGE_NAME).v$(CSV_VERSION)
 CSV_DISPLAY_NAME := Camel K Operator
 CSV_SUPPORT := Camel
 CSV_REPLACES := $(LAST_RELEASED_IMAGE_NAME).v$(LAST_RELEASED_VERSION)
 CSV_FILENAME := $(PACKAGE).clusterserviceversion.yaml
-CSV_PATH := config/manifests/bases/$(CSV_FILENAME)
-CSV_GENERATED_PATH := bundle/manifests/$(CSV_FILENAME)
+CSV_PATH := $(MANIFESTS)/bases/$(CSV_FILENAME)
+CSV_PRODUCTION_PATH := bundle/manifests/$(CSV_FILENAME)
+
+# Test Bundle Index
+BUNDLE_INDEX := quay.io/operatorhubio/catalog:latest
+INDEX_DIR := index
+OPM := opm
 
 # Used to push pre-release artifacts
 STAGING_IMAGE_NAME := docker.io/camelk/camel-k
@@ -233,12 +240,13 @@ test: do-build
 # This should execute all tests and not exit if one of the suites fail
 #
 test-integration: do-build
-	STAGING_RUNTIME_REPO="$(STAGING_RUNTIME_REPO)" \
-	go test -timeout 60m -v ./e2e/global/common -tags=integration $(TEST_INTEGRATION_COMMON_RUN); \
-	go test -timeout 60m -v ./e2e/global/common/build -tags=integration $(TEST_INTEGRATION_COMMON_BUILD_RUN); \
-	go test -timeout 60m -v ./e2e/global/common/config -tags=integration $(TEST_INTEGRATION_COMMON_CONFIG_RUN); \
-	go test -timeout 60m -v ./e2e/global/common/languages -tags=integration $(TEST_INTEGRATION_COMMON_LANG_RUN); \
-	go test -timeout 60m -v ./e2e/global/common/traits -tags=integration $(TEST_INTEGRATION_COMMON_TRAITS_RUN)
+	FAILED=0; STAGING_RUNTIME_REPO="$(STAGING_RUNTIME_REPO)"; \
+	go test -timeout 60m -v ./e2e/global/common -tags=integration $(TEST_INTEGRATION_COMMON_RUN) || FAILED=1; \
+	go test -timeout 60m -v ./e2e/global/common/build -tags=integration $(TEST_INTEGRATION_COMMON_BUILD_RUN) || FAILED=1; \
+	go test -timeout 60m -v ./e2e/global/common/config -tags=integration $(TEST_INTEGRATION_COMMON_CONFIG_RUN) || FAILED=1; \
+	go test -timeout 60m -v ./e2e/global/common/languages -tags=integration $(TEST_INTEGRATION_COMMON_LANG_RUN) || FAILED=1; \
+	go test -timeout 60m -v ./e2e/global/common/traits -tags=integration $(TEST_INTEGRATION_COMMON_TRAITS_RUN) || FAILED=1; \
+	exit $${FAILED}
 
 test-knative: do-build
 	STAGING_RUNTIME_REPO="$(STAGING_RUNTIME_REPO)" \
@@ -267,10 +275,11 @@ test-registry-maven-wagon: do-build
 ###############################
 
 test-install: do-build
-	STAGING_RUNTIME_REPO="$(STAGING_RUNTIME_REPO)" \
-	go test -timeout 60m -v ./e2e/namespace/install/ -tags=integration $(TEST_INSTALL_RUN) \
-	go test -timeout 60m -v ./e2e/namespace/install/cli -tags=integration $(TEST_INSTALL_CLI_RUN); \
-	go test -timeout 60m -v ./e2e/namespace/install/kustomize -tags=integration $(TEST_INSTALL_KUSTOMIZE_RUN)
+	FAILED=0; STAGING_RUNTIME_REPO="$(STAGING_RUNTIME_REPO)"; \
+	go test -timeout 60m -v ./e2e/namespace/install/ -tags=integration $(TEST_INSTALL_RUN) || FAILED=1; \
+	go test -timeout 60m -v ./e2e/namespace/install/cli -tags=integration $(TEST_INSTALL_CLI_RUN) || FAILED=1; \
+	go test -timeout 60m -v ./e2e/namespace/install/kustomize -tags=integration $(TEST_INSTALL_KUSTOMIZE_RUN) || FAILED=1; \
+	exit $${FAILED}
 
 test-quarkus-native: do-build
 	STAGING_RUNTIME_REPO="$(STAGING_RUNTIME_REPO)" \
@@ -327,6 +336,7 @@ clean:
 	rm -rf camel-k-client-*.tar.gz
 	rm -rf camel-k-examples-*.tar.gz
 	rm -rf bundle
+	rm -rf $(INDEX_DIR) $(INDEX_DIR).Dockerfile
 
 dep:
 	@echo "Cleaning dependencies..."
@@ -335,6 +345,18 @@ dep:
 	cd pkg/client/camel && go mod tidy
 	cd pkg/kamelet/repository && go mod tidy
 
+detect-os:
+ifeq '$(findstring ;,$(PATH))' ';'
+OS := Windows
+OS_LOWER := windows
+else
+OS := $(shell echo $$(uname 2>/dev/null) || echo Unknown)
+OS := $(patsubst CYGWIN%,Cygwin,$(OS))
+OS := $(patsubst MSYS%,MSYS,$(OS))
+OS := $(patsubst MINGW%,MSYS,$(OS))
+OS_LOWER := $(shell echo $(OS) | tr '[:upper:]' '[:lower:]')
+endif
+
 lint:
 	GOGC=$(LINT_GOGC) golangci-lint run --config .golangci.yml --out-format tab --deadline $(LINT_DEADLINE) --verbose
 
@@ -413,6 +435,9 @@ get-last-released-version:
 get-csv-name:
 	@echo $(CSV_PRODUCTION_NAME)
 
+get-bundle-image:
+	@echo $(BUNDLE_IMAGE_NAME)
+
 set-version:
 	./script/set_version.sh $(CUSTOM_VERSION) $(CUSTOM_IMAGE)
 
@@ -451,7 +476,7 @@ install-minikube:
 get-staging-repo:
 	@echo $(or ${STAGING_RUNTIME_REPO},https://repository.apache.org/content/repositories/snapshots@id=apache-snapshots@snapshots)
 
-.PHONY: do-build build build-kamel build-resources dep codegen images images-dev images-push images-push-staging test check test-integration clean release cross-compile package-examples set-version git-tag release-notes check-licenses generate-deepcopy generate-client generate-doc build-resources release-helm release-staging release-nightly get-staging-repo get-version build-submodules set-module-version bundle-kamelets generate-keda generate-strimzi
+.PHONY: do-build build build-kamel build-resources dep detect-os codegen images images-dev images-push images-push-staging test check test-integration clean release cross-compile package-examples set-version git-tag release-notes check-licenses generate-deepcopy generate-client generate-doc build-resources release-helm release-staging release-nightly get-staging-repo get-version build-submodules set-module-version bundle-kamelets generate-keda generate-strimzi
 .PHONY: controller-gen kubectl kustomize operator-sdk
 .PHONY: kubectl kustomize operator-sdk opm
 
@@ -475,19 +500,13 @@ else
 KUSTOMIZE=$(shell command -v kustomize 2> /dev/null)
 endif
 
-operator-sdk:
+operator-sdk: detect-os
 ifeq (, $(shell command -v operator-sdk 2> /dev/null))
 	@{ \
 	set -e ;\
-	if [ "$(shell uname -s 2>/dev/null || echo Unknown)" == "Darwin" ] ; then \
-		curl \
-			-L https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_darwin_amd64 \
-			-o operator-sdk ; \
-	else \
-		curl \
-			-L https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_linux_amd64 \
-			-o operator-sdk ; \
-	fi ;\
+	curl \
+		-L https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$(OS_LOWER)_amd64 \
+		-o operator-sdk ; \
 	chmod +x operator-sdk ;\
 	mv operator-sdk $(GOBIN)/ ;\
 	}
@@ -575,9 +594,23 @@ bundle: set-version generate-crd $(BUNDLE_CAMEL_APIS) kustomize operator-sdk pre
 	./script/add_license.sh bundle/manifests ./script/headers/yaml.txt
 # Rename the CSV name to conform with the existing released operator versions
 # This cannot happen in pre-bundle as the operator-sdk generation expects a CSV name the same as PACKAGE
-	@sed -i "s/^  name: $(CSV_NAME)/  name: $(CSV_PRODUCTION_NAME)/" $(CSV_GENERATED_PATH)
+	@sed -i "s/^  name: $(CSV_NAME)/  name: $(CSV_PRODUCTION_NAME)/" $(CSV_PRODUCTION_PATH)
 	$(OPERATOR_SDK) bundle validate ./bundle
 
 # Build the bundle image.
 bundle-build: bundle
-	cd bundle && docker build -f Dockerfile -t $(BUNDLE_IMAGE_NAME) .
+	cd bundle && docker build -f Dockerfile -t $(BUNDLE_IMAGE_NAME):$(CUSTOM_VERSION) .
+
+.PHONY: bundle-push bundle-index
+
+bundle-push: bundle-build
+	docker push $(BUNDLE_IMAGE_NAME):$(CUSTOM_VERSION)
+
+#
+# Builds a test catalog index for installing the operator via an OLM
+#
+bundle-index: opm
+	BUNDLE_INDEX=$(BUNDLE_INDEX) INDEX_DIR=$(INDEX_DIR) PACKAGE=$(PACKAGE) \
+	OPM=$(OPM) BUNDLE_IMAGE=$(BUNDLE_IMAGE_NAME):$(CUSTOM_VERSION) CSV_NAME=$(CSV_PRODUCTION_NAME) \
+	CSV_SKIPS=$(CSV_SKIP_RANGE) CSV_REPLACES=$(CSV_REPLACES) CHANNEL="$(DEFAULT_CHANNEL)-dev" \
+	./script/build_bundle_index.sh
diff --git a/script/build_bundle_index.sh b/script/build_bundle_index.sh
new file mode 100755
index 000000000..cb6e71093
--- /dev/null
+++ b/script/build_bundle_index.sh
@@ -0,0 +1,129 @@
+#!/bin/bash
+
+check_env_var() {
+  if [ -z "${2}" ]; then
+    echo "Error: ${1} env var not defined"
+    exit 1
+  fi
+}
+
+check_env_var "BUNDLE_INDEX" ${BUNDLE_INDEX}
+check_env_var "INDEX_DIR" ${INDEX_DIR}
+check_env_var "PACKAGE" ${PACKAGE}
+check_env_var "OPM" ${OPM}
+check_env_var "BUNDLE_IMAGE" ${BUNDLE_IMAGE}
+check_env_var "CSV_NAME" ${CSV_NAME}
+check_env_var "CSV_REPLACES" ${CSV_REPLACES}
+check_env_var "CHANNEL" ${CHANNEL}
+
+PACKAGE_YAML=${INDEX_DIR}/${PACKAGE}.yaml
+
+if ! command -v ${OPM} &> /dev/null
+then
+  echo "Error: opm is not available. Was OPM env var defined correctly: ${OPM}"
+  exit 1
+fi
+
+if [ -n "${CSV_REPLACES}" ] && [ -n "${CSV_SKIPS}" ]; then
+  echo
+  echo "Both CSV_REPLACES and CSV_SKIPS have been specified."
+  while [ -z "${brs}" ]; do
+    read -p "Do you wish to include both (b), ignore 'replaces' (r) or ignore 'skips' (s): " brs
+    case ${brs} in
+        [Bb]* )
+          echo "... including both"
+          echo
+          ;;
+        [Rr]* )
+          echo ".. ignoring 'replaces'"
+          echo
+          CSV_REPLACES=""
+          ;;
+        [Ss]* )
+          echo ".. ignoring 'skips'"
+          echo
+          CSV_SKIPS=""
+          ;;
+        * )
+          echo "Please answer b, r or s."
+          echo
+          ;;
+    esac
+  done
+fi
+
+if [ -f "${INDEX_DIR}.Dockerfile" ]; then
+  rm -f "${INDEX_DIR}.Dockerfile"
+fi
+
+mkdir -p "${INDEX_DIR}"
+
+if [ ! -f ${INDEX_DIR}/bundles.yaml ]; then
+  ${OPM} render ${BUNDLE_INDEX} -o yaml > ${INDEX_DIR}/bundles.yaml
+  if [ $? != 0 ]; then
+    echo "Error: failed to render the base catalog"
+    exit 1
+  fi
+fi
+
+${OPM} render --skip-tls -o yaml \
+  ${BUNDLE_IMAGE} > ${PACKAGE_YAML}
+if [ $? != 0 ]; then
+  echo "Error: failed to render the ${PACKAGE} bundle catalog"
+  exit 1
+fi
+
+
+
+cat << EOF >> ${PACKAGE_YAML}
+---
+schema: olm.channel
+package: ${PACKAGE}
+name: ${CHANNEL}
+entries:
+  - name: ${CSV_NAME}
+EOF
+
+if [ -n "${CSV_REPLACES}" ]; then
+cat << EOF >> ${PACKAGE_YAML}
+    replaces: ${CSV_REPLACES}
+EOF
+fi
+
+if [ -n "${CSV_SKIPS}" ]; then
+cat << EOF >> ${PACKAGE_YAML}
+    skipRange: "\'${CSV_SKIPS}\'"
+EOF
+fi
+
+echo -n "Validating index ... "
+STATUS=$(${OPM} validate ${INDEX_DIR} 2>&1)
+if [ $? != 0 ]; then
+  echo "Failed"
+  echo "Error: ${STATUS}"
+  exit 1
+else
+  echo "OK"
+fi
+
+echo -n "Generating catalog dockerfile ... "
+STATUS=$(${OPM} generate dockerfile ${INDEX_DIR} 2>&1)
+if [ $? != 0 ]; then
+  echo "Failed"
+  echo "Error: ${STATUS}"
+  exit 1
+else
+  echo "OK"
+fi
+
+echo -n "Building catalog image ... "
+BUNDLE_INDEX_IMAGE="${BUNDLE_IMAGE%:*}-index":"${BUNDLE_IMAGE#*:}"
+STATUS=$(docker build . -f ${INDEX_DIR}.Dockerfile -t ${BUNDLE_INDEX_IMAGE} 2>&1)
+if [ $? != 0 ]; then
+  echo "Failed"
+  echo "Error: ${STATUS}"
+  exit 1
+else
+  echo "OK"
+  echo "Index image ${BUNDLE_INDEX_IMAGE} can be pushed"
+fi


Test patch:
diff --git a/e2e/global/builder/build_test.go b/e2e/global/builder/build_test.go
index 4e4abfb76..7a379ae2e 100644
--- a/e2e/global/builder/build_test.go
+++ b/e2e/global/builder/build_test.go
@@ -57,6 +57,11 @@ func TestKitKnativeFullBuild(t *testing.T) {
 }
 
 func TestKitTimerToLogFullNativeBuild(t *testing.T) {
+	if os.Getenv("CAMEL_K_CLUSTER_OCP3") == "true" {
+		t.Skip("INFO: Skipping test as known to never pass on OCP3")
+		return
+	}
+
 	doKitFullBuild(t, "timer-to-log", "4Gi", "15m0s", TestTimeoutVeryLong, kitOptions{
 		dependencies: []string{
 			"camel:timer", "camel:log",
diff --git a/e2e/global/common/config/config_test.go b/e2e/global/common/config/config_test.go
index 7dc60fe48..cda0c2990 100644
--- a/e2e/global/common/config/config_test.go
+++ b/e2e/global/common/config/config_test.go
@@ -73,13 +73,13 @@ func TestRunConfigExamples(t *testing.T) {
 		// Store a configmap on the cluster
 		var cmData = make(map[string]string)
 		cmData["my-configmap-key"] = "my-configmap-content"
-		NewPlainTextConfigmap(ns, "my-cm", cmData)
+		CreatePlainTextConfigmap(ns, "my-cm", cmData)
 
 		// Store a configmap with multiple values
 		var cmDataMulti = make(map[string]string)
 		cmDataMulti["my-configmap-key"] = "should-not-see-it"
 		cmDataMulti["my-configmap-key-2"] = "my-configmap-content-2"
-		NewPlainTextConfigmap(ns, "my-cm-multi", cmDataMulti)
+		CreatePlainTextConfigmap(ns, "my-cm-multi", cmDataMulti)
 
 		t.Run("Config configmap", func(t *testing.T) {
 			Expect(KamelRunWithID(operatorID, ns, "./files/config-configmap-route.groovy", "--config", "configmap:my-cm").Execute()).To(Succeed())
@@ -123,7 +123,7 @@ func TestRunConfigExamples(t *testing.T) {
 		// Store a configmap as property file
 		var cmDataProps = make(map[string]string)
 		cmDataProps["my.properties"] = "my.key.1=hello\nmy.key.2=world"
-		NewPlainTextConfigmap(ns, "my-cm-properties", cmDataProps)
+		CreatePlainTextConfigmap(ns, "my-cm-properties", cmDataProps)
 
 		t.Run("Config configmap as property file", func(t *testing.T) {
 			Expect(KamelRunWithID(operatorID, ns, "./files/config-configmap-properties-route.groovy", "--config", "configmap:my-cm-properties").Execute()).To(Succeed())
@@ -138,7 +138,7 @@ func TestRunConfigExamples(t *testing.T) {
 		// Store a secret on the cluster
 		var secData = make(map[string]string)
 		secData["my-secret-key"] = "very top secret"
-		NewPlainTextSecret(ns, "my-sec", secData)
+		CreatePlainTextSecret(ns, "my-sec", secData)
 
 		t.Run("Config secret", func(t *testing.T) {
 			Expect(KamelRunWithID(operatorID, ns, "./files/config-secret-route.groovy", "--config", "secret:my-sec").Execute()).To(Succeed())
diff --git a/e2e/global/common/languages/groovy_test.go b/e2e/global/common/languages/groovy_test.go
index 17f8c896a..5c35e67e3 100644
--- a/e2e/global/common/languages/groovy_test.go
+++ b/e2e/global/common/languages/groovy_test.go
@@ -47,7 +47,7 @@ func TestRunSimpleGroovyExamples(t *testing.T) {
 		})
 
 		t.Run("init run groovy", func(t *testing.T) {
-			RunInitGeneratedExample(camelv1.LanguageGroovy, ns, t)
+			RunInitGeneratedExample(t, operatorID, ns, camelv1.LanguageGroovy)
 		})
 	})
 }
diff --git a/e2e/global/common/languages/init_test_support.go b/e2e/global/common/languages/init_test_support.go
index 923c0badf..ea4c66901 100644
--- a/e2e/global/common/languages/init_test_support.go
+++ b/e2e/global/common/languages/init_test_support.go
@@ -36,13 +36,13 @@ import (
 	camelv1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 )
 
-func RunInitGeneratedExample(lang camelv1.Language, ns string, t *testing.T) {
+func RunInitGeneratedExample(t *testing.T, operatorID string, ns string, lang camelv1.Language) {
 	dir := util.MakeTempDir(t)
 	itName := fmt.Sprintf("init%s", string(lang))          // e.g. initjava
 	fileName := fmt.Sprintf("%s.%s", itName, string(lang)) // e.g. initjava.java
 	file := path.Join(dir, fileName)
 	Expect(Kamel("init", file).Execute()).To(Succeed())
-	Expect(KamelRun(ns, file).Execute()).To(Succeed())
+	Expect(KamelRunWithID(operatorID, ns, file).Execute()).To(Succeed())
 	Eventually(IntegrationPodPhase(ns, itName), TestTimeoutLong).Should(Equal(v1.PodRunning))
 	Eventually(IntegrationLogs(ns, itName), TestTimeoutShort).Should(ContainSubstring(languageInitExpectedString(lang)))
 	Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
diff --git a/e2e/global/common/languages/java_test.go b/e2e/global/common/languages/java_test.go
index ec7efd3c1..e498b87da 100644
--- a/e2e/global/common/languages/java_test.go
+++ b/e2e/global/common/languages/java_test.go
@@ -55,7 +55,7 @@ func TestRunSimpleJavaExamples(t *testing.T) {
 		})
 
 		t.Run("init run java", func(t *testing.T) {
-			RunInitGeneratedExample(camelv1.LanguageJavaSource, ns, t)
+			RunInitGeneratedExample(t, operatorID, ns, camelv1.LanguageJavaSource)
 		})
 	})
 }
diff --git a/e2e/global/common/languages/js_test.go b/e2e/global/common/languages/js_test.go
index daeaa3d7d..d7569499a 100644
--- a/e2e/global/common/languages/js_test.go
+++ b/e2e/global/common/languages/js_test.go
@@ -47,7 +47,7 @@ func TestRunSimpleJavaScriptExamples(t *testing.T) {
 		})
 
 		t.Run("init run JavaScript", func(t *testing.T) {
-			RunInitGeneratedExample(camelv1.LanguageJavaScript, ns, t)
+			RunInitGeneratedExample(t, operatorID, ns, camelv1.LanguageJavaScript)
 		})
 	})
 }
diff --git a/e2e/global/common/languages/kotlin_test.go b/e2e/global/common/languages/kotlin_test.go
index 6e43a8153..69cd58a2d 100644
--- a/e2e/global/common/languages/kotlin_test.go
+++ b/e2e/global/common/languages/kotlin_test.go
@@ -47,7 +47,7 @@ func TestRunSimpleKotlinExamples(t *testing.T) {
 		})
 
 		t.Run("init run Kotlin", func(t *testing.T) {
-			RunInitGeneratedExample(camelv1.LanguageKotlin, ns, t)
+			RunInitGeneratedExample(t, operatorID, ns, camelv1.LanguageKotlin)
 		})
 	})
 }
diff --git a/e2e/global/common/languages/xml_test.go b/e2e/global/common/languages/xml_test.go
index a332f0af3..04371cae4 100644
--- a/e2e/global/common/languages/xml_test.go
+++ b/e2e/global/common/languages/xml_test.go
@@ -47,7 +47,7 @@ func TestRunSimpleXmlExamples(t *testing.T) {
 		})
 
 		t.Run("init run xml", func(t *testing.T) {
-			RunInitGeneratedExample(camelv1.LanguageXML, ns, t)
+			RunInitGeneratedExample(t, operatorID, ns, camelv1.LanguageXML)
 		})
 	})
 }
diff --git a/e2e/global/common/languages/yaml_test.go b/e2e/global/common/languages/yaml_test.go
index 5a754e968..3a55ac330 100644
--- a/e2e/global/common/languages/yaml_test.go
+++ b/e2e/global/common/languages/yaml_test.go
@@ -47,7 +47,7 @@ func TestRunSimpleYamlExamples(t *testing.T) {
 		})
 
 		t.Run("init run yaml", func(t *testing.T) {
-			RunInitGeneratedExample(camelv1.LanguageYaml, ns, t)
+			RunInitGeneratedExample(t, operatorID, ns, camelv1.LanguageYaml)
 		})
 	})
 }
diff --git a/e2e/global/common/operator_metrics_test.go b/e2e/global/common/operator_metrics_test.go
index bd4658cb5..c0c1e9ed5 100644
--- a/e2e/global/common/operator_metrics_test.go
+++ b/e2e/global/common/operator_metrics_test.go
@@ -203,7 +203,7 @@ func TestMetrics(t *testing.T) {
 				"LoggerName":       Equal("camel-k.controller.integrationplatform"),
 				"Message":          Equal("Reconciling IntegrationPlatform"),
 				"RequestNamespace": Equal(ns),
-				"RequestName":      Equal("camel-k"),
+				"RequestName":      Equal(operatorID),
 			}))
 			Expect(err).To(BeNil())
 
diff --git a/e2e/global/common/traits/jvm_test.go b/e2e/global/common/traits/jvm_test.go
index b1ddae902..b26f46a9f 100644
--- a/e2e/global/common/traits/jvm_test.go
+++ b/e2e/global/common/traits/jvm_test.go
@@ -46,7 +46,7 @@ func TestJVMTrait(t *testing.T) {
 		source, err := ioutil.ReadFile("./files/jvm/sample-1.0.jar")
 		assert.Nil(t, err)
 		cmData["sample-1.0.jar"] = source
-		err = NewBinaryConfigmap(ns, "my-deps", cmData)
+		err = CreateBinaryConfigmap(ns, "my-deps", cmData)
 		assert.Nil(t, err)
 
 		t.Run("JVM trait classpath", func(t *testing.T) {
diff --git a/e2e/global/common/traits/openapi_test.go b/e2e/global/common/traits/openapi_test.go
index 61f2d3256..d9bf91303 100644
--- a/e2e/global/common/traits/openapi_test.go
+++ b/e2e/global/common/traits/openapi_test.go
@@ -93,7 +93,7 @@ func TestOpenAPIConfigmap(t *testing.T) {
 		assert.Nil(t, err)
 		var cmDataProps = make(map[string]string)
 		cmDataProps["petstore-api.yaml"] = string(openapiContent)
-		NewPlainTextConfigmap(ns, "my-openapi", cmDataProps)
+		CreatePlainTextConfigmap(ns, "my-openapi", cmDataProps)
 
 		Expect(KamelRunWithID(operatorID, ns,
 			"--name", "petstore",
diff --git a/e2e/namespace/install/cli/describe_test.go b/e2e/namespace/install/cli/describe_test.go
index b953b974a..c0fdc839d 100644
--- a/e2e/namespace/install/cli/describe_test.go
+++ b/e2e/namespace/install/cli/describe_test.go
@@ -70,7 +70,7 @@ func TestKamelCliDescribe(t *testing.T) {
 		})
 
 		t.Run("Test kamel describe integration platform", func(t *testing.T) {
-			platform := GetOutputString(Kamel("describe", "platform", "camel-k", "-n", ns))
+			platform := GetOutputString(Kamel("describe", "platform", operatorID, "-n", ns))
 			r, _ := regexp.Compile("(?sm).*Name:\\s+camel-k.*")
 			Expect(platform).To(MatchRegexp(r.String()))
 
diff --git a/e2e/namespace/install/cli/dev_mode_test.go b/e2e/namespace/install/cli/dev_mode_test.go
index 5decd8849..96b8d1d1e 100644
--- a/e2e/namespace/install/cli/dev_mode_test.go
+++ b/e2e/namespace/install/cli/dev_mode_test.go
@@ -135,7 +135,7 @@ func TestRunDevMode(t *testing.T) {
 			defer pipew.Close()
 			defer piper.Close()
 
-			file := util.MakeTempCopy(t, "../config/files/resource-file-location-route.groovy")
+			file := util.MakeTempCopy(t, "files/resource-file-location-route.groovy")
 
 			kamelRun := KamelRunWithContext(ctx, operatorID, ns, file, "--dev", "--resource", fmt.Sprintf("file:%s@/tmp/file.txt", tmpFile.Name()))
 			kamelRun.SetOut(pipew)
diff --git a/e2e/namespace/install/cli/duplicate_parameters_test.go b/e2e/namespace/install/cli/duplicate_parameters_test.go
index e6c52a21c..b3a114e1b 100644
--- a/e2e/namespace/install/cli/duplicate_parameters_test.go
+++ b/e2e/namespace/install/cli/duplicate_parameters_test.go
@@ -40,12 +40,17 @@ func TestDuplicateParameters(t *testing.T) {
 
 	// run kamel to output the traits/configuration structure in json format to check the processed values
 	// the tracing.enabled is false inside JavaDuplicateParams.java, so we can check the output of this trait as true.
-	cmdParams := []string{"kamel", "run", "files/JavaDuplicateParams.java", "-o", "json", "-t", "tracing.enabled=true", "--trait", "pull-secret.enabled=true", "--property", "prop1=true", "-p", "prop2=true"}
+	cmdParams := []string{"kamel", "run", "files/JavaDuplicateParams.java",
+		"-o", "json",
+		"-t", "tracing.enabled=true", "--trait", "pull-secret.enabled=true",
+		"--property", "prop1=true", "-p", "prop2=true",
+		"--force"}
 	comm, _, _ := cmd.NewKamelWithModelineCommand(ctx, cmdParams)
 
 	// the command is executed inside GetOutputString function
 	commOutput := GetOutputString(comm)
 
-	outParams := `"traits":{"affinity":{"enabled":true},"camel":{"properties":["prop1 = true","prop2 = true","foo = bar"]},"pull-secret":{"enabled":true},"addons":{"tracing":{"enabled":true}}}`
+	outParams :=
+		`"traits":{"affinity":{"enabled":true},"camel":{"properties":["prop1 = true","prop2 = true","foo = bar"]},"pull-secret":{"enabled":true},"addons":{"tracing":{"enabled":true}}}`
 	Expect(commOutput).To(ContainSubstring(outParams))
 }
diff --git a/e2e/namespace/install/cli/files/Java.java b/e2e/namespace/install/cli/files/Java.java
new file mode 100644
index 000000000..66fef5fe8
--- /dev/null
+++ b/e2e/namespace/install/cli/files/Java.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+import org.apache.camel.builder.RouteBuilder;
+
+public class Java extends RouteBuilder {
+  @Override
+  public void configure() throws Exception {
+	  from("timer:tick")
+	  .setHeader("m").constant("string!")
+	  .setBody().simple("Magic${header.m}")
+      .log("${body}");
+  }
+}
diff --git a/e2e/global/common/files/promote-route.groovy b/e2e/namespace/install/cli/files/promote-route.groovy
similarity index 100%
rename from e2e/global/common/files/promote-route.groovy
rename to e2e/namespace/install/cli/files/promote-route.groovy
diff --git a/e2e/global/common/files/timer-kamelet-usage.groovy b/e2e/namespace/install/cli/files/resource-file-location-route.groovy
similarity index 85%
rename from e2e/global/common/files/timer-kamelet-usage.groovy
rename to e2e/namespace/install/cli/files/resource-file-location-route.groovy
index 11ad5bb27..076a2a9c7 100644
--- a/e2e/global/common/files/timer-kamelet-usage.groovy
+++ b/e2e/namespace/install/cli/files/resource-file-location-route.groovy
@@ -1,3 +1,4 @@
+// camel-k: language=groovy
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
  * contributor license agreements.  See the NOTICE file distributed with
@@ -15,5 +16,5 @@
  * limitations under the License.
  */
 
-from('kamelet:my-own-timer-source?message=Hello+world')
-    .to('log:info?showAll=false')
+from('file:/tmp/?fileName=file.txt&noop=true&idempotent=false')
+    .log('resource file content is: ${body}')
diff --git a/e2e/namespace/install/cli/global_kamelet_test.go b/e2e/namespace/install/cli/global_kamelet_test.go
index b03f1d16b..6884769fe 100644
--- a/e2e/namespace/install/cli/global_kamelet_test.go
+++ b/e2e/namespace/install/cli/global_kamelet_test.go
@@ -47,7 +47,8 @@ func TestRunGlobalKamelet(t *testing.T) {
 	}
 
 	WithGlobalOperatorNamespace(t, func(operatorNamespace string) {
-		Expect(KamelInstall(operatorNamespace, "--global", "--force").Execute()).To(Succeed())
+		operatorID := "camel-k-global-kamelet"
+		Expect(KamelInstallWithID(operatorID, operatorNamespace, "--global", "--force").Execute()).To(Succeed())
 
 		t.Run("Global operator + namespaced kamelet test", func(t *testing.T) {
 
@@ -55,7 +56,6 @@ func TestRunGlobalKamelet(t *testing.T) {
 			WithNewTestNamespace(t, func(ns2 string) {
 				Expect(CreateTimerKamelet(ns2, "my-own-timer-source")()).To(Succeed())
 
-				operatorID := "camel-k-local-ns2"
 				Expect(KamelInstallWithID(operatorID, ns2, "--skip-operator-setup", "--olm=false").Execute()).To(Succeed())
 
 				Expect(KamelRunWithID(operatorID, ns2, "files/timer-kamelet-usage.groovy").Execute()).To(Succeed())
@@ -71,7 +71,6 @@ func TestRunGlobalKamelet(t *testing.T) {
 
 			// NS3: namespace without operator
 			WithNewTestNamespace(t, func(ns3 string) {
-				operatorID := "camel-k-local-ns3"
 				Expect(KamelInstallWithID(operatorID, ns3, "--skip-operator-setup", "--olm=false").Execute()).To(Succeed())
 
 				Expect(KamelRunWithID(operatorID, ns3, "files/timer-kamelet-usage.groovy").Execute()).To(Succeed())
diff --git a/e2e/namespace/install/cli/global_test.go b/e2e/namespace/install/cli/global_test.go
index f96b76b32..12943ef7a 100644
--- a/e2e/namespace/install/cli/global_test.go
+++ b/e2e/namespace/install/cli/global_test.go
@@ -54,6 +54,7 @@ func TestRunGlobalInstall(t *testing.T) {
 	WithGlobalOperatorNamespace(t, func(operatorNamespace string) {
 		Expect(KamelInstall(operatorNamespace, "--global", "--force").Execute()).To(Succeed())
 		Eventually(OperatorPodPhase(operatorNamespace), TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+
 		t.Run("Global test on namespace with platform", func(t *testing.T) {
 			WithNewTestNamespace(t, func(ns2 string) {
 				// Creating namespace local platform
diff --git a/e2e/namespace/install/cli/install_test.go b/e2e/namespace/install/cli/install_test.go
index 1df50622d..aee7732c9 100644
--- a/e2e/namespace/install/cli/install_test.go
+++ b/e2e/namespace/install/cli/install_test.go
@@ -73,9 +73,10 @@ func TestMavenRepositoryInstallation(t *testing.T) {
 	WithNewTestNamespace(t, func(ns string) {
 		operatorID := fmt.Sprintf("camel-k-%s", ns)
 		Expect(KamelInstallWithID(operatorID, ns, "--maven-repository", "https://my.repo.org/public/").Execute()).To(Succeed())
-		Eventually(Configmap(ns, "camel-k-maven-settings")).Should(Not(BeNil()))
+		configmapName := fmt.Sprintf("%s-maven-settings", operatorID)
+		Eventually(Configmap(ns, configmapName)).Should(Not(BeNil()))
 		Eventually(func() string {
-			return Configmap(ns, "camel-k-maven-settings")().Data["settings.xml"]
+			return Configmap(ns, configmapName)().Data["settings.xml"]
 		}).Should(ContainSubstring("https://my.repo.org/public/"))
 	})
 }
diff --git a/e2e/global/common/promote_test.go b/e2e/namespace/install/cli/promote_test.go
similarity index 88%
rename from e2e/global/common/promote_test.go
rename to e2e/namespace/install/cli/promote_test.go
index ba9ac85e6..76ad5b62a 100644
--- a/e2e/global/common/promote_test.go
+++ b/e2e/namespace/install/cli/promote_test.go
@@ -36,19 +36,20 @@ import (
 func TestKamelCLIPromote(t *testing.T) {
 	// Dev environment namespace
 	WithNewTestNamespace(t, func(nsDev string) {
-		Expect(KamelInstall(nsDev).Execute()).To(Succeed())
+		operatorDevID := "camel-k-cli-promote-dev"
+		Expect(KamelInstallWithID(operatorDevID, 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)
+		CreatePlainTextConfigmap(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)
+		CreatePlainTextSecret(nsDev, "my-sec", secData)
 
 		t.Run("plain integration dev", func(t *testing.T) {
-			Expect(KamelRun(nsDev, "./files/promote-route.groovy",
+			Expect(KamelRunWithID(operatorDevID, nsDev, "./files/promote-route.groovy",
 				"--config", "configmap:my-cm",
 				"--config", "secret:my-sec",
 			).Execute()).To(Succeed())
@@ -60,30 +61,32 @@ func TestKamelCLIPromote(t *testing.T) {
 
 		t.Run("kamelet integration dev", func(t *testing.T) {
 			Expect(CreateTimerKamelet(nsDev, "my-own-timer-source")()).To(Succeed())
-			Expect(KamelRun(nsDev, "files/timer-kamelet-usage.groovy").Execute()).To(Succeed())
+			Expect(KamelRunWithID(operatorDevID, 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(KamelBind(nsDev, "kb-timer-source", "log:info", "-p", "source.message=my-kamelet-binding-rocks").Execute()).To(Succeed())
+			Expect(KamelBindWithID(operatorDevID, nsDev, "kb-timer-source", "log:info", "-p", "source.message=my-kamelet-binding-rocks").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(KamelInstall(nsProd).Execute()).To(Succeed())
+			operatorProdID := "camel-k-cli-promote-prod"
+			Expect(KamelInstallWithID(operatorProdID, nsProd).Execute()).To(Succeed())
 			Eventually(PlatformPhase(nsProd), TestTimeoutMedium).Should(Equal(v1.IntegrationPlatformPhaseReady))
 
 			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)
+			CreatePlainTextConfigmap(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())
@@ -92,7 +95,7 @@ func TestKamelCLIPromote(t *testing.T) {
 			// Prod secret
 			var secData = make(map[string]string)
 			secData["my-secret-key"] = "very top secret production"
-			NewPlainTextSecret(nsProd, "my-sec", secData)
+			CreatePlainTextSecret(nsProd, "my-sec", secData)
 
 			t.Run("plain integration promotion", func(t *testing.T) {
 				Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).To(Succeed())
diff --git a/e2e/namespace/install/cli/run_test.go b/e2e/namespace/install/cli/run_test.go
index 14922aef0..4bca39e8a 100644
--- a/e2e/namespace/install/cli/run_test.go
+++ b/e2e/namespace/install/cli/run_test.go
@@ -43,26 +43,43 @@ func TestKamelCLIRun(t *testing.T) {
 
 		t.Run("Examples from GitHub", func(t *testing.T) {
 			t.Run("Java", func(t *testing.T) {
-				Expect(KamelRunWithID(operatorID, ns, "github:apache/camel-k/e2e/namespace/install/files/Java.java").Execute()).To(Succeed())
+				Expect(KamelRunWithID(operatorID, ns,
+					"github:apache/camel-k/e2e/namespace/install/files/Java.java").Execute()).To(Succeed())
 				Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
-				Eventually(IntegrationConditionStatus(ns, "java", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+				Eventually(IntegrationConditionStatus(ns, "java", v1.IntegrationConditionReady), TestTimeoutShort).
+					Should(Equal(corev1.ConditionTrue))
 				Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
 				Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
 			})
 
 			t.Run("Java (RAW)", func(t *testing.T) {
-				Expect(KamelRunWithID(operatorID, ns, "https://raw.githubusercontent.com/apache/camel-k/main/e2e/namespace/install/files/Java.java").Execute()).To(Succeed())
+				Expect(KamelRunWithID(operatorID, ns,
+					"https://raw.githubusercontent.com/apache/camel-k/main/e2e/namespace/install/files/Java.java").Execute()).
+					To(Succeed())
 				Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
-				Eventually(IntegrationConditionStatus(ns, "java", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+				Eventually(IntegrationConditionStatus(ns, "java", v1.IntegrationConditionReady), TestTimeoutShort).
+					Should(Equal(corev1.ConditionTrue))
+				Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
+				Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
+			})
+
+			t.Run("Java (branch)", func(t *testing.T) {
+				Expect(KamelRunWithID(operatorID, ns,
+					"github:apache/camel-k/e2e/namespace/install/files/Java.java?branch=main").Execute()).To(Succeed())
+				Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
+				Eventually(IntegrationConditionStatus(ns, "java", v1.IntegrationConditionReady), TestTimeoutShort).
+					Should(Equal(corev1.ConditionTrue))
 				Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
 				Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
 			})
 
 			t.Run("Gist (ID)", func(t *testing.T) {
 				name := "github-gist-id"
-				Expect(KamelRunWithID(operatorID, ns, "--name", name, "gist:e2c3f9a5fd0d9e79b21b04809786f17a").Execute()).To(Succeed())
+				Expect(KamelRunWithID(operatorID, ns, "--name", name,
+					"gist:e2c3f9a5fd0d9e79b21b04809786f17a").Execute()).To(Succeed())
 				Eventually(IntegrationPodPhase(ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning))
-				Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+				Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).
+					Should(Equal(corev1.ConditionTrue))
 				Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
 				Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Tick!"))
 				Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
@@ -70,9 +87,11 @@ func TestKamelCLIRun(t *testing.T) {
 
 			t.Run("Gist (URL)", func(t *testing.T) {
 				name := "github-gist-url"
-				Expect(KamelRunWithID(operatorID, ns, "--name", name, "https://gist.github.com/lburgazzoli/e2c3f9a5fd0d9e79b21b04809786f17a").Execute()).To(Succeed())
+				Expect(KamelRunWithID(operatorID, ns, "--name", name,
+					"https://gist.github.com/lburgazzoli/e2c3f9a5fd0d9e79b21b04809786f17a").Execute()).To(Succeed())
 				Eventually(IntegrationPodPhase(ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning))
-				Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+				Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).
+					Should(Equal(corev1.ConditionTrue))
 				Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
 				Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Tick!"))
 				Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
@@ -86,21 +105,25 @@ func TestKamelCLIRun(t *testing.T) {
 			name := "run"
 			Expect(KamelRunWithID(operatorID, ns, "files/run.yaml", "--name", name).Execute()).To(Succeed())
 			Eventually(IntegrationPodPhase(ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning))
-			Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+			Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).
+				Should(Equal(corev1.ConditionTrue))
 			Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magic default"))
 
 			// Re-run the Integration with an updated configuration
-			Expect(KamelRunWithID(operatorID, ns, "files/run.yaml", "--name", name, "-p", "property=value").Execute()).To(Succeed())
+			Expect(KamelRunWithID(operatorID, ns, "files/run.yaml", "--name", name, "-p", "property=value").Execute()).
+				To(Succeed())
 
 			// Check the Deployment has progressed successfully
-			Eventually(DeploymentCondition(ns, name, appsv1.DeploymentProgressing), TestTimeoutShort).Should(MatchFields(IgnoreExtras, Fields{
-				"Status": Equal(corev1.ConditionTrue),
-				"Reason": Equal("NewReplicaSetAvailable"),
-			}))
+			Eventually(DeploymentCondition(ns, name, appsv1.DeploymentProgressing), TestTimeoutShort).
+				Should(MatchFields(IgnoreExtras, Fields{
+					"Status": Equal(corev1.ConditionTrue),
+					"Reason": Equal("NewReplicaSetAvailable"),
+				}))
 
 			// Check the new configuration is taken into account
 			Eventually(IntegrationPodPhase(ns, name), TestTimeoutShort).Should(Equal(corev1.PodRunning))
-			Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+			Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).
+				Should(Equal(corev1.ConditionTrue))
 			Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magic value"))
 
 			// Clean up
diff --git a/e2e/namespace/install/maven_http_proxy_test.go b/e2e/namespace/install/maven_http_proxy_test.go
index 3e0302366..06b52d676 100644
--- a/e2e/namespace/install/maven_http_proxy_test.go
+++ b/e2e/namespace/install/maven_http_proxy_test.go
@@ -147,6 +147,7 @@ func TestMavenProxy(t *testing.T) {
 		noProxy = append(noProxy, svc.Spec.ClusterIPs...)
 
 		// Install Camel K with the HTTP proxy
+		operatorID := "camel-k-maven-proxy"
 		olm, olmErr := olm.IsAPIAvailable(TestContext, TestClient(), ns)
 		installed, inErr := kubernetes.IsAPIResourceInstalled(TestClient(), configv1.GroupVersion.String(), reflect.TypeOf(configv1.Proxy{}).Name())
 		permission, pErr := kubernetes.CheckPermission(TestContext, TestClient(), configv1.GroupName, reflect.TypeOf(configv1.Proxy{}).Name(), "", "cluster", "edit")
@@ -176,10 +177,8 @@ func TestMavenProxy(t *testing.T) {
 			}()
 
 			// ENV values should be injected by the OLM
-			operatorID := "camel-k-maven-proxy"
 			Expect(KamelInstallWithID(operatorID, ns).Execute()).To(Succeed())
 		} else {
-			operatorID := "camel-k-maven-proxy"
 			Expect(KamelInstallWithID(operatorID, ns,
 				"--operator-env-vars", fmt.Sprintf("HTTP_PROXY=http://%s", hostname),
 				// TODO: enable TLS for the HTTPS proxy when Maven supports it
@@ -193,7 +192,7 @@ func TestMavenProxy(t *testing.T) {
 
 		// Run the Integration
 		name := "java"
-		Expect(KamelRun(ns, "files/Java.java", "--name", name).Execute()).To(Succeed())
+		Expect(KamelRunWithID(operatorID, ns, "files/Java.java", "--name", name).Execute()).To(Succeed())
 
 		Eventually(IntegrationPodPhase(ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning))
 		Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
diff --git a/e2e/namespace/install/operator_id_filtering_test.go b/e2e/namespace/install/operator_id_filtering_test.go
index 1f8fa0b0f..b142634d2 100644
--- a/e2e/namespace/install/operator_id_filtering_test.go
+++ b/e2e/namespace/install/operator_id_filtering_test.go
@@ -63,15 +63,15 @@ func TestOperatorIDFiltering(t *testing.T) {
 				t.Run("Operators ignore non-scoped integrations", func(t *testing.T) {
 					RegisterTestingT(t)
 
-					Expect(KamelRun(ns, "files/yaml.yaml", "--name", "untouched").Execute()).To(Succeed())
+					Expect(KamelRun(ns, "files/yaml.yaml", "--name", "untouched", "--force").Execute()).To(Succeed())
 					Consistently(IntegrationPhase(ns, "untouched"), 10*time.Second).Should(BeEmpty())
 				})
 
 				t.Run("Operators run scoped integrations", func(t *testing.T) {
 					RegisterTestingT(t)
 
-					Expect(KamelRun(ns, "files/yaml.yaml", "--name", "moving").Execute()).To(Succeed())
-					Expect(AssignIntegrationToOperator(ns, "moving", "operator-1")).To(Succeed())
+					Expect(KamelRun(ns, "files/yaml.yaml", "--name", "moving", "--force").Execute()).To(Succeed())
+					Expect(AssignIntegrationToOperator(ns, "moving", operator1)).To(Succeed())
 					Eventually(IntegrationPhase(ns, "moving"), TestTimeoutMedium).Should(Equal(v1.IntegrationPhaseRunning))
 					Eventually(IntegrationPodPhase(ns, "moving"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
 					Eventually(IntegrationLogs(ns, "moving"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
@@ -80,7 +80,7 @@ func TestOperatorIDFiltering(t *testing.T) {
 				t.Run("Operators can handoff scoped integrations", func(t *testing.T) {
 					RegisterTestingT(t)
 
-					Expect(AssignIntegrationToOperator(ns, "moving", "operator-2")).To(Succeed())
+					Expect(AssignIntegrationToOperator(ns, "moving", operator2)).To(Succeed())
 					Eventually(IntegrationPhase(ns, "moving"), TestTimeoutMedium).Should(Equal(v1.IntegrationPhaseRunning))
 					Expect(Kamel("rebuild", "-n", ns, "moving").Execute()).To(Succeed())
 					Eventually(IntegrationPhase(ns, "moving"), TestTimeoutMedium).Should(Equal(v1.IntegrationPhaseRunning))
@@ -107,9 +107,10 @@ func TestOperatorIDFiltering(t *testing.T) {
 					// Save resources by deleting "moving" integration
 					Expect(Kamel("delete", "moving", "-n", ns).Execute()).To(Succeed())
 
-					Expect(KamelRun(ns, "files/yaml.yaml", "--name", "pre-built", "-t", fmt.Sprintf("container.image=%s", image)).Execute()).To(Succeed())
+					Expect(KamelRun(ns, "files/yaml.yaml", "--name", "pre-built", "--force",
+						"-t", fmt.Sprintf("container.image=%s", image)).Execute()).To(Succeed())
 					Consistently(IntegrationPhase(ns, "pre-built"), 10*time.Second).Should(BeEmpty())
-					Expect(AssignIntegrationToOperator(ns, "pre-built", "operator-2")).To(Succeed())
+					Expect(AssignIntegrationToOperator(ns, "pre-built", operator2)).To(Succeed())
 					Eventually(IntegrationPhase(ns, "pre-built"), TestTimeoutShort).Should(Equal(v1.IntegrationPhaseRunning))
 					Eventually(IntegrationPodPhase(ns, "pre-built"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
 					Eventually(IntegrationLogs(ns, "pre-built"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
@@ -119,10 +120,11 @@ func TestOperatorIDFiltering(t *testing.T) {
 				t.Run("Operators can run scoped kamelet bindings", func(t *testing.T) {
 					RegisterTestingT(t)
 
-					Expect(KamelBind(ns, "timer-source?message=Hello", "log-sink", "--name", "klb").Execute()).To(Succeed())
+					Expect(KamelBind(ns, "timer-source?message=Hello", "log-sink",
+						"--name", "klb", "--force").Execute()).To(Succeed())
 					Consistently(Integration(ns, "klb"), 10*time.Second).Should(BeNil())
 
-					Expect(AssignKameletBindingToOperator(ns, "klb", "operator-1")).To(Succeed())
+					Expect(AssignKameletBindingToOperator(ns, "klb", operator1)).To(Succeed())
 					Eventually(Integration(ns, "klb"), TestTimeoutShort).ShouldNot(BeNil())
 					Eventually(IntegrationPhase(ns, "klb"), TestTimeoutMedium).Should(Equal(v1.IntegrationPhaseRunning))
 					Eventually(IntegrationPodPhase(ns, "klb"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
diff --git a/e2e/namespace/native/native_test.go b/e2e/namespace/native/native_test.go
index c84e49704..f2b10692c 100644
--- a/e2e/namespace/native/native_test.go
+++ b/e2e/namespace/native/native_test.go
@@ -23,6 +23,7 @@ limitations under the License.
 package native
 
 import (
+	"os"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -39,6 +40,11 @@ var (
 )
 
 func TestNativeIntegrations(t *testing.T) {
+	if os.Getenv("CAMEL_K_CLUSTER_OCP3") == "true" {
+		t.Skip("INFO: Skipping test as known to never pass on OCP3")
+		return
+	}
+
 	WithNewTestNamespace(t, func(ns string) {
 		operatorID := "camel-k-quarkus-native"
 		Expect(KamelInstallWithID(operatorID, ns,
diff --git a/e2e/support/test_support.go b/e2e/support/test_support.go
index 2f8cbc150..1d690cfb1 100644
--- a/e2e/support/test_support.go
+++ b/e2e/support/test_support.go
@@ -1131,7 +1131,7 @@ func AutogeneratedConfigmapsCount(ns string) func() int {
 	}
 }
 
-func NewPlainTextConfigmap(ns string, name string, data map[string]string) error {
+func CreatePlainTextConfigmap(ns string, name string, data map[string]string) error {
 	cm := corev1.ConfigMap{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "ConfigMap",
@@ -1146,7 +1146,7 @@ func NewPlainTextConfigmap(ns string, name string, data map[string]string) error
 	return TestClient().Create(TestContext, &cm)
 }
 
-func NewBinaryConfigmap(ns string, name string, data map[string][]byte) error {
+func CreateBinaryConfigmap(ns string, name string, data map[string][]byte) error {
 	cm := corev1.ConfigMap{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "ConfigMap",
@@ -1161,7 +1161,7 @@ func NewBinaryConfigmap(ns string, name string, data map[string][]byte) error {
 	return TestClient().Create(TestContext, &cm)
 }
 
-func NewPlainTextSecret(ns string, name string, data map[string]string) error {
+func CreatePlainTextSecret(ns string, name string, data map[string]string) error {
 	sec := corev1.Secret{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Secret",
diff --git a/pkg/apis/camel/v1/common_types_support_test.go b/pkg/apis/camel/v1/common_types_support_test.go
new file mode 100644
index 000000000..e46455493
--- /dev/null
+++ b/pkg/apis/camel/v1/common_types_support_test.go
@@ -0,0 +1,131 @@
+/*
+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 v1
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/apache/camel-k/pkg/apis/camel/v1/trait"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"k8s.io/utils/pointer"
+)
+
+func TestTraitsMerge(t *testing.T) {
+	t1 := Traits{
+		Container: &trait.ContainerTrait{
+			Trait: trait.Trait{
+				Configuration: configurationFromMap(t, map[string]interface{}{
+					"name": "test-container",
+				}),
+			},
+			Auto:        pointer.Bool(false),
+			ServicePort: 81,
+		},
+		Logging: &trait.LoggingTrait{
+			Color: pointer.Bool(false),
+			Level: "INFO",
+		},
+		Addons: map[string]AddonTrait{
+			"master": toAddonTrait(t, map[string]interface{}{
+				"resourceName": "test-lock",
+			}),
+			"tracing": toAddonTrait(t, map[string]interface{}{
+				"enabled": true,
+			}),
+		},
+	}
+	t2 := Traits{
+		Container: &trait.ContainerTrait{
+			Trait: trait.Trait{
+				Configuration: configurationFromMap(t, map[string]interface{}{
+					"port": 8081,
+				}),
+			},
+			PortName: "http-8081",
+		},
+		Logging: &trait.LoggingTrait{
+			Color: pointer.Bool(true),
+			Level: "DEBUG",
+		},
+		Addons: map[string]AddonTrait{
+			"tracing": toAddonTrait(t, map[string]interface{}{
+				"serviceName": "test-integration",
+			}),
+		},
+	}
+
+	err := t1.Merge(t2)
+
+	require.NoError(t, err)
+
+	assert.NotNil(t, t1.Container)
+	assert.False(t, pointer.BoolDeref(t1.Container.Auto, true))
+	assert.Equal(t, "http-8081", t1.Container.PortName)
+	assert.Equal(t, 81, t1.Container.ServicePort)
+	assert.Equal(t,
+		configurationFromMap(t, map[string]interface{}{
+			"name": "test-container",
+			"port": 8081,
+		}),
+		t1.Container.Configuration)
+
+	// values from merged trait take precedence over the original ones
+	assert.NotNil(t, t1.Logging)
+	assert.True(t, pointer.BoolDeref(t1.Logging.Color, false))
+	assert.Equal(t, "DEBUG", t1.Logging.Level)
+
+	assert.NotNil(t, t1.Addons)
+	assert.Equal(t,
+		toAddonTrait(t, map[string]interface{}{
+			"resourceName": "test-lock",
+		}),
+		t1.Addons["master"])
+	assert.Equal(t,
+		toAddonTrait(t, map[string]interface{}{
+			"enabled":     true,
+			"serviceName": "test-integration",
+		}),
+		t1.Addons["tracing"])
+}
+
+func configurationFromMap(t *testing.T, configMap map[string]interface{}) *trait.Configuration {
+	t.Helper()
+
+	data, err := json.Marshal(configMap)
+	require.NoError(t, err)
+
+	return &trait.Configuration{
+		RawMessage: data,
+	}
+}
+
+func toAddonTrait(t *testing.T, config map[string]interface{}) AddonTrait {
+	t.Helper()
+
+	data, err := json.Marshal(config)
+	require.NoError(t, err)
+
+	var addon AddonTrait
+	err = json.Unmarshal(data, &addon)
+	require.NoError(t, err)
+
+	return addon
+}
diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go
index adc6beb09..597308d25 100644
--- a/pkg/util/util_test.go
+++ b/pkg/util/util_test.go
@@ -32,5 +32,15 @@ func TestStringContainsPrefix(t *testing.T) {
 func TestRandomString(t *testing.T) {
 	assert.Equal(t, 10, len(RandomString(10)))
 	assert.NotEqual(t, RandomString(10), RandomString(10))
+}
+
+func TestSubstringFrom(t *testing.T) {
+	assert.Equal(t, "/bbb/ccc", SubstringFrom("aaa/bbb/ccc", "/"))
+	assert.Empty(t, SubstringFrom("aaa/bbb/ccc", "?"))
+}
 
+func TestSubstringBefore(t *testing.T) {
+	assert.Equal(t, "aaa/bbb", SubstringBefore("aaa/bbb/ccc", "/"))
+	assert.Equal(t, "aaa/bbb", SubstringBefore("aaa/bbb?ccc=ddd", "?"))
+	assert.Empty(t, SubstringBefore("aaa/bbb/ccc", "?"))
 }