Repository: apache/camel-k
Pull request #4621 (2023-07-28 14:29:08)

feat(cmd): improve `kamel promote`
<!-- Description -->

This PR addresses a few enhancements which are helping GitOps or in general deployment operations. They are following up the some limitations founds in the proposed [Camel K GitOps strategy](https://camel.apache.org/blog/2023/07/camel-k-gitops/).

First of all, we get rid of the destination checks when in dry-run mode. Second, we introduce a simple flag which helps putting in place any unopinionated deployment strategy, for example:
```
$ kamel promote test -i --to test
10.110.251.124/default/camel-k-kit-cj1ra5e9j3ks738a7bbg@sha256:99835a5da746fc3d7f96d0ae50c64df0e29eb3e8d131e61bfdfd617fdaa078ba
```
Finally we allow to specify the operator ID tenant which must be used to take care of the promoted integration, ie `kamel promote test -x my-production-operator-id --to test`

Closes https://github.com/apache/camel-k/issues/4534
Closes https://github.com/apache/camel-k/issues/3890
<!--
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
feat(cmd): improve `kamel promote`
```


github-actions[bot]: :camel: **Thank you for contributing!**

Code Coverage Report :warning: - Coverage changed: 39.4% --> 39.1% (Coverage difference: **-.3%**)
github-actions[bot]: :camel: **Thank you for contributing!**

Code Coverage Report :warning: - Coverage changed: 39.4% --> 39.1% (Coverage difference: **-.3%**)

Patch:
diff --git a/.github/workflows/nightly-automatic-updates.yml b/.github/workflows/nightly-automatic-updates.yml
index fcdc02e1f..2dd4d3e57 100644
--- a/.github/workflows/nightly-automatic-updates.yml
+++ b/.github/workflows/nightly-automatic-updates.yml
@@ -29,6 +29,7 @@ jobs:
   auto-updates:
     strategy:
       fail-fast: false
+      max-parallel: 1
       matrix:
         ref-branch: [main, release-1.12.x, release-2.0.x]
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 629d48349..f1b4e5bce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,12 +6,15 @@
 
 **Closed issues:**
 
+- Include Camel release subproject in the release process [\#4627](https://github.com/apache/camel-k/issues/4627)
+- Bot updating docs/antora.yaml causes camel-website build to fail [\#4624](https://github.com/apache/camel-k/issues/4624)
 - Upgrade to Go 1.19.x or 1.20.x [\#4613](https://github.com/apache/camel-k/issues/4613)
 - Cannot build with Camel Quarkus 3.0.0 RC1 [\#4609](https://github.com/apache/camel-k/issues/4609)
 - \[Openshift\] Repeated install/uninstall and removal of CamelCatalog leads to re-creation of builder image [\#4569](https://github.com/apache/camel-k/issues/4569)
 - Give the possibility to add a maven profile to an Integration project [\#4560](https://github.com/apache/camel-k/issues/4560)
 - Upload local dependencies as maven artifact [\#4244](https://github.com/apache/camel-k/issues/4244)
 - Release script utils: Remove examples upload since they don't exist anymore [\#4069](https://github.com/apache/camel-k/issues/4069)
+- Use `govulncheck` to check security vulnerabilities [\#3753](https://github.com/apache/camel-k/issues/3753)
 - Split Operator binary from Kamel CLI binary [\#3175](https://github.com/apache/camel-k/issues/3175)
 - Document installation on air-gapped clusters [\#1721](https://github.com/apache/camel-k/issues/1721)
 - Delegate installation to external tools [\#1503](https://github.com/apache/camel-k/issues/1503)
@@ -20,6 +23,7 @@
 
 **Merged pull requests:**
 
+- feat\(ci\): Add govulncheck as ci workflow [\#4623](https://github.com/apache/camel-k/pull/4623) ([gansheer](https://github.com/gansheer))
 - feat\(build\): Upgrade to Golang 1.20 [\#4620](https://github.com/apache/camel-k/pull/4620) ([gansheer](https://github.com/gansheer))
 - chore\(e2e\): Test on kamelets configurations [\#4619](https://github.com/apache/camel-k/pull/4619) ([gansheer](https://github.com/gansheer))
 - fix: use non deprecated components [\#4616](https://github.com/apache/camel-k/pull/4616) ([squakez](https://github.com/squakez))
diff --git a/camel-k-sbom/camel-k-sbom.json b/camel-k-sbom/camel-k-sbom.json
index 2d3c819e2..d33bfdb99 100644
--- a/camel-k-sbom/camel-k-sbom.json
+++ b/camel-k-sbom/camel-k-sbom.json
@@ -2,10 +2,10 @@
   "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
   "bomFormat": "CycloneDX",
   "specVersion": "1.4",
-  "serialNumber": "urn:uuid:d8ad2480-6522-47b4-b963-3e9c070a6bbc",
+  "serialNumber": "urn:uuid:2a6f2ffe-5ffe-4a3c-be47-346e0b679318",
   "version": 1,
   "metadata": {
-    "timestamp": "2023-07-29T23:44:06Z",
+    "timestamp": "2023-07-31T23:46:44Z",
     "tools": [
       {
         "vendor": "CycloneDX",
@@ -46,11 +46,11 @@
       }
     ],
     "component": {
-      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "type": "application",
       "name": "github.com/apache/camel-k/v2",
-      "version": "v0.0.0-20230728234208-e1926c3304e9",
-      "purl": "pkg:golang/github.com/apache/camel-k/v2@v0.0.0-20230728234208-e1926c3304e9?type=module\u0026goos=linux\u0026goarch=amd64",
+      "version": "v0.0.0-20230731234318-2c6cdcd6e2a6",
+      "purl": "pkg:golang/github.com/apache/camel-k/v2@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module\u0026goos=linux\u0026goarch=amd64",
       "externalReferences": [
         {
           "url": "https://github.com/apache/camel-k",
@@ -423,18 +423,18 @@
       }
     },
     {
-      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "type": "library",
       "name": "github.com/apache/camel-k/v2/pkg/apis/camel",
-      "version": "v0.0.0-20230728234208-e1926c3304e9",
+      "version": "v0.0.0-20230731234318-2c6cdcd6e2a6",
       "scope": "required",
       "hashes": [
         {
           "alg": "SHA-256",
-          "content": "d59558cccb5baa6165a66497c13a08ddf58ad4b9ba352c7d4ac0169978da70de"
+          "content": "0d124e8c3cd74cadd7a83607399dd00be23d88ee102623bc00e713cf1d0812b6"
         }
       ],
-      "purl": "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230728234208-e1926c3304e9?type=module\u0026goos=linux\u0026goarch=amd64",
+      "purl": "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module\u0026goos=linux\u0026goarch=amd64",
       "externalReferences": [
         {
           "url": "https://github.com/apache/camel-k/v2/pkg/apis/camel",
@@ -443,18 +443,18 @@
       ]
     },
     {
-      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "type": "library",
       "name": "github.com/apache/camel-k/v2/pkg/client/camel",
-      "version": "v0.0.0-20230728234208-e1926c3304e9",
+      "version": "v0.0.0-20230731234318-2c6cdcd6e2a6",
       "scope": "required",
       "hashes": [
         {
           "alg": "SHA-256",
-          "content": "837310015ea7be8c604daa23d79f5782289168b109fd2dff937f2c8d32d4802e"
+          "content": "97d04a5a092c876faebe97160dd16ea57d6e89b512fdedf91f0267f3ac50e80e"
         }
       ],
-      "purl": "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230728234208-e1926c3304e9?type=module\u0026goos=linux\u0026goarch=amd64",
+      "purl": "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module\u0026goos=linux\u0026goarch=amd64",
       "externalReferences": [
         {
           "url": "https://github.com/apache/camel-k/v2/pkg/client/camel",
@@ -463,18 +463,18 @@
       ]
     },
     {
-      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "bom-ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "type": "library",
       "name": "github.com/apache/camel-k/v2/pkg/kamelet/repository",
-      "version": "v0.0.0-20230728234208-e1926c3304e9",
+      "version": "v0.0.0-20230731234318-2c6cdcd6e2a6",
       "scope": "required",
       "hashes": [
         {
           "alg": "SHA-256",
-          "content": "244a68e66b007f030669c26786005f63eaf02e54e08a3f69eaf7c6d3af69605d"
+          "content": "0c6a31d5ca8c925ac2300aecf55c42ac54781b6b6806a0fe45f1bde90fc5cf81"
         }
       ],
-      "purl": "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230728234208-e1926c3304e9?type=module\u0026goos=linux\u0026goarch=amd64",
+      "purl": "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module\u0026goos=linux\u0026goarch=amd64",
       "externalReferences": [
         {
           "url": "https://github.com/apache/camel-k/v2/pkg/kamelet/repository",
@@ -4081,12 +4081,12 @@
   ],
   "dependencies": [
     {
-      "ref": "pkg:golang/github.com/apache/camel-k/v2@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "ref": "pkg:golang/github.com/apache/camel-k/v2@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "dependsOn": [
         "pkg:golang/github.com/Masterminds/semver@v1.5.0?type=module",
-        "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
-        "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
-        "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230728234208-e1926c3304e9?type=module",
+        "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
+        "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
+        "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
         "pkg:golang/github.com/container-tools/spectrum@v0.6.32?type=module",
         "pkg:golang/github.com/evanphx/json-patch@v5.6.0+incompatible?type=module",
         "pkg:golang/github.com/fsnotify/fsnotify@v1.6.0?type=module",
@@ -4245,7 +4245,7 @@
       "ref": "pkg:golang/github.com/antlr/antlr4/runtime/Go/antlr@v1.4.10?type=module"
     },
     {
-      "ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "dependsOn": [
         "pkg:golang/github.com/imdario/mergo@v0.3.13?type=module",
         "pkg:golang/github.com/stretchr/testify@v1.8.4?type=module",
@@ -4271,9 +4271,9 @@
       ]
     },
     {
-      "ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "dependsOn": [
-        "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
+        "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
         "pkg:golang/k8s.io/api@v0.25.6?type=module",
         "pkg:golang/k8s.io/apimachinery@v0.25.6?type=module",
         "pkg:golang/k8s.io/client-go@v0.25.6?type=module",
@@ -4317,10 +4317,10 @@
       ]
     },
     {
-      "ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230728234208-e1926c3304e9?type=module",
+      "ref": "pkg:golang/github.com/apache/camel-k/v2/pkg/kamelet/repository@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
       "dependsOn": [
-        "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
-        "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230728234208-e1926c3304e9?type=module",
+        "pkg:golang/github.com/apache/camel-k/v2/pkg/apis/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
+        "pkg:golang/github.com/apache/camel-k/v2/pkg/client/camel@v0.0.0-20230731234318-2c6cdcd6e2a6?type=module",
         "pkg:golang/github.com/stretchr/testify@v1.8.4?type=module",
         "pkg:golang/golang.org/x/oauth2@v0.10.0?type=module",
         "pkg:golang/k8s.io/apimachinery@v0.25.6?type=module",
diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go
index b87170f73..b15cc0838 100644
--- a/pkg/cmd/promote.go
+++ b/pkg/cmd/promote.go
@@ -49,15 +49,17 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO
 		RootCmdOptions: rootCmdOptions,
 	}
 	cmd := cobra.Command{
-		Use:     "promote my-it --to [namespace]",
+		Use:     "promote my-it [--to <namespace>] [-x <promoted-operator-id>]",
 		Short:   "Promote an Integration/Pipe from an environment to another",
 		Long:    "Promote an Integration/Pipe from an environment to another, for example from a Development environment to a Production environment",
 		PreRunE: decode(&options),
 		RunE:    options.run,
 	}
 
-	cmd.Flags().String("to", "", "The namespace where to promote the Integration")
+	cmd.Flags().String("to", "", "The namespace where to promote the Integration/Pipe")
+	cmd.Flags().StringP("to-operator", "x", "", "The operator id which will reconcile the promoted Integration/Pipe")
 	cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml")
+	cmd.Flags().BoolP("image", "i", false, "Output the container image only")
 
 	return &cmd, &options
 }
@@ -65,15 +67,20 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO
 type promoteCmdOptions struct {
 	*RootCmdOptions
 	To           string `mapstructure:"to" yaml:",omitempty"`
+	ToOperator   string `mapstructure:"to-operator" yaml:",omitempty"`
 	OutputFormat string `mapstructure:"output" yaml:",omitempty"`
+	Image        bool   `mapstructure:"image" yaml:",omitempty"`
 }
 
 func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error {
 	if len(args) != 1 {
-		return errors.New("promote expects an Integration/Pipe name argument")
+		return errors.New("promote requires an Integration/Pipe name argument")
 	}
 	if o.To == "" {
-		return errors.New("promote expects a destination namespace as --to argument")
+		return errors.New("promote requires a destination namespace as --to argument")
+	}
+	if o.To == o.Namespace {
+		return errors.New("source and destination namespaces must be different in order to avoid promoted Integration/Pipe clashes with the source Integration/Pipe")
 	}
 	return nil
 }
@@ -88,19 +95,23 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return fmt.Errorf("could not retrieve cluster client: %w", err)
 	}
-	opSource, err := operatorInfo(o.Context, c, o.Namespace)
-	if err != nil {
-		return fmt.Errorf("could not retrieve info for Camel K operator source: %w", err)
-	}
-	opDest, err := operatorInfo(o.Context, c, o.To)
-	if err != nil {
-		return fmt.Errorf("could not retrieve info for Camel K operator destination: %w", err)
-	}
+	if !o.isDryRun() {
+		// Skip these checks if in dry mode
+		opSource, err := operatorInfo(o.Context, c, o.Namespace)
+		if err != nil {
+			return fmt.Errorf("could not retrieve info for Camel K operator source: %w", err)
+		}
+		opDest, err := operatorInfo(o.Context, c, o.To)
+		if err != nil {
+			return fmt.Errorf("could not retrieve info for Camel K operator destination: %w", err)
+		}
 
-	err = checkOpsCompatibility(cmd, opSource, opDest)
-	if err != nil {
-		return fmt.Errorf("could not verify operators compatibility: %w", err)
+		err = checkOpsCompatibility(cmd, opSource, opDest)
+		if err != nil {
+			return fmt.Errorf("could not verify operators compatibility: %w", err)
+		}
 	}
+
 	promotePipe := false
 	var sourceIntegration *v1.Integration
 	// We first look if a Pipe with the name exists
@@ -118,41 +129,51 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error {
 	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 fmt.Errorf("could not validate destination resources: %w", err)
+
+	// Image only mode
+	if o.Image {
+		showImageOnly(cmd, sourceIntegration)
+		return nil
+	}
+
+	if !o.isDryRun() {
+		// Skip these checks if in dry mode
+		err = o.validateDestResources(c, sourceIntegration)
+		if err != nil {
+			return fmt.Errorf("could not validate destination resources: %w", err)
+		}
 	}
 
 	// Pipe promotion
 	if promotePipe {
 		destPipe := o.editPipe(sourcePipe, sourceIntegration)
+		if o.OutputFormat != "" {
+			return showPipeOutput(cmd, destPipe, o.OutputFormat, c.GetScheme())
+		}
 		// Ensure the destination namespace has access to the source namespace images
 		err = addSystemPullerRoleBinding(o.Context, c, sourceIntegration.Namespace, destPipe.Namespace)
 		if err != nil {
 			return err
 		}
 		replaced, err := o.replaceResource(destPipe)
-		if o.OutputFormat != "" {
-			return showPipeOutput(cmd, destPipe, o.OutputFormat, c.GetScheme())
-		}
 		if !replaced {
-			fmt.Fprintln(cmd.OutOrStdout(), `Promoted Integration "`+name+`" created`)
+			fmt.Fprintln(cmd.OutOrStdout(), `Promoted Pipe "`+name+`" created`)
 		} else {
-			fmt.Fprintln(cmd.OutOrStdout(), `Promoted Integration "`+name+`" updated`)
+			fmt.Fprintln(cmd.OutOrStdout(), `Promoted Pipe "`+name+`" updated`)
 		}
 		return err
 	}
 
 	// Plain Integration promotion
 	destIntegration := o.editIntegration(sourceIntegration)
+	if o.OutputFormat != "" {
+		return showIntegrationOutput(cmd, destIntegration, o.OutputFormat)
+	}
 	// Ensure the destination namespace has access to the source namespace images
 	err = addSystemPullerRoleBinding(o.Context, c, sourceIntegration.Namespace, destIntegration.Namespace)
 	if err != nil {
 		return err
 	}
-	if o.OutputFormat != "" {
-		return showIntegrationOutput(cmd, destIntegration, o.OutputFormat)
-	}
 	replaced, err := o.replaceResource(destIntegration)
 	if !replaced {
 		fmt.Fprintln(cmd.OutOrStdout(), `Promoted Integration "`+name+`" created`)
@@ -441,7 +462,7 @@ 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.Annotations = cloneAnnotations(it.Annotations)
+	dst.Annotations = cloneAnnotations(it.Annotations, o.ToOperator)
 	dst.Labels = cloneLabels(it.Labels)
 	if dst.Spec.Traits.Container == nil {
 		dst.Spec.Traits.Container = &traitv1.ContainerTrait{}
@@ -450,14 +471,23 @@ func (o *promoteCmdOptions) editIntegration(it *v1.Integration) *v1.Integration
 	return &dst
 }
 
-// Return all annotations but the ones specific to source (ie, the operator).
-func cloneAnnotations(ann map[string]string) map[string]string {
+// Return all annotations overriding the operator Id if provided.
+func cloneAnnotations(ann map[string]string, operatorID string) map[string]string {
+	operatorIDAnnotationSet := false
 	newMap := make(map[string]string)
 	for k, v := range ann {
-		if k != v1.OperatorIDAnnotation {
+		if k == v1.OperatorIDAnnotation {
+			if operatorID != "" {
+				newMap[v1.OperatorIDAnnotation] = operatorID
+				operatorIDAnnotationSet = true
+			}
+		} else {
 			newMap[k] = v
 		}
 	}
+	if !operatorIDAnnotationSet && operatorID != "" {
+		newMap[v1.OperatorIDAnnotation] = operatorID
+	}
 	return newMap
 }
 
@@ -473,7 +503,7 @@ func cloneLabels(lbs map[string]string) map[string]string {
 func (o *promoteCmdOptions) editPipe(kb *v1.Pipe, it *v1.Integration) *v1.Pipe {
 	dst := v1.NewPipe(o.To, kb.Name)
 	dst.Spec = *kb.Spec.DeepCopy()
-	dst.Annotations = cloneAnnotations(kb.Annotations)
+	dst.Annotations = cloneAnnotations(kb.Annotations, o.ToOperator)
 	dst.Labels = cloneLabels(kb.Labels)
 	contImage := it.Status.Image
 	if dst.Spec.Integration == nil {
@@ -503,6 +533,10 @@ func (o *promoteCmdOptions) replaceResource(res k8sclient.Object) (bool, error)
 	return kubernetes.ReplaceResource(o.Context, o._client, res)
 }
 
+func (o *promoteCmdOptions) isDryRun() bool {
+	return o.OutputFormat != "" || o.Image
+}
+
 // RoleBinding is required to allow access to images in one namespace
 // by another namespace. Without this on rbac-enabled clusters, the
 // image cannot be pulled.
@@ -533,3 +567,7 @@ func addSystemPullerRoleBinding(ctx context.Context, c client.Client, sourceNS s
 
 	return err
 }
+
+func showImageOnly(cmd *cobra.Command, integration *v1.Integration) {
+	fmt.Fprintln(cmd.OutOrStdout(), integration.Status.Image)
+}
diff --git a/release.adoc b/release.adoc
index e7c9b2629..4f3e9d4f7 100644
--- a/release.adoc
+++ b/release.adoc
@@ -196,6 +196,10 @@ The version of Camel K and the main related dependencies are scraped automatical
 
 After the vote has passed, update the camel-website `antora-playbook.yml` `content.sources` section for camel-k and camel-k-runtime to use the newly released versions, replacing the previous released version.
 
+You also need to create an entry in the release section of Camel website project: https://github.com/apache/camel-website/tree/main/content/releases/k - you can use any previous document as a reference.
+
+NOTE: the milestone is the github project milestone ID used to track the release.
+
 === Operator Hub
 
 The https://github.com/k8s-operatorhub/community-operators/[OperatorHub] downstream channel should be synced to publish the latest version


Test patch:
diff --git a/pkg/cmd/promote_test.go b/pkg/cmd/promote_test.go
index de35de413..bc39d63de 100644
--- a/pkg/cmd/promote_test.go
+++ b/pkg/cmd/promote_test.go
@@ -65,9 +65,8 @@ func TestIntegrationNotCompatible(t *testing.T) {
 	srcCatalog := createTestCamelCatalog(srcPlatform)
 	dstCatalog := createTestCamelCatalog(dstPlatform)
 
-	promoteCmdOptions, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultIntegration, &srcCatalog, &dstCatalog)
-	_, err := test.ExecuteCommand(promoteCmd, cmdPromote, "my-it-test", "--to", "prod-namespace", "-o", "yaml", "-n", "default")
-	assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat)
+	_, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultIntegration, &srcCatalog, &dstCatalog)
+	_, err := test.ExecuteCommand(promoteCmd, cmdPromote, "my-it-test", "--to", "prod-namespace", "-n", "default")
 	assert.NotNil(t, err)
 	assert.Equal(t,
 		fmt.Sprintf("could not verify operators compatibility: source (%s) and destination (0.0.1) Camel K operator versions are not compatible", defaults.Version),
@@ -137,7 +136,6 @@ metadata:
   creationTimestamp: null
   name: my-kb-test
   namespace: prod-namespace
-  resourceVersion: "1"
 spec:
   integration:
     traits:
@@ -238,7 +236,6 @@ metadata:
     my-label: my-value
   name: my-kb-test
   namespace: prod-namespace
-  resourceVersion: "1"
 spec:
   integration:
     traits:
@@ -249,3 +246,98 @@ spec:
 status: {}
 `, output)
 }
+
+func TestItImageOnly(t *testing.T) {
+	srcPlatform := v1.NewIntegrationPlatform("default", platform.DefaultPlatformName)
+	srcPlatform.Status.Version = defaults.Version
+	srcPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
+	srcPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady
+	dstPlatform := v1.NewIntegrationPlatform("prod-namespace", platform.DefaultPlatformName)
+	dstPlatform.Status.Version = defaults.Version
+	dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
+	dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady
+	defaultIntegration := nominalIntegration("my-it-test")
+	srcCatalog := createTestCamelCatalog(srcPlatform)
+	dstCatalog := createTestCamelCatalog(dstPlatform)
+
+	_, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultIntegration, &srcCatalog, &dstCatalog)
+	output, err := test.ExecuteCommand(promoteCmd, cmdPromote, "my-it-test", "--to", "prod-namespace", "-i", "-n", "default")
+	assert.Nil(t, err)
+	assert.Equal(t, "my-special-image\n", output)
+}
+
+func TestPipeImageOnly(t *testing.T) {
+	srcPlatform := v1.NewIntegrationPlatform("default", platform.DefaultPlatformName)
+	srcPlatform.Status.Version = defaults.Version
+	srcPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
+	srcPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady
+	dstPlatform := v1.NewIntegrationPlatform("prod-namespace", platform.DefaultPlatformName)
+	dstPlatform.Status.Version = defaults.Version
+	dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
+	dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady
+	defaultKB := nominalPipe("my-kb-test")
+	defaultIntegration := nominalIntegration("my-kb-test")
+	srcCatalog := createTestCamelCatalog(srcPlatform)
+	dstCatalog := createTestCamelCatalog(dstPlatform)
+
+	_, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultKB, &defaultIntegration, &srcCatalog, &dstCatalog)
+	output, err := test.ExecuteCommand(promoteCmd, cmdPromote, "my-kb-test", "--to", "prod-namespace", "-i", "-n", "default")
+	assert.Nil(t, err)
+	assert.Equal(t, "my-special-image\n", output)
+}
+
+func TestIntegrationToOperatorId(t *testing.T) {
+	srcPlatform := v1.NewIntegrationPlatform("default", platform.DefaultPlatformName)
+	srcPlatform.Status.Version = defaults.Version
+	srcPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
+	srcPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady
+	dstPlatform := v1.NewIntegrationPlatform("prod-namespace", platform.DefaultPlatformName)
+	dstPlatform.Status.Version = defaults.Version
+	dstPlatform.Status.Build.RuntimeVersion = defaults.DefaultRuntimeVersion
+	dstPlatform.Status.Phase = v1.IntegrationPlatformPhaseReady
+	defaultIntegration := nominalIntegration("my-it-test")
+	srcCatalog := createTestCamelCatalog(srcPlatform)
+	dstCatalog := createTestCamelCatalog(dstPlatform)
+
+	// Verify default (missing) operator Id
+	promoteCmdOptions, promoteCmd, _ := initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultIntegration, &srcCatalog, &dstCatalog)
+	output, err := test.ExecuteCommand(promoteCmd, cmdPromote, "my-it-test", "-x", "my-prod-operator", "-o", "yaml", "--to", "prod")
+	assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat)
+	assert.Nil(t, err)
+	assert.Equal(t, `apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+  annotations:
+    camel.apache.org/operator.id: my-prod-operator
+  creationTimestamp: null
+  name: my-it-test
+  namespace: prod
+spec:
+  traits:
+    container:
+      image: my-special-image
+status: {}
+`, output)
+	// Verify also when the operator Id is set in the integration
+	defaultIntegration.Annotations = map[string]string{
+		"camel.apache.org/operator.id": "camel-k",
+	}
+	promoteCmdOptions, promoteCmd, _ = initializePromoteCmdOptions(t, &srcPlatform, &dstPlatform, &defaultIntegration, &srcCatalog, &dstCatalog)
+	output, err = test.ExecuteCommand(promoteCmd, cmdPromote, "my-it-test", "-x", "my-prod-operator", "-o", "yaml", "--to", "prod")
+	assert.Equal(t, "yaml", promoteCmdOptions.OutputFormat)
+	assert.Nil(t, err)
+	assert.Equal(t, `apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+  annotations:
+    camel.apache.org/operator.id: my-prod-operator
+  creationTimestamp: null
+  name: my-it-test
+  namespace: prod
+spec:
+  traits:
+    container:
+      image: my-special-image
+status: {}
+`, output)
+}