From 55e80a59eedb343c9b88735dbc645bafa3dd262d Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Thu, 15 May 2025 11:22:00 +0200
Subject: [PATCH 01/17] add release upgrade functionality

---
 .../src/platform/release/spec.rs              | 76 ++++++++++++++++++
 .../stackable-cockpit/src/utils/k8s/client.rs | 43 ++++++++++
 rust/stackablectl/src/cmds/release.rs         | 80 +++++++++++++++++++
 3 files changed, 199 insertions(+)

diff --git a/rust/stackable-cockpit/src/platform/release/spec.rs b/rust/stackable-cockpit/src/platform/release/spec.rs
index 1bb8be6b..1f443cda 100644
--- a/rust/stackable-cockpit/src/platform/release/spec.rs
+++ b/rust/stackable-cockpit/src/platform/release/spec.rs
@@ -13,6 +13,7 @@ use crate::{
         operator::{self, ChartSourceType, OperatorSpec},
         product,
     },
+    utils::k8s::{self, Client},
 };
 
 type Result<T, E = Error> = std::result::Result<T, E>;
@@ -30,6 +31,15 @@ pub enum Error {
 
     #[snafu(display("failed to launch background task"))]
     BackgroundTask { source: JoinError },
+
+    #[snafu(display("failed to deploy manifests using the kube client"))]
+    DeployManifest { source: k8s::Error },
+
+    #[snafu(display("failed to access the CRDs from source"))]
+    AccessCRDs { source: reqwest::Error },
+
+    #[snafu(display("failed to read CRD manifests"))]
+    ReadManifests { source: reqwest::Error },
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -108,6 +118,72 @@ impl ReleaseSpec {
             .await
     }
 
+    /// Upgrades a release by upgrading individual operators.
+    #[instrument(skip_all, fields(
+        %namespace,
+    ))]
+    pub async fn upgrade_crds(
+        &self,
+        include_products: &[String],
+        exclude_products: &[String],
+        namespace: &str,
+        k8s_client: &Client,
+    ) -> Result<()> {
+        debug!("Upgrading CRDs for release");
+
+        include_products.iter().for_each(|product| {
+            Span::current().record("product.included", product);
+        });
+        exclude_products.iter().for_each(|product| {
+            Span::current().record("product.excluded", product);
+        });
+
+        let client = reqwest::Client::new();
+
+        let operators = self.filter_products(include_products, exclude_products);
+
+        Span::current().pb_set_style(
+            &ProgressStyle::with_template("Upgrading CRDs {wide_bar} {pos}/{len}").unwrap(),
+        );
+        Span::current().pb_set_length(operators.len() as u64);
+
+        for (product_name, product) in operators {
+            Span::current().record("product_name", &product_name);
+            debug!("Upgrading CRDs for {product_name}-operator");
+
+            let release = match product.version.pre.as_str() {
+                "dev" => "main".to_string(),
+                _ => {
+                    format!("{}", product.version)
+                }
+            };
+
+            let request_url = format!(
+                "https://raw.githubusercontent.com/stackabletech/{product_name}-operator/{release}/deploy/helm/{product_name}-operator/crds/crds.yaml"
+            );
+
+            // Get CRD manifests
+            // TODO bei nicht 200 Status, Fehler werfen
+            let response = client
+                .get(request_url)
+                .send()
+                .await
+                .context(AccessCRDsSnafu)?;
+            let crd_manifests = response.text().await.context(ReadManifestsSnafu)?;
+
+            // Upgrade CRDs
+            k8s_client
+                .replace_crds(&crd_manifests)
+                .await
+                .context(DeployManifestSnafu)?;
+
+            debug!("Upgraded {product_name}-operator CRDs");
+            Span::current().pb_inc(1);
+        }
+
+        Ok(())
+    }
+
     #[instrument(skip_all)]
     pub fn uninstall(&self, namespace: &str) -> Result<()> {
         info!("Uninstalling release");
diff --git a/rust/stackable-cockpit/src/utils/k8s/client.rs b/rust/stackable-cockpit/src/utils/k8s/client.rs
index c87b7118..9c0228b8 100644
--- a/rust/stackable-cockpit/src/utils/k8s/client.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/client.rs
@@ -65,6 +65,9 @@ pub enum Error {
     #[snafu(display("failed to retrieve cluster information"))]
     ClusterInformation { source: cluster::Error },
 
+    #[snafu(display("failed to retrieve previous resource version information"))]
+    ResourceVersion { source: kube::error::Error },
+
     #[snafu(display("invalid or empty secret data in '{secret_name}'"))]
     InvalidSecretData { secret_name: String },
 
@@ -148,6 +151,46 @@ impl Client {
         Ok(())
     }
 
+    pub async fn replace_crds(&self, crds: &str) -> Result<()> {
+        for crd in serde_yaml::Deserializer::from_str(crds) {
+            let mut object = DynamicObject::deserialize(crd).context(DeserializeYamlSnafu)?;
+
+            let object_type = object.types.as_ref().ok_or(
+                ObjectTypeSnafu {
+                    object: object.clone(),
+                }
+                .build(),
+            )?;
+
+            let gvk = Self::gvk_of_typemeta(object_type);
+            let (resource, _) = self
+                .resolve_gvk(&gvk)
+                .await?
+                .context(GVKUnkownSnafu { gvk })?;
+
+            // CRDs are cluster scoped
+            let api: Api<DynamicObject> = Api::all_with(self.client.clone(), &resource);
+
+            if let Some(resource) = api
+                .get_opt(&object.name_any())
+                .await
+                .context(KubeClientFetchSnafu)?
+            {
+                object.metadata.resource_version = resource.resource_version();
+                api.replace(&object.name_any(), &PostParams::default(), &object)
+                    .await
+                    .context(KubeClientPatchSnafu)?;
+            } else {
+                // Create CRD if a previous version wasn't found
+                api.create(&PostParams::default(), &object)
+                    .await
+                    .context(KubeClientCreateSnafu)?;
+            }
+        }
+
+        Ok(())
+    }
+
     /// Lists objects by looking up a GVK via the discovery. It returns an
     /// optional list of dynamic objects. The method returns [`Ok(None)`]
     /// if the client was unable to resolve the GVK. An error is returned
diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index bbaee551..0a386cb7 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -44,6 +44,9 @@ pub enum ReleaseCommands {
     /// Uninstall a release
     #[command(aliases(["rm", "un"]))]
     Uninstall(ReleaseUninstallArgs),
+
+    // Upgrade a release
+    Upgrade(ReleaseUpgradeArgs),
 }
 
 #[derive(Debug, Args)]
@@ -83,6 +86,25 @@ pub struct ReleaseInstallArgs {
     local_cluster: CommonClusterArgs,
 }
 
+#[derive(Debug, Args)]
+pub struct ReleaseUpgradeArgs {
+    /// Release to upgrade to
+    #[arg(name = "RELEASE")]
+    release: String,
+
+    /// List of product operators to upgrade
+    #[arg(short, long = "include", group = "products")]
+    included_products: Vec<String>,
+
+    /// Blacklist of product operators to install
+    #[arg(short, long = "exclude", group = "products")]
+    excluded_products: Vec<String>,
+
+    /// Namespace in the cluster used to deploy the operators
+    #[arg(long, default_value = DEFAULT_OPERATOR_NAMESPACE, visible_aliases(["operator-ns"]))]
+    pub operator_namespace: String,
+}
+
 #[derive(Debug, Args)]
 pub struct ReleaseUninstallArgs {
     /// Name of the release to uninstall
@@ -111,6 +133,9 @@ pub enum CmdError {
     #[snafu(display("failed to install release"))]
     ReleaseInstall { source: release::Error },
 
+    #[snafu(display("failed to upgrade CRDs for release"))]
+    CrdUpgrade { source: release::Error },
+
     #[snafu(display("failed to uninstall release"))]
     ReleaseUninstall { source: release::Error },
 
@@ -146,6 +171,7 @@ impl ReleaseArgs {
             ReleaseCommands::Describe(args) => describe_cmd(args, cli, release_list).await,
             ReleaseCommands::Install(args) => install_cmd(args, cli, release_list).await,
             ReleaseCommands::Uninstall(args) => uninstall_cmd(args, cli, release_list).await,
+            ReleaseCommands::Upgrade(args) => upgrade_cmd(args, cli, release_list).await,
         }
     }
 }
@@ -314,6 +340,60 @@ async fn install_cmd(
     }
 }
 
+#[instrument(skip(cli, release_list))]
+async fn upgrade_cmd(
+    args: &ReleaseUpgradeArgs,
+    cli: &Cli,
+    release_list: release::ReleaseList,
+) -> Result<String, CmdError> {
+    debug!(release = %args.release, "Upgrading release");
+    Span::current().pb_set_style(&ProgressStyle::with_template("").unwrap());
+
+    match release_list.get(&args.release) {
+        Some(release) => {
+            let mut output = cli.result();
+            let client = Client::new().await.context(KubeClientCreateSnafu)?;
+
+            // Uninstall the old operator release first
+            release
+                .uninstall(&args.operator_namespace)
+                .context(ReleaseUninstallSnafu)?;
+
+            // Upgrade the CRDs for all the operators to be upgraded
+            release
+                .upgrade_crds(
+                    &args.included_products,
+                    &args.excluded_products,
+                    &args.operator_namespace,
+                    &client,
+                )
+                .await
+                .context(CrdUpgradeSnafu)?;
+
+            // Install the new operator release
+            release
+                .install(
+                    &args.included_products,
+                    &args.excluded_products,
+                    &args.operator_namespace,
+                    &ChartSourceType::from(cli.chart_type()),
+                )
+                .await
+                .context(ReleaseInstallSnafu)?;
+
+            output
+                .with_command_hint(
+                    "stackablectl operator installed",
+                    "list installed operators",
+                )
+                .with_output(format!("Upgraded to release '{}'", args.release));
+
+            Ok(output.render())
+        }
+        None => Ok("No such release".into()),
+    }
+}
+
 #[instrument(skip(cli, release_list))]
 async fn uninstall_cmd(
     args: &ReleaseUninstallArgs,

From 20a3ef032d7f54abc2ad45bf44e4d81ba50a85c4 Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Wed, 21 May 2025 16:35:02 +0200
Subject: [PATCH 02/17] error handling and include/exclude operators in/from
 upgrading

---
 .../src/platform/release/spec.rs              | 43 +++++++++++--------
 .../stackable-cockpit/src/utils/k8s/client.rs | 13 +++---
 rust/stackablectl/src/cmds/release.rs         | 13 ++++--
 3 files changed, 43 insertions(+), 26 deletions(-)

diff --git a/rust/stackable-cockpit/src/platform/release/spec.rs b/rust/stackable-cockpit/src/platform/release/spec.rs
index 1f443cda..1e738cb6 100644
--- a/rust/stackable-cockpit/src/platform/release/spec.rs
+++ b/rust/stackable-cockpit/src/platform/release/spec.rs
@@ -35,6 +35,9 @@ pub enum Error {
     #[snafu(display("failed to deploy manifests using the kube client"))]
     DeployManifest { source: k8s::Error },
 
+    #[snafu(display("failed to construct a valid request"))]
+    ConstructRequest { source: reqwest::Error },
+
     #[snafu(display("failed to access the CRDs from source"))]
     AccessCRDs { source: reqwest::Error },
 
@@ -129,7 +132,7 @@ impl ReleaseSpec {
         namespace: &str,
         k8s_client: &Client,
     ) -> Result<()> {
-        debug!("Upgrading CRDs for release");
+        info!("Upgrading CRDs for release");
 
         include_products.iter().for_each(|product| {
             Span::current().record("product.included", product);
@@ -139,19 +142,12 @@ impl ReleaseSpec {
         });
 
         let client = reqwest::Client::new();
-
         let operators = self.filter_products(include_products, exclude_products);
 
-        Span::current().pb_set_style(
-            &ProgressStyle::with_template("Upgrading CRDs {wide_bar} {pos}/{len}").unwrap(),
-        );
-        Span::current().pb_set_length(operators.len() as u64);
-
         for (product_name, product) in operators {
-            Span::current().record("product_name", &product_name);
-            debug!("Upgrading CRDs for {product_name}-operator");
+            info!("Upgrading CRDs for {product_name}-operator");
 
-            let release = match product.version.pre.as_str() {
+            let release_branch = match product.version.pre.as_str() {
                 "dev" => "main".to_string(),
                 _ => {
                     format!("{}", product.version)
@@ -159,16 +155,18 @@ impl ReleaseSpec {
             };
 
             let request_url = format!(
-                "https://raw.githubusercontent.com/stackabletech/{product_name}-operator/{release}/deploy/helm/{product_name}-operator/crds/crds.yaml"
+                "https://raw.githubusercontent.com/stackabletech/{product_name}-operator/{release_branch}/deploy/helm/{product_name}-operator/crds/crds.yaml"
             );
 
-            // Get CRD manifests
-            // TODO bei nicht 200 Status, Fehler werfen
+            // Get CRD manifests from request_url
             let response = client
                 .get(request_url)
                 .send()
                 .await
+                .context(ConstructRequestSnafu)?
+                .error_for_status()
                 .context(AccessCRDsSnafu)?;
+
             let crd_manifests = response.text().await.context(ReadManifestsSnafu)?;
 
             // Upgrade CRDs
@@ -177,18 +175,29 @@ impl ReleaseSpec {
                 .await
                 .context(DeployManifestSnafu)?;
 
-            debug!("Upgraded {product_name}-operator CRDs");
-            Span::current().pb_inc(1);
+            info!("Upgraded {product_name}-operator CRDs");
         }
 
         Ok(())
     }
 
     #[instrument(skip_all)]
-    pub fn uninstall(&self, namespace: &str) -> Result<()> {
+    pub fn uninstall(&self,
+        include_products: &[String],
+        exclude_products: &[String],
+        namespace: &str) -> Result<()> {
         info!("Uninstalling release");
 
-        for (product_name, product_spec) in &self.products {
+        include_products.iter().for_each(|product| {
+            Span::current().record("product.included", product);
+        });
+        exclude_products.iter().for_each(|product| {
+            Span::current().record("product.excluded", product);
+        });
+
+        let operators = self.filter_products(include_products, exclude_products);
+
+        for (product_name, product_spec) in operators {
             info!("Uninstalling {product_name}-operator");
 
             // Create operator spec
diff --git a/rust/stackable-cockpit/src/utils/k8s/client.rs b/rust/stackable-cockpit/src/utils/k8s/client.rs
index 9c0228b8..471dacd0 100644
--- a/rust/stackable-cockpit/src/utils/k8s/client.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/client.rs
@@ -37,6 +37,9 @@ pub enum Error {
     #[snafu(display("failed to patch/create Kubernetes object"))]
     KubeClientPatch { source: kube::error::Error },
 
+    #[snafu(display("failed to replace Kubernetes object"))]
+    KubeClientReplace { source: kube::error::Error },
+
     #[snafu(display("failed to deserialize YAML data"))]
     DeserializeYaml { source: serde_yaml::Error },
 
@@ -65,9 +68,6 @@ pub enum Error {
     #[snafu(display("failed to retrieve cluster information"))]
     ClusterInformation { source: cluster::Error },
 
-    #[snafu(display("failed to retrieve previous resource version information"))]
-    ResourceVersion { source: kube::error::Error },
-
     #[snafu(display("invalid or empty secret data in '{secret_name}'"))]
     InvalidSecretData { secret_name: String },
 
@@ -151,6 +151,9 @@ impl Client {
         Ok(())
     }
 
+    /// Replaces CRDs defined the in raw `crds` YAML string. This
+    /// method will fail if it is unable to parse the CRDs, unable to
+    /// resolve GVKs or unable to replace/create the dynamic objects.
     pub async fn replace_crds(&self, crds: &str) -> Result<()> {
         for crd in serde_yaml::Deserializer::from_str(crds) {
             let mut object = DynamicObject::deserialize(crd).context(DeserializeYamlSnafu)?;
@@ -179,12 +182,12 @@ impl Client {
                 object.metadata.resource_version = resource.resource_version();
                 api.replace(&object.name_any(), &PostParams::default(), &object)
                     .await
-                    .context(KubeClientPatchSnafu)?;
+                    .context(KubeClientReplaceSnafu)?;
             } else {
                 // Create CRD if a previous version wasn't found
                 api.create(&PostParams::default(), &object)
                     .await
-                    .context(KubeClientCreateSnafu)?;
+                    .context(KubeClientPatchSnafu)?;
             }
         }
 
diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 0a386cb7..11b981d9 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -346,8 +346,7 @@ async fn upgrade_cmd(
     cli: &Cli,
     release_list: release::ReleaseList,
 ) -> Result<String, CmdError> {
-    debug!(release = %args.release, "Upgrading release");
-    Span::current().pb_set_style(&ProgressStyle::with_template("").unwrap());
+    info!(release = %args.release, "Upgrading release");
 
     match release_list.get(&args.release) {
         Some(release) => {
@@ -356,7 +355,10 @@ async fn upgrade_cmd(
 
             // Uninstall the old operator release first
             release
-                .uninstall(&args.operator_namespace)
+                .uninstall(
+                    &args.included_products,
+                    &args.excluded_products,
+                    &args.operator_namespace)
                 .context(ReleaseUninstallSnafu)?;
 
             // Upgrade the CRDs for all the operators to be upgraded
@@ -403,7 +405,10 @@ async fn uninstall_cmd(
     match release_list.get(&args.release) {
         Some(release) => {
             release
-                .uninstall(&args.operator_namespace)
+                .uninstall(
+                    &Vec::new(),
+                    &Vec::new(),
+                    &args.operator_namespace)
                 .context(ReleaseUninstallSnafu)?;
 
             let mut result = cli.result();

From 45ab412ed6d10d32a39638b33792f540726859d3 Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Thu, 22 May 2025 08:36:32 +0200
Subject: [PATCH 03/17] docs and autogenerated parts

---
 .../stackablectl/pages/commands/release.adoc  |  68 ++++--
 .../partials/commands/release.adoc            |   1 +
 extra/completions/_stackablectl               |  55 +++++
 extra/completions/stackablectl.bash           | 195 +++++++++++++++++-
 extra/completions/stackablectl.elv            |  32 +++
 extra/completions/stackablectl.fish           |  49 +++--
 extra/completions/stackablectl.nu             |  32 +++
 .../src/platform/release/spec.rs              |   6 +-
 rust/stackablectl/src/cmds/release.rs         |  12 +-
 9 files changed, 403 insertions(+), 47 deletions(-)

diff --git a/docs/modules/stackablectl/pages/commands/release.adoc b/docs/modules/stackablectl/pages/commands/release.adoc
index fb3c750e..a1ef3be1 100644
--- a/docs/modules/stackablectl/pages/commands/release.adoc
+++ b/docs/modules/stackablectl/pages/commands/release.adoc
@@ -14,25 +14,35 @@ To list the available Stackable releases run the following command:
 [source,console]
 ----
 $ stackablectl release list
-┌───┬─────────┬──────────────┬─────────────────────────────────────────────────────────────────────────────┐
-│ # ┆ RELEASE ┆ RELEASE DATE ┆ DESCRIPTION                                                                 │
-╞═══╪═════════╪══════════════╪═════════════════════════════════════════════════════════════════════════════╡
-│ 1 ┆ 23.7    ┆ 2023-07-26   ┆ Sixth release focusing on resources and pod overrides                       │
-├╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│ 2 ┆ 23.4    ┆ 2023-05-17   ┆ Fifth release focusing on affinities and product status                     │
-├╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│ 3 ┆ 23.1    ┆ 2023-01-27   ┆ Fourth release focusing on image selection and logging                      │
-├╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│ 4 ┆ 22.11   ┆ 2022-11-14   ┆ Third release focusing on resource management                               │
-├╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│ 5 ┆ 22.09   ┆ 2022-09-09   ┆ Second release focusing on security and OpenShift support                   │
-├╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│ 6 ┆ 22.06   ┆ 2022-06-30   ┆ First official release of the Stackable Data Platform                       │
-├╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│ 7 ┆ latest  ┆ 2023-07-26   ┆ Always pointing to the latest stable version of the Stackable Data Platform │
-├╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
-│ 8 ┆ dev     ┆ 2023-01-27   ┆ Development versions from main branch. Not stable!                          │
-└───┴─────────┴──────────────┴─────────────────────────────────────────────────────────────────────────────┘
+┌────┬─────────┬──────────────┬─────────────────────────────────────────────────────────────────────────────────┐
+│ #  ┆ RELEASE ┆ RELEASE DATE ┆ DESCRIPTION                                                                     │
+╞════╪═════════╪══════════════╪═════════════════════════════════════════════════════════════════════════════════╡
+│ 1  ┆ 25.3    ┆ 2025-03-24   ┆ The March 2025 release                                                          │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 2  ┆ 24.11   ┆ 2024-11-18   ┆ The November 2024 release                                                       │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 3  ┆ 24.7    ┆ 2024-07-24   ┆ Security focused release to reduce CVEs and to build product images from source │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 4  ┆ 24.3    ┆ 2024-03-20   ┆ Security focused release to improve platform authentication and authorization   │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 5  ┆ 23.11   ┆ 2023-11-30   ┆ Seventh release focusing on PodDisruptionBudgets and graceful shutdown          │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 6  ┆ 23.7    ┆ 2023-07-26   ┆ Sixth release focusing on resources and pod overrides                           │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 7  ┆ 23.4    ┆ 2023-05-17   ┆ Fifth release focusing on affinities and product status                         │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 8  ┆ 23.1    ┆ 2023-01-27   ┆ Fourth release focusing on image selection and logging                          │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 9  ┆ 22.11   ┆ 2022-11-14   ┆ Third release focusing on resource management                                   │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 10 ┆ 22.09   ┆ 2022-09-09   ┆ Second release focusing on security and OpenShift support                       │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 11 ┆ 22.06   ┆ 2022-06-30   ┆ First official release of the Stackable Data Platform                           │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 12 ┆ latest  ┆ 2025-03-24   ┆ Always pointing to the latest stable version of the Stackable Data Platform     │
+├╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ 13 ┆ dev     ┆ 1970-01-01   ┆ Development versions from main branch. Not stable!                              │
+└────┴─────────┴──────────────┴─────────────────────────────────────────────────────────────────────────────────┘
 ----
 
 Detailed information of a release can be queried with the `stackablectl release describe` command:
@@ -128,3 +138,23 @@ Installed product trino=23.7.0
 Installed product zookeeper=23.7.0
 Installed release 23.7
 ----
+
+== Upgrading Releases
+
+As described in the xref:home::release-notes.adoc[Upgrade sections of the Release Notes], the upgrade process can be achieved by the following three steps:
+
+. Uninstalling the previous release with `stackablectl release uninstall <RELEASE>`
+. Replacing the CRDs with `kubectl replace`
+. Installing the next release with `stackablectl release install <RELEASE>`
+
+For convenience `stackablectl` also provides an upgrade functionality which executes those steps by itself.
+To upgrade a release, run the following command:
+
+[source,console]
+----
+$ stackablectl release upgrade 25.3
+
+Upgraded to release '25.3'
+
+Use "stackablectl operator installed" to list installed operators.
+----
diff --git a/docs/modules/stackablectl/partials/commands/release.adoc b/docs/modules/stackablectl/partials/commands/release.adoc
index a2030bd4..eac12e67 100644
--- a/docs/modules/stackablectl/partials/commands/release.adoc
+++ b/docs/modules/stackablectl/partials/commands/release.adoc
@@ -10,6 +10,7 @@ Commands:
   describe   Print out detailed release information
   install    Install a specific release
   uninstall  Uninstall a release
+  upgrade    Upgrade a release
   help       Print this message or the help of the given subcommand(s)
 
 Options:
diff --git a/extra/completions/_stackablectl b/extra/completions/_stackablectl
index 25c69764..768a339a 100644
--- a/extra/completions/_stackablectl
+++ b/extra/completions/_stackablectl
@@ -418,6 +418,35 @@ repo\:"index.yaml-based repositories\: resolution (dev, test, stable) is based o
 ':RELEASE -- Name of the release to uninstall:' \
 && ret=0
 ;;
+(upgrade)
+_arguments "${_arguments_options[@]}" : \
+'*-i+[List of product operators to upgrade]:INCLUDED_PRODUCTS: ' \
+'*--include=[List of product operators to upgrade]:INCLUDED_PRODUCTS: ' \
+'*-e+[Blacklist of product operators to install]:EXCLUDED_PRODUCTS: ' \
+'*--exclude=[Blacklist of product operators to install]:EXCLUDED_PRODUCTS: ' \
+'--operator-namespace=[Namespace in the cluster used to deploy the operators]:OPERATOR_NAMESPACE: ' \
+'--operator-ns=[Namespace in the cluster used to deploy the operators]:OPERATOR_NAMESPACE: ' \
+'-l+[Log level this application uses]:LOG_LEVEL: ' \
+'--log-level=[Log level this application uses]:LOG_LEVEL: ' \
+'*-d+[Provide one or more additional (custom) demo file(s)]:DEMO_FILE:_files' \
+'*--demo-file=[Provide one or more additional (custom) demo file(s)]:DEMO_FILE:_files' \
+'*-s+[Provide one or more additional (custom) stack file(s)]:STACK_FILE:_files' \
+'*--stack-file=[Provide one or more additional (custom) stack file(s)]:STACK_FILE:_files' \
+'*-r+[Provide one or more additional (custom) release file(s)]:RELEASE_FILE:_files' \
+'*--release-file=[Provide one or more additional (custom) release file(s)]:RELEASE_FILE:_files' \
+'--helm-repo-stable=[Provide a custom Helm stable repository URL]:URL:_urls' \
+'--helm-repo-test=[Provide a custom Helm test repository URL]:URL:_urls' \
+'--helm-repo-dev=[Provide a custom Helm dev repository URL]:URL:_urls' \
+'--chart-source=[Source the charts from either a OCI registry or from index.yaml-based repositories]:CHART_SOURCE:((oci\:"OCI registry"
+repo\:"index.yaml-based repositories\: resolution (dev, test, stable) is based on the version and thus will be operator-specific"))' \
+'--no-cache[Do not cache the remote (default) demo, stack and release files]' \
+'-h[Print help (see more with '\''--help'\'')]' \
+'--help[Print help (see more with '\''--help'\'')]' \
+'-V[Print version]' \
+'--version[Print version]' \
+':RELEASE -- Upgrade to the specified release:' \
+&& ret=0
+;;
 (help)
 _arguments "${_arguments_options[@]}" : \
 ":: :_stackablectl__release__help_commands" \
@@ -446,6 +475,10 @@ _arguments "${_arguments_options[@]}" : \
 _arguments "${_arguments_options[@]}" : \
 && ret=0
 ;;
+(upgrade)
+_arguments "${_arguments_options[@]}" : \
+&& ret=0
+;;
 (help)
 _arguments "${_arguments_options[@]}" : \
 && ret=0
@@ -1315,6 +1348,10 @@ _arguments "${_arguments_options[@]}" : \
 (uninstall)
 _arguments "${_arguments_options[@]}" : \
 && ret=0
+;;
+(upgrade)
+_arguments "${_arguments_options[@]}" : \
+&& ret=0
 ;;
         esac
     ;;
@@ -1820,6 +1857,7 @@ _stackablectl__help__release_commands() {
 'describe:Print out detailed release information' \
 'install:Install a specific release' \
 'uninstall:Uninstall a release' \
+'upgrade:Upgrade a release' \
     )
     _describe -t commands 'stackablectl help release commands' commands "$@"
 }
@@ -1843,6 +1881,11 @@ _stackablectl__help__release__uninstall_commands() {
     local commands; commands=()
     _describe -t commands 'stackablectl help release uninstall commands' commands "$@"
 }
+(( $+functions[_stackablectl__help__release__upgrade_commands] )) ||
+_stackablectl__help__release__upgrade_commands() {
+    local commands; commands=()
+    _describe -t commands 'stackablectl help release upgrade commands' commands "$@"
+}
 (( $+functions[_stackablectl__help__stack_commands] )) ||
 _stackablectl__help__stack_commands() {
     local commands; commands=(
@@ -1971,6 +2014,7 @@ _stackablectl__release_commands() {
 'describe:Print out detailed release information' \
 'install:Install a specific release' \
 'uninstall:Uninstall a release' \
+'upgrade:Upgrade a release' \
 'help:Print this message or the help of the given subcommand(s)' \
     )
     _describe -t commands 'stackablectl release commands' commands "$@"
@@ -1987,6 +2031,7 @@ _stackablectl__release__help_commands() {
 'describe:Print out detailed release information' \
 'install:Install a specific release' \
 'uninstall:Uninstall a release' \
+'upgrade:Upgrade a release' \
 'help:Print this message or the help of the given subcommand(s)' \
     )
     _describe -t commands 'stackablectl release help commands' commands "$@"
@@ -2016,6 +2061,11 @@ _stackablectl__release__help__uninstall_commands() {
     local commands; commands=()
     _describe -t commands 'stackablectl release help uninstall commands' commands "$@"
 }
+(( $+functions[_stackablectl__release__help__upgrade_commands] )) ||
+_stackablectl__release__help__upgrade_commands() {
+    local commands; commands=()
+    _describe -t commands 'stackablectl release help upgrade commands' commands "$@"
+}
 (( $+functions[_stackablectl__release__install_commands] )) ||
 _stackablectl__release__install_commands() {
     local commands; commands=()
@@ -2031,6 +2081,11 @@ _stackablectl__release__uninstall_commands() {
     local commands; commands=()
     _describe -t commands 'stackablectl release uninstall commands' commands "$@"
 }
+(( $+functions[_stackablectl__release__upgrade_commands] )) ||
+_stackablectl__release__upgrade_commands() {
+    local commands; commands=()
+    _describe -t commands 'stackablectl release upgrade commands' commands "$@"
+}
 (( $+functions[_stackablectl__stack_commands] )) ||
 _stackablectl__stack_commands() {
     local commands; commands=(
diff --git a/extra/completions/stackablectl.bash b/extra/completions/stackablectl.bash
index 095ed06b..bd247a0c 100644
--- a/extra/completions/stackablectl.bash
+++ b/extra/completions/stackablectl.bash
@@ -201,6 +201,9 @@ _stackablectl() {
             stackablectl__help__release,uninstall)
                 cmd="stackablectl__help__release__uninstall"
                 ;;
+            stackablectl__help__release,upgrade)
+                cmd="stackablectl__help__release__upgrade"
+                ;;
             stackablectl__help__stack,describe)
                 cmd="stackablectl__help__stack__describe"
                 ;;
@@ -267,6 +270,9 @@ _stackablectl() {
             stackablectl__release,uninstall)
                 cmd="stackablectl__release__uninstall"
                 ;;
+            stackablectl__release,upgrade)
+                cmd="stackablectl__release__upgrade"
+                ;;
             stackablectl__release__help,describe)
                 cmd="stackablectl__release__help__describe"
                 ;;
@@ -282,6 +288,9 @@ _stackablectl() {
             stackablectl__release__help,uninstall)
                 cmd="stackablectl__release__help__uninstall"
                 ;;
+            stackablectl__release__help,upgrade)
+                cmd="stackablectl__release__help__upgrade"
+                ;;
             stackablectl__stack,describe)
                 cmd="stackablectl__stack__describe"
                 ;;
@@ -2883,7 +2892,7 @@ _stackablectl() {
             return 0
             ;;
         stackablectl__help__release)
-            opts="list describe install uninstall"
+            opts="list describe install uninstall upgrade"
             if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
                 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
                 return 0
@@ -2952,6 +2961,20 @@ _stackablectl() {
             COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
             return 0
             ;;
+        stackablectl__help__release__upgrade)
+            opts=""
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
         stackablectl__help__stack)
             opts="list describe install"
             if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
@@ -3985,7 +4008,7 @@ _stackablectl() {
             return 0
             ;;
         stackablectl__release)
-            opts="-l -d -s -r -h -V --log-level --no-cache --demo-file --stack-file --release-file --helm-repo-stable --helm-repo-test --helm-repo-dev --chart-source --help --version list describe install uninstall help"
+            opts="-l -d -s -r -h -V --log-level --no-cache --demo-file --stack-file --release-file --helm-repo-stable --helm-repo-test --helm-repo-dev --chart-source --help --version list describe install uninstall upgrade help"
             if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
                 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
                 return 0
@@ -4249,7 +4272,7 @@ _stackablectl() {
             return 0
             ;;
         stackablectl__release__help)
-            opts="list describe install uninstall help"
+            opts="list describe install uninstall upgrade help"
             if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
                 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
                 return 0
@@ -4332,6 +4355,20 @@ _stackablectl() {
             COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
             return 0
             ;;
+        stackablectl__release__help__upgrade)
+            opts=""
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
         stackablectl__release__install)
             opts="-i -e -c -l -d -s -r -h -V --include --exclude --operator-ns --operator-namespace --cluster --cluster-name --cluster-nodes --cluster-cp-nodes --log-level --no-cache --demo-file --stack-file --release-file --helm-repo-stable --helm-repo-test --helm-repo-dev --chart-source --help --version <RELEASE>"
             if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
@@ -4776,6 +4813,158 @@ _stackablectl() {
             COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
             return 0
             ;;
+        stackablectl__release__upgrade)
+            opts="-i -e -l -d -s -r -h -V --include --exclude --operator-ns --operator-namespace --log-level --no-cache --demo-file --stack-file --release-file --helm-repo-stable --helm-repo-test --helm-repo-dev --chart-source --help --version <RELEASE>"
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                --include)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -i)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --exclude)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -e)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --operator-namespace)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --operator-ns)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --log-level)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                -l)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --demo-file)
+                    local oldifs
+                    if [ -n "${IFS+x}" ]; then
+                        oldifs="$IFS"
+                    fi
+                    IFS=$'\n'
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    if [ -n "${oldifs+x}" ]; then
+                        IFS="$oldifs"
+                    fi
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
+                    return 0
+                    ;;
+                -d)
+                    local oldifs
+                    if [ -n "${IFS+x}" ]; then
+                        oldifs="$IFS"
+                    fi
+                    IFS=$'\n'
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    if [ -n "${oldifs+x}" ]; then
+                        IFS="$oldifs"
+                    fi
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
+                    return 0
+                    ;;
+                --stack-file)
+                    local oldifs
+                    if [ -n "${IFS+x}" ]; then
+                        oldifs="$IFS"
+                    fi
+                    IFS=$'\n'
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    if [ -n "${oldifs+x}" ]; then
+                        IFS="$oldifs"
+                    fi
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
+                    return 0
+                    ;;
+                -s)
+                    local oldifs
+                    if [ -n "${IFS+x}" ]; then
+                        oldifs="$IFS"
+                    fi
+                    IFS=$'\n'
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    if [ -n "${oldifs+x}" ]; then
+                        IFS="$oldifs"
+                    fi
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
+                    return 0
+                    ;;
+                --release-file)
+                    local oldifs
+                    if [ -n "${IFS+x}" ]; then
+                        oldifs="$IFS"
+                    fi
+                    IFS=$'\n'
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    if [ -n "${oldifs+x}" ]; then
+                        IFS="$oldifs"
+                    fi
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
+                    return 0
+                    ;;
+                -r)
+                    local oldifs
+                    if [ -n "${IFS+x}" ]; then
+                        oldifs="$IFS"
+                    fi
+                    IFS=$'\n'
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    if [ -n "${oldifs+x}" ]; then
+                        IFS="$oldifs"
+                    fi
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
+                    return 0
+                    ;;
+                --helm-repo-stable)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --helm-repo-test)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --helm-repo-dev)
+                    COMPREPLY=($(compgen -f "${cur}"))
+                    return 0
+                    ;;
+                --chart-source)
+                    COMPREPLY=($(compgen -W "oci repo" -- "${cur}"))
+                    return 0
+                    ;;
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
         stackablectl__stack)
             opts="-l -d -s -r -h -V --release --log-level --no-cache --demo-file --stack-file --release-file --helm-repo-stable --helm-repo-test --helm-repo-dev --chart-source --help --version list describe install help"
             if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
diff --git a/extra/completions/stackablectl.elv b/extra/completions/stackablectl.elv
index f943defe..2e55ca7d 100644
--- a/extra/completions/stackablectl.elv
+++ b/extra/completions/stackablectl.elv
@@ -224,6 +224,7 @@ set edit:completion:arg-completer[stackablectl] = {|@words|
             cand describe 'Print out detailed release information'
             cand install 'Install a specific release'
             cand uninstall 'Uninstall a release'
+            cand upgrade 'Upgrade a release'
             cand help 'Print this message or the help of the given subcommand(s)'
         }
         &'stackablectl;release;list'= {
@@ -319,11 +320,37 @@ set edit:completion:arg-completer[stackablectl] = {|@words|
             cand -V 'Print version'
             cand --version 'Print version'
         }
+        &'stackablectl;release;upgrade'= {
+            cand -i 'List of product operators to upgrade'
+            cand --include 'List of product operators to upgrade'
+            cand -e 'Blacklist of product operators to install'
+            cand --exclude 'Blacklist of product operators to install'
+            cand --operator-namespace 'Namespace in the cluster used to deploy the operators'
+            cand --operator-ns 'Namespace in the cluster used to deploy the operators'
+            cand -l 'Log level this application uses'
+            cand --log-level 'Log level this application uses'
+            cand -d 'Provide one or more additional (custom) demo file(s)'
+            cand --demo-file 'Provide one or more additional (custom) demo file(s)'
+            cand -s 'Provide one or more additional (custom) stack file(s)'
+            cand --stack-file 'Provide one or more additional (custom) stack file(s)'
+            cand -r 'Provide one or more additional (custom) release file(s)'
+            cand --release-file 'Provide one or more additional (custom) release file(s)'
+            cand --helm-repo-stable 'Provide a custom Helm stable repository URL'
+            cand --helm-repo-test 'Provide a custom Helm test repository URL'
+            cand --helm-repo-dev 'Provide a custom Helm dev repository URL'
+            cand --chart-source 'Source the charts from either a OCI registry or from index.yaml-based repositories'
+            cand --no-cache 'Do not cache the remote (default) demo, stack and release files'
+            cand -h 'Print help (see more with ''--help'')'
+            cand --help 'Print help (see more with ''--help'')'
+            cand -V 'Print version'
+            cand --version 'Print version'
+        }
         &'stackablectl;release;help'= {
             cand list 'List available releases'
             cand describe 'Print out detailed release information'
             cand install 'Install a specific release'
             cand uninstall 'Uninstall a release'
+            cand upgrade 'Upgrade a release'
             cand help 'Print this message or the help of the given subcommand(s)'
         }
         &'stackablectl;release;help;list'= {
@@ -334,6 +361,8 @@ set edit:completion:arg-completer[stackablectl] = {|@words|
         }
         &'stackablectl;release;help;uninstall'= {
         }
+        &'stackablectl;release;help;upgrade'= {
+        }
         &'stackablectl;release;help;help'= {
         }
         &'stackablectl;stack'= {
@@ -916,6 +945,7 @@ set edit:completion:arg-completer[stackablectl] = {|@words|
             cand describe 'Print out detailed release information'
             cand install 'Install a specific release'
             cand uninstall 'Uninstall a release'
+            cand upgrade 'Upgrade a release'
         }
         &'stackablectl;help;release;list'= {
         }
@@ -925,6 +955,8 @@ set edit:completion:arg-completer[stackablectl] = {|@words|
         }
         &'stackablectl;help;release;uninstall'= {
         }
+        &'stackablectl;help;release;upgrade'= {
+        }
         &'stackablectl;help;stack'= {
             cand list 'List available stacks'
             cand describe 'Describe a specific stack'
diff --git a/extra/completions/stackablectl.fish b/extra/completions/stackablectl.fish
index 7fe840ee..28cb3fa0 100644
--- a/extra/completions/stackablectl.fish
+++ b/extra/completions/stackablectl.fish
@@ -132,22 +132,23 @@ complete -c stackablectl -n "__fish_stackablectl_using_subcommand operator; and
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand operator; and __fish_seen_subcommand_from help" -f -a "uninstall" -d 'Uninstall one or more operators'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand operator; and __fish_seen_subcommand_from help" -f -a "installed" -d 'List installed operators'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand operator; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -s l -l log-level -d 'Log level this application uses' -r
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -s d -l demo-file -d 'Provide one or more additional (custom) demo file(s)' -r -F
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -s s -l stack-file -d 'Provide one or more additional (custom) stack file(s)' -r -F
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -s r -l release-file -d 'Provide one or more additional (custom) release file(s)' -r -F
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -l helm-repo-stable -d 'Provide a custom Helm stable repository URL' -r -f
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -l helm-repo-test -d 'Provide a custom Helm test repository URL' -r -f
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -l helm-repo-dev -d 'Provide a custom Helm dev repository URL' -r -f
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -l chart-source -d 'Source the charts from either a OCI registry or from index.yaml-based repositories' -r -f -a "{oci\t'OCI registry',repo\t'index.yaml-based repositories: resolution (dev, test, stable) is based on the version and thus will be operator-specific'}"
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -l no-cache -d 'Do not cache the remote (default) demo, stack and release files'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -s h -l help -d 'Print help (see more with \'--help\')'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -s V -l version -d 'Print version'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -f -a "list" -d 'List available releases'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -f -a "describe" -d 'Print out detailed release information'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -f -a "install" -d 'Install a specific release'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -f -a "uninstall" -d 'Uninstall a release'
-complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -s l -l log-level -d 'Log level this application uses' -r
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -s d -l demo-file -d 'Provide one or more additional (custom) demo file(s)' -r -F
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -s s -l stack-file -d 'Provide one or more additional (custom) stack file(s)' -r -F
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -s r -l release-file -d 'Provide one or more additional (custom) release file(s)' -r -F
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -l helm-repo-stable -d 'Provide a custom Helm stable repository URL' -r -f
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -l helm-repo-test -d 'Provide a custom Helm test repository URL' -r -f
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -l helm-repo-dev -d 'Provide a custom Helm dev repository URL' -r -f
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -l chart-source -d 'Source the charts from either a OCI registry or from index.yaml-based repositories' -r -f -a "{oci\t'OCI registry',repo\t'index.yaml-based repositories: resolution (dev, test, stable) is based on the version and thus will be operator-specific'}"
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -l no-cache -d 'Do not cache the remote (default) demo, stack and release files'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -s h -l help -d 'Print help (see more with \'--help\')'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -s V -l version -d 'Print version'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -f -a "list" -d 'List available releases'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -f -a "describe" -d 'Print out detailed release information'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -f -a "install" -d 'Install a specific release'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -f -a "uninstall" -d 'Uninstall a release'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -f -a "upgrade" -d 'Upgrade a release'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and not __fish_seen_subcommand_from list describe install uninstall upgrade help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from list" -s o -l output -r -f -a "{plain\t'Print output formatted as plain text',table\t'Print output formatted as a table',json\t'Print output formatted as JSON',yaml\t'Print output formatted as YAML'}"
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from list" -s l -l log-level -d 'Log level this application uses' -r
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from list" -s d -l demo-file -d 'Provide one or more additional (custom) demo file(s)' -r -F
@@ -202,10 +203,25 @@ complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and _
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from uninstall" -l no-cache -d 'Do not cache the remote (default) demo, stack and release files'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from uninstall" -s h -l help -d 'Print help (see more with \'--help\')'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from uninstall" -s V -l version -d 'Print version'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s i -l include -d 'List of product operators to upgrade' -r
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s e -l exclude -d 'Blacklist of product operators to install' -r
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -l operator-namespace -l operator-ns -d 'Namespace in the cluster used to deploy the operators' -r
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s l -l log-level -d 'Log level this application uses' -r
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s d -l demo-file -d 'Provide one or more additional (custom) demo file(s)' -r -F
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s s -l stack-file -d 'Provide one or more additional (custom) stack file(s)' -r -F
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s r -l release-file -d 'Provide one or more additional (custom) release file(s)' -r -F
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -l helm-repo-stable -d 'Provide a custom Helm stable repository URL' -r -f
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -l helm-repo-test -d 'Provide a custom Helm test repository URL' -r -f
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -l helm-repo-dev -d 'Provide a custom Helm dev repository URL' -r -f
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -l chart-source -d 'Source the charts from either a OCI registry or from index.yaml-based repositories' -r -f -a "{oci\t'OCI registry',repo\t'index.yaml-based repositories: resolution (dev, test, stable) is based on the version and thus will be operator-specific'}"
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -l no-cache -d 'Do not cache the remote (default) demo, stack and release files'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s h -l help -d 'Print help (see more with \'--help\')'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from upgrade" -s V -l version -d 'Print version'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from help" -f -a "list" -d 'List available releases'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from help" -f -a "describe" -d 'Print out detailed release information'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from help" -f -a "install" -d 'Install a specific release'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from help" -f -a "uninstall" -d 'Uninstall a release'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from help" -f -a "upgrade" -d 'Upgrade a release'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand release; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand stack; and not __fish_seen_subcommand_from list describe install help" -l release -d 'Target a specific Stackable release' -r
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand stack; and not __fish_seen_subcommand_from list describe install help" -s l -l log-level -d 'Log level this application uses' -r
@@ -534,6 +550,7 @@ complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fi
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fish_seen_subcommand_from release" -f -a "describe" -d 'Print out detailed release information'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fish_seen_subcommand_from release" -f -a "install" -d 'Install a specific release'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fish_seen_subcommand_from release" -f -a "uninstall" -d 'Uninstall a release'
+complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fish_seen_subcommand_from release" -f -a "upgrade" -d 'Upgrade a release'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fish_seen_subcommand_from stack" -f -a "list" -d 'List available stacks'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fish_seen_subcommand_from stack" -f -a "describe" -d 'Describe a specific stack'
 complete -c stackablectl -n "__fish_stackablectl_using_subcommand help; and __fish_seen_subcommand_from stack" -f -a "install" -d 'Install a specific stack'
diff --git a/extra/completions/stackablectl.nu b/extra/completions/stackablectl.nu
index 448f1f63..6d899b1b 100644
--- a/extra/completions/stackablectl.nu
+++ b/extra/completions/stackablectl.nu
@@ -315,6 +315,30 @@ module completions {
     --version(-V)             # Print version
   ]
 
+  def "nu-complete stackablectl release upgrade chart_source" [] {
+    [ "oci" "repo" ]
+  }
+
+  # Upgrade a release
+  export extern "stackablectl release upgrade" [
+    RELEASE: string           # Upgrade to the specified release
+    --include(-i): string     # List of product operators to upgrade
+    --exclude(-e): string     # Blacklist of product operators to install
+    --operator-namespace: string # Namespace in the cluster used to deploy the operators
+    --operator-ns: string     # Namespace in the cluster used to deploy the operators
+    --log-level(-l): string   # Log level this application uses
+    --no-cache                # Do not cache the remote (default) demo, stack and release files
+    --demo-file(-d): string   # Provide one or more additional (custom) demo file(s)
+    --stack-file(-s): string  # Provide one or more additional (custom) stack file(s)
+    --release-file(-r): string # Provide one or more additional (custom) release file(s)
+    --helm-repo-stable: string # Provide a custom Helm stable repository URL
+    --helm-repo-test: string  # Provide a custom Helm test repository URL
+    --helm-repo-dev: string   # Provide a custom Helm dev repository URL
+    --chart-source: string@"nu-complete stackablectl release upgrade chart_source" # Source the charts from either a OCI registry or from index.yaml-based repositories
+    --help(-h)                # Print help (see more with '--help')
+    --version(-V)             # Print version
+  ]
+
   # Print this message or the help of the given subcommand(s)
   export extern "stackablectl release help" [
   ]
@@ -335,6 +359,10 @@ module completions {
   export extern "stackablectl release help uninstall" [
   ]
 
+  # Upgrade a release
+  export extern "stackablectl release help upgrade" [
+  ]
+
   # Print this message or the help of the given subcommand(s)
   export extern "stackablectl release help help" [
   ]
@@ -967,6 +995,10 @@ module completions {
   export extern "stackablectl help release uninstall" [
   ]
 
+  # Upgrade a release
+  export extern "stackablectl help release upgrade" [
+  ]
+
   # Interact with stacks, which are ready-to-use product combinations
   export extern "stackablectl help stack" [
   ]
diff --git a/rust/stackable-cockpit/src/platform/release/spec.rs b/rust/stackable-cockpit/src/platform/release/spec.rs
index 1e738cb6..7a2512b3 100644
--- a/rust/stackable-cockpit/src/platform/release/spec.rs
+++ b/rust/stackable-cockpit/src/platform/release/spec.rs
@@ -182,10 +182,12 @@ impl ReleaseSpec {
     }
 
     #[instrument(skip_all)]
-    pub fn uninstall(&self,
+    pub fn uninstall(
+        &self,
         include_products: &[String],
         exclude_products: &[String],
-        namespace: &str) -> Result<()> {
+        namespace: &str,
+    ) -> Result<()> {
         info!("Uninstalling release");
 
         include_products.iter().for_each(|product| {
diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 11b981d9..95baed09 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -45,7 +45,7 @@ pub enum ReleaseCommands {
     #[command(aliases(["rm", "un"]))]
     Uninstall(ReleaseUninstallArgs),
 
-    // Upgrade a release
+    /// Upgrade a release
     Upgrade(ReleaseUpgradeArgs),
 }
 
@@ -88,7 +88,7 @@ pub struct ReleaseInstallArgs {
 
 #[derive(Debug, Args)]
 pub struct ReleaseUpgradeArgs {
-    /// Release to upgrade to
+    /// Upgrade to the specified release
     #[arg(name = "RELEASE")]
     release: String,
 
@@ -358,7 +358,8 @@ async fn upgrade_cmd(
                 .uninstall(
                     &args.included_products,
                     &args.excluded_products,
-                    &args.operator_namespace)
+                    &args.operator_namespace,
+                )
                 .context(ReleaseUninstallSnafu)?;
 
             // Upgrade the CRDs for all the operators to be upgraded
@@ -405,10 +406,7 @@ async fn uninstall_cmd(
     match release_list.get(&args.release) {
         Some(release) => {
             release
-                .uninstall(
-                    &Vec::new(),
-                    &Vec::new(),
-                    &args.operator_namespace)
+                .uninstall(&Vec::new(), &Vec::new(), &args.operator_namespace)
                 .context(ReleaseUninstallSnafu)?;
 
             let mut result = cli.result();

From 8dd56cd4c2381203027d3d6a743224765bd1c29c Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Thu, 22 May 2025 17:35:08 +0200
Subject: [PATCH 04/17] only upgrade installed operators and specifically
 included ones

---
 rust/stackablectl/src/cmds/release.rs | 38 ++++++++++++++++++++++++---
 1 file changed, 34 insertions(+), 4 deletions(-)

diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 95baed09..919f2957 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -7,8 +7,14 @@ use snafu::{ResultExt, Snafu};
 use stackable_cockpit::{
     common::list,
     constants::DEFAULT_OPERATOR_NAMESPACE,
-    platform::{namespace, operator::ChartSourceType, release},
+    helm::{self, Release},
+    platform::{
+        namespace,
+        operator::{self, ChartSourceType},
+        release,
+    },
     utils::{
+        self,
         k8s::{self, Client},
         path::PathOrUrlParseError,
     },
@@ -118,6 +124,9 @@ pub struct ReleaseUninstallArgs {
 
 #[derive(Debug, Snafu)]
 pub enum CmdError {
+    #[snafu(display("Helm error"))]
+    HelmError { source: helm::Error },
+
     #[snafu(display("failed to serialize YAML output"))]
     SerializeYamlOutput { source: serde_yaml::Error },
 
@@ -353,19 +362,40 @@ async fn upgrade_cmd(
             let mut output = cli.result();
             let client = Client::new().await.context(KubeClientCreateSnafu)?;
 
+            // Get all currently installed operators to only upgrade those
+            let installed_charts: Vec<Release> =
+                helm::list_releases(&args.operator_namespace).context(HelmSnafu)?;
+
+            let mut operators: Vec<String> = operator::VALID_OPERATORS
+                .iter()
+                .filter(|operator| {
+                    installed_charts
+                        .iter()
+                        .any(|release| release.name == utils::operator_chart_name(operator))
+                })
+                .map(|operator| operator.to_string())
+                .collect();
+
             // Uninstall the old operator release first
             release
                 .uninstall(
-                    &args.included_products,
+                    &operators,
                     &args.excluded_products,
                     &args.operator_namespace,
                 )
                 .context(ReleaseUninstallSnafu)?;
 
+            // If operators were added to args.included_products, install them as well
+            for product in &args.included_products {
+                if !operators.contains(product) {
+                    operators.push(product.clone());
+                }
+            }
+
             // Upgrade the CRDs for all the operators to be upgraded
             release
                 .upgrade_crds(
-                    &args.included_products,
+                    &operators,
                     &args.excluded_products,
                     &args.operator_namespace,
                     &client,
@@ -376,7 +406,7 @@ async fn upgrade_cmd(
             // Install the new operator release
             release
                 .install(
-                    &args.included_products,
+                    &operators,
                     &args.excluded_products,
                     &args.operator_namespace,
                     &ChartSourceType::from(cli.chart_type()),

From ec6de2fd77b7cd526bd5074f563f9cd413859d0a Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Thu, 22 May 2025 17:56:14 +0200
Subject: [PATCH 05/17] update docs

---
 .../modules/stackablectl/pages/commands/release.adoc | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/docs/modules/stackablectl/pages/commands/release.adoc b/docs/modules/stackablectl/pages/commands/release.adoc
index a1ef3be1..80059440 100644
--- a/docs/modules/stackablectl/pages/commands/release.adoc
+++ b/docs/modules/stackablectl/pages/commands/release.adoc
@@ -158,3 +158,15 @@ Upgraded to release '25.3'
 
 Use "stackablectl operator installed" to list installed operators.
 ----
+
+The above command only upgrades the currently installed operators.
+To include additional operators in the installation step, use the `--include`/`-i` subcommands and specify the desired operators.
+
+For example
+[source,console]
+----
+$ stackablectl release upgrade 25.3 -i druid -i nifi
+----
+would upgrade the existing operators as well as install the Stackable operators for Apache Druid and Apache NiFi.
+
+Likewise, operators can be exluded from the upgrade using the `--exclude`/`-e` subcommands.

From a2a2cc493e5a8579cb3b7505098fd185e227e9ba Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Fri, 23 May 2025 09:16:21 +0200
Subject: [PATCH 06/17] add changelog entry

---
 rust/stackablectl/CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/rust/stackablectl/CHANGELOG.md b/rust/stackablectl/CHANGELOG.md
index 8daf172d..c16b6480 100644
--- a/rust/stackablectl/CHANGELOG.md
+++ b/rust/stackablectl/CHANGELOG.md
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
 
 - Pass the stack/demo namespace as a templating variable `NAMESPACE` to manifests.
   This should unblock demos to run in all namespaces, as they can template the namespace e.g. for the FQDN of services ([#355]).
+- Add release upgrade functionality to `stackablectl release` command through `upgrade` subcommand ([#379]).
 
 ### Changed
 
@@ -20,6 +21,7 @@ All notable changes to this project will be documented in this file.
 
 [#368]: https://github.com/stackabletech/stackable-cockpit/pull/368
 [#373]: https://github.com/stackabletech/stackable-cockpit/pull/373
+[#379]: https://github.com/stackabletech/stackable-cockpit/pull/379
 
 ## [25.3.0] - 2025-03-27
 

From f301c91261b7eb8f862d74be952f00c65322b1d2 Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Sun, 25 May 2025 16:11:40 +0200
Subject: [PATCH 07/17] add progress reporting to upgrade

---
 .../src/platform/release/spec.rs              | 72 +++++++++++--------
 rust/stackablectl/src/cmds/release.rs         |  3 +-
 2 files changed, 45 insertions(+), 30 deletions(-)

diff --git a/rust/stackable-cockpit/src/platform/release/spec.rs b/rust/stackable-cockpit/src/platform/release/spec.rs
index bbe501ba..73964088 100644
--- a/rust/stackable-cockpit/src/platform/release/spec.rs
+++ b/rust/stackable-cockpit/src/platform/release/spec.rs
@@ -134,6 +134,7 @@ impl ReleaseSpec {
     /// Upgrades a release by upgrading individual operators.
     #[instrument(skip_all, fields(
         %namespace,
+        indicatif.pb_show = true
     ))]
     pub async fn upgrade_crds(
         &self,
@@ -143,6 +144,7 @@ impl ReleaseSpec {
         k8s_client: &Client,
     ) -> Result<()> {
         info!("Upgrading CRDs for release");
+        Span::current().pb_set_style(&PROGRESS_BAR_STYLE);
 
         include_products.iter().for_each(|product| {
             Span::current().record("product.included", product);
@@ -154,38 +156,50 @@ impl ReleaseSpec {
         let client = reqwest::Client::new();
         let operators = self.filter_products(include_products, exclude_products);
 
+        Span::current().pb_set_length(operators.len() as u64);
+
         for (product_name, product) in operators {
             info!("Upgrading CRDs for {product_name}-operator");
+            let iter_span = tracing::info_span!("upgrade_crds_iter", indicatif.pb_show = true);
+
+            let client = client.clone();
+            async move {
+                Span::current().pb_set_message(format!("Ugrading CRDs for {product_name}-operator").as_str());
+
+                let release_branch = match product.version.pre.as_str() {
+                    "dev" => "main".to_string(),
+                    _ => {
+                        format!("{}", product.version)
+                    }
+                };
 
-            let release_branch = match product.version.pre.as_str() {
-                "dev" => "main".to_string(),
-                _ => {
-                    format!("{}", product.version)
-                }
-            };
-
-            let request_url = format!(
-                "https://raw.githubusercontent.com/stackabletech/{product_name}-operator/{release_branch}/deploy/helm/{product_name}-operator/crds/crds.yaml"
-            );
-
-            // Get CRD manifests from request_url
-            let response = client
-                .get(request_url)
-                .send()
-                .await
-                .context(ConstructRequestSnafu)?
-                .error_for_status()
-                .context(AccessCRDsSnafu)?;
-
-            let crd_manifests = response.text().await.context(ReadManifestsSnafu)?;
-
-            // Upgrade CRDs
-            k8s_client
-                .replace_crds(&crd_manifests)
-                .await
-                .context(DeployManifestSnafu)?;
-
-            info!("Upgraded {product_name}-operator CRDs");
+                let request_url = format!(
+                    "https://raw.githubusercontent.com/stackabletech/{product_name}-operator/{release_branch}/deploy/helm/{product_name}-operator/crds/crds.yaml"
+                );
+
+                // Get CRD manifests from request_url
+                let response = client
+                    .get(request_url)
+                    .send()
+                    .await
+                    .context(ConstructRequestSnafu)?
+                    .error_for_status()
+                    .context(AccessCRDsSnafu)?;
+
+                let crd_manifests = response.text().await.context(ReadManifestsSnafu)?;
+
+                // Upgrade CRDs
+                k8s_client
+                    .replace_crds(&crd_manifests)
+                    .await
+                    .context(DeployManifestSnafu)?;
+
+                info!("Upgraded {product_name}-operator CRDs");
+
+                Ok::<(), Error>(())
+            }.instrument(iter_span).await?;
+
+            Span::current().pb_inc(1);
         }
 
         Ok(())
diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index e4bb5c29..23f50c7c 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -354,13 +354,14 @@ async fn install_cmd(
     }
 }
 
-#[instrument(skip(cli, release_list))]
+#[instrument(skip(cli, release_list), fields(indicatif.pb_show = true))]
 async fn upgrade_cmd(
     args: &ReleaseUpgradeArgs,
     cli: &Cli,
     release_list: release::ReleaseList,
 ) -> Result<String, CmdError> {
     info!(release = %args.release, "Upgrading release");
+    Span::current().pb_set_message("Upgrading release");
 
     match release_list.get(&args.release) {
         Some(release) => {

From 2e6b8a5a878c179850661cd8a08fd63a4d3ff46d Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Mon, 26 May 2025 13:52:05 +0200
Subject: [PATCH 08/17] use to_string() instead format

---
 rust/stackable-cockpit/src/platform/release/spec.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/rust/stackable-cockpit/src/platform/release/spec.rs b/rust/stackable-cockpit/src/platform/release/spec.rs
index 73964088..be214ca4 100644
--- a/rust/stackable-cockpit/src/platform/release/spec.rs
+++ b/rust/stackable-cockpit/src/platform/release/spec.rs
@@ -169,7 +169,7 @@ impl ReleaseSpec {
                 let release_branch = match product.version.pre.as_str() {
                     "dev" => "main".to_string(),
                     _ => {
-                        format!("{}", product.version)
+                        product.version.to_string()
                     }
                 };
 

From 9f6f8e6af47fbc8668b9dd8609aaedf76b5d8245 Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Tue, 27 May 2025 09:57:36 +0200
Subject: [PATCH 09/17] use client with cache to fetch CRDs

---
 .../src/platform/release/spec.rs              | 45 ++++++++++---------
 rust/stackable-cockpit/src/xfer/mod.rs        |  2 +
 rust/stackablectl/src/cmds/release.rs         |  8 +++-
 3 files changed, 32 insertions(+), 23 deletions(-)

diff --git a/rust/stackable-cockpit/src/platform/release/spec.rs b/rust/stackable-cockpit/src/platform/release/spec.rs
index be214ca4..d9693625 100644
--- a/rust/stackable-cockpit/src/platform/release/spec.rs
+++ b/rust/stackable-cockpit/src/platform/release/spec.rs
@@ -14,7 +14,11 @@ use crate::{
         operator::{self, ChartSourceType, OperatorSpec},
         product,
     },
-    utils::k8s::{self, Client},
+    utils::{
+        k8s::{self, Client},
+        path::{IntoPathOrUrl as _, PathOrUrlParseError},
+    },
+    xfer::{self, processor::Text},
 };
 
 type Result<T, E = Error> = std::result::Result<T, E>;
@@ -24,6 +28,17 @@ pub enum Error {
     #[snafu(display("failed to parse operator spec"))]
     OperatorSpecParse { source: operator::SpecParseError },
 
+    /// This error indicates that parsing a string into a path or URL failed.
+    #[snafu(display("failed to parse '{path_or_url}' as path/url"))]
+    ParsePathOrUrl {
+        source: PathOrUrlParseError,
+        path_or_url: String,
+    },
+
+    /// This error indicates that receiving remote content failed.
+    #[snafu(display("failed to receive remote content"))]
+    FileTransfer { source: xfer::Error },
+
     #[snafu(display("failed to install release using Helm"))]
     HelmInstall { source: helm::Error },
 
@@ -35,15 +50,6 @@ pub enum Error {
 
     #[snafu(display("failed to deploy manifests using the kube client"))]
     DeployManifest { source: k8s::Error },
-
-    #[snafu(display("failed to construct a valid request"))]
-    ConstructRequest { source: reqwest::Error },
-
-    #[snafu(display("failed to access the CRDs from source"))]
-    AccessCRDs { source: reqwest::Error },
-
-    #[snafu(display("failed to read CRD manifests"))]
-    ReadManifests { source: reqwest::Error },
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -142,6 +148,7 @@ impl ReleaseSpec {
         exclude_products: &[String],
         namespace: &str,
         k8s_client: &Client,
+        transfer_client: &xfer::Client,
     ) -> Result<()> {
         info!("Upgrading CRDs for release");
         Span::current().pb_set_style(&PROGRESS_BAR_STYLE);
@@ -153,7 +160,6 @@ impl ReleaseSpec {
             Span::current().record("product.excluded", product);
         });
 
-        let client = reqwest::Client::new();
         let operators = self.filter_products(include_products, exclude_products);
 
         Span::current().pb_set_length(operators.len() as u64);
@@ -162,7 +168,6 @@ impl ReleaseSpec {
             info!("Upgrading CRDs for {product_name}-operator");
             let iter_span = tracing::info_span!("upgrade_crds_iter", indicatif.pb_show = true);
 
-            let client = client.clone();
             async move {
                 Span::current().pb_set_message(format!("Ugrading CRDs for {product_name}-operator").as_str());
 
@@ -173,20 +178,18 @@ impl ReleaseSpec {
                     }
                 };
 
-                let request_url = format!(
+                let request_url = &format!(
                     "https://raw.githubusercontent.com/stackabletech/{product_name}-operator/{release_branch}/deploy/helm/{product_name}-operator/crds/crds.yaml"
                 );
+                let request_url = request_url.into_path_or_url().context(ParsePathOrUrlSnafu {
+                    path_or_url: request_url.clone(),
+                })?;
 
                 // Get CRD manifests from request_url
-                let response = client
-                    .get(request_url)
-                    .send()
+                let crd_manifests: String = transfer_client
+                    .get(&request_url, &Text)
                     .await
-                    .context(ConstructRequestSnafu)?
-                    .error_for_status()
-                    .context(AccessCRDsSnafu)?;
-
-                let crd_manifests = response.text().await.context(ReadManifestsSnafu)?;
+                    .context(FileTransferSnafu)?;
 
                 // Upgrade CRDs
                 k8s_client
diff --git a/rust/stackable-cockpit/src/xfer/mod.rs b/rust/stackable-cockpit/src/xfer/mod.rs
index 87dde643..3792305b 100644
--- a/rust/stackable-cockpit/src/xfer/mod.rs
+++ b/rust/stackable-cockpit/src/xfer/mod.rs
@@ -120,6 +120,8 @@ impl Client {
             .client
             .execute(req)
             .await
+            .context(FetchRemoteContentSnafu)?
+            .error_for_status()
             .context(FetchRemoteContentSnafu)?;
 
         result.text().await.context(FetchRemoteContentSnafu)
diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 23f50c7c..8af8fcad 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -181,7 +181,9 @@ impl ReleaseArgs {
             ReleaseCommands::Describe(args) => describe_cmd(args, cli, release_list).await,
             ReleaseCommands::Install(args) => install_cmd(args, cli, release_list).await,
             ReleaseCommands::Uninstall(args) => uninstall_cmd(args, cli, release_list).await,
-            ReleaseCommands::Upgrade(args) => upgrade_cmd(args, cli, release_list).await,
+            ReleaseCommands::Upgrade(args) => {
+                upgrade_cmd(args, cli, release_list, &transfer_client).await
+            }
         }
     }
 }
@@ -354,11 +356,12 @@ async fn install_cmd(
     }
 }
 
-#[instrument(skip(cli, release_list), fields(indicatif.pb_show = true))]
+#[instrument(skip(cli, release_list, transfer_client), fields(indicatif.pb_show = true))]
 async fn upgrade_cmd(
     args: &ReleaseUpgradeArgs,
     cli: &Cli,
     release_list: release::ReleaseList,
+    transfer_client: &xfer::Client,
 ) -> Result<String, CmdError> {
     info!(release = %args.release, "Upgrading release");
     Span::current().pb_set_message("Upgrading release");
@@ -405,6 +408,7 @@ async fn upgrade_cmd(
                     &args.excluded_products,
                     &args.operator_namespace,
                     &client,
+                    transfer_client,
                 )
                 .await
                 .context(CrdUpgradeSnafu)?;

From 69b75c7e863d6f79e96d7dc1e098092f127ce8b1 Mon Sep 17 00:00:00 2001
From: Xenia <xenia.fischer@stackable.tech>
Date: Tue, 27 May 2025 10:10:11 +0200
Subject: [PATCH 10/17] Update rust/stackablectl/src/cmds/release.rs

Co-authored-by: Nick <10092581+NickLarsenNZ@users.noreply.github.com>
---
 rust/stackablectl/src/cmds/release.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 8af8fcad..635152ff 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -356,7 +356,7 @@ async fn install_cmd(
     }
 }
 
-#[instrument(skip(cli, release_list, transfer_client), fields(indicatif.pb_show = true))]
+#[instrument(skip_all, fields(indicatif.pb_show = true))]
 async fn upgrade_cmd(
     args: &ReleaseUpgradeArgs,
     cli: &Cli,

From 5e66494f1cec9d166883361f938a52ee3792e627 Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Tue, 27 May 2025 11:07:48 +0200
Subject: [PATCH 11/17] use Debug impl for String in Error

---
 rust/stackable-cockpit/src/platform/manifests.rs    | 2 +-
 rust/stackable-cockpit/src/platform/release/spec.rs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/rust/stackable-cockpit/src/platform/manifests.rs b/rust/stackable-cockpit/src/platform/manifests.rs
index c1b3a566..d9d23794 100644
--- a/rust/stackable-cockpit/src/platform/manifests.rs
+++ b/rust/stackable-cockpit/src/platform/manifests.rs
@@ -22,7 +22,7 @@ use crate::{
 #[derive(Debug, Snafu)]
 pub enum Error {
     /// This error indicates that parsing a string into a path or URL failed.
-    #[snafu(display("failed to parse '{path_or_url}' as path/url"))]
+    #[snafu(display("failed to parse {path_or_url:?} as path/url"))]
     ParsePathOrUrl {
         source: PathOrUrlParseError,
         path_or_url: String,
diff --git a/rust/stackable-cockpit/src/platform/release/spec.rs b/rust/stackable-cockpit/src/platform/release/spec.rs
index d9693625..13f589a9 100644
--- a/rust/stackable-cockpit/src/platform/release/spec.rs
+++ b/rust/stackable-cockpit/src/platform/release/spec.rs
@@ -29,7 +29,7 @@ pub enum Error {
     OperatorSpecParse { source: operator::SpecParseError },
 
     /// This error indicates that parsing a string into a path or URL failed.
-    #[snafu(display("failed to parse '{path_or_url}' as path/url"))]
+    #[snafu(display("failed to parse {path_or_url:?} as path/url"))]
     ParsePathOrUrl {
         source: PathOrUrlParseError,
         path_or_url: String,

From 5ee5c354d97d5b02e145eb141b1ccf5118efbdcf Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Tue, 27 May 2025 12:49:11 +0200
Subject: [PATCH 12/17] add gvk info to KubeClientReplace Error

---
 rust/stackable-cockpit/src/utils/k8s/client.rs | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/rust/stackable-cockpit/src/utils/k8s/client.rs b/rust/stackable-cockpit/src/utils/k8s/client.rs
index 092e6091..7b3f220f 100644
--- a/rust/stackable-cockpit/src/utils/k8s/client.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/client.rs
@@ -39,7 +39,10 @@ pub enum Error {
     KubeClientPatch { source: kube::error::Error },
 
     #[snafu(display("failed to replace Kubernetes object"))]
-    KubeClientReplace { source: kube::error::Error },
+    KubeClientReplace {
+        source: kube::error::Error,
+        gvk: GroupVersionKind,
+    },
 
     #[snafu(display("failed to deserialize YAML data"))]
     DeserializeYaml { source: serde_yaml::Error },
@@ -48,7 +51,7 @@ pub enum Error {
     GVKDiscoveryRun { source: kube::error::Error },
 
     #[snafu(display("GVK {gvk:?} is not known"))]
-    GVKUnkown { gvk: GroupVersionKind },
+    GVKUnknown { gvk: GroupVersionKind },
 
     #[snafu(display("failed to deploy manifest because type of object {object:?} is not set"))]
     ObjectType { object: DynamicObject },
@@ -131,7 +134,7 @@ impl Client {
             let (resource, capabilities) = self
                 .resolve_gvk(&gvk)
                 .await?
-                .context(GVKUnkownSnafu { gvk })?;
+                .context(GVKUnknownSnafu { gvk })?;
 
             let api: Api<DynamicObject> = match capabilities.scope {
                 Scope::Cluster => {
@@ -173,7 +176,7 @@ impl Client {
             let (resource, _) = self
                 .resolve_gvk(&gvk)
                 .await?
-                .context(GVKUnkownSnafu { gvk })?;
+                .context(GVKUnknownSnafu { gvk: gvk.clone() })?;
 
             // CRDs are cluster scoped
             let api: Api<DynamicObject> = Api::all_with(self.client.clone(), &resource);
@@ -186,7 +189,7 @@ impl Client {
                 object.metadata.resource_version = resource.resource_version();
                 api.replace(&object.name_any(), &PostParams::default(), &object)
                     .await
-                    .context(KubeClientReplaceSnafu)?;
+                    .context(KubeClientReplaceSnafu { gvk })?;
             } else {
                 // Create CRD if a previous version wasn't found
                 api.create(&PostParams::default(), &object)

From b4003986e817e0063e997f7dc080c82f7e9f0648 Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Tue, 27 May 2025 13:58:08 +0200
Subject: [PATCH 13/17] return error on no release found, exit main with error
 if error from run

---
 rust/stackablectl/src/cmds/release.rs | 19 +++++++++++++++----
 rust/stackablectl/src/main.rs         |  3 ++-
 2 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 635152ff..2d83ad26 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -140,6 +140,9 @@ pub enum CmdError {
     #[snafu(display("failed to build release list"))]
     BuildList { source: list::Error },
 
+    #[snafu(display("no release '{release}'"))]
+    NoSuchRelease { release: String },
+
     #[snafu(display("failed to install release"))]
     ReleaseInstall { source: release::Error },
 
@@ -301,7 +304,9 @@ async fn describe_cmd(
             OutputType::Json => serde_json::to_string(&release).context(SerializeJsonOutputSnafu),
             OutputType::Yaml => serde_yaml::to_string(&release).context(SerializeYamlOutputSnafu),
         },
-        None => Ok("No such release".into()),
+        None => Err(CmdError::NoSuchRelease {
+            release: args.release.clone(),
+        }),
     }
 }
 
@@ -352,7 +357,9 @@ async fn install_cmd(
 
             Ok(output.render())
         }
-        None => Ok("No such release".into()),
+        None => Err(CmdError::NoSuchRelease {
+            release: args.release.clone(),
+        }),
     }
 }
 
@@ -433,7 +440,9 @@ async fn upgrade_cmd(
 
             Ok(output.render())
         }
-        None => Ok("No such release".into()),
+        None => Err(CmdError::NoSuchRelease {
+            release: args.release.clone(),
+        }),
     }
 }
 
@@ -459,6 +468,8 @@ async fn uninstall_cmd(
 
             Ok(result.render())
         }
-        None => Ok("No such release".into()),
+        None => Err(CmdError::NoSuchRelease {
+            release: args.release.clone(),
+        }),
     }
 }
diff --git a/rust/stackablectl/src/main.rs b/rust/stackablectl/src/main.rs
index 9ad159d2..8d6efb9d 100644
--- a/rust/stackablectl/src/main.rs
+++ b/rust/stackablectl/src/main.rs
@@ -68,7 +68,8 @@ async fn main() -> Result<(), Error> {
             let mut output = app.error();
             output.with_error_report(err);
 
-            eprint!("{}", output.render())
+            eprint!("{}", output.render());
+            std::process::exit(1);
         }
     }
 

From 17a2764f479a5f91c4be6100e9f3ab227f164204 Mon Sep 17 00:00:00 2001
From: Nick Larsen <nick.larsen@stackable.tech>
Date: Tue, 27 May 2025 14:33:34 +0200
Subject: [PATCH 14/17] chore: use indicatif_(e)println

---
 rust/stackablectl/src/main.rs       | 6 +++---
 rust/stackablectl/src/output/mod.rs | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/rust/stackablectl/src/main.rs b/rust/stackablectl/src/main.rs
index 8d6efb9d..d6f96b24 100644
--- a/rust/stackablectl/src/main.rs
+++ b/rust/stackablectl/src/main.rs
@@ -6,7 +6,7 @@ use tracing::{Level, metadata::LevelFilter};
 use tracing_indicatif::{
     IndicatifLayer,
     filter::{IndicatifFilter, hide_indicatif_span_fields},
-    indicatif_eprintln,
+    indicatif_eprintln, indicatif_println,
 };
 use tracing_subscriber::{
     Layer as _,
@@ -63,12 +63,12 @@ async fn main() -> Result<(), Error> {
     }
 
     match app.run().await {
-        Ok(result) => print!("{result}"),
+        Ok(result) => indicatif_println!("{result}"),
         Err(err) => {
             let mut output = app.error();
             output.with_error_report(err);
 
-            eprint!("{}", output.render());
+            indicatif_eprintln!("{error}", error = output.render());
             std::process::exit(1);
         }
     }
diff --git a/rust/stackablectl/src/output/mod.rs b/rust/stackablectl/src/output/mod.rs
index eb1fe85f..9053ca87 100644
--- a/rust/stackablectl/src/output/mod.rs
+++ b/rust/stackablectl/src/output/mod.rs
@@ -65,7 +65,7 @@ where
         let mut index = 1;
 
         while let Some(source) = error.source() {
-            writeln!(report, " {}: {}", index, source)?;
+            writeln!(report, " {index}: {source}")?;
             error = source;
             index += 1;
         }

From eee8119f80a18797737f5054d966bfd375ff5c3a Mon Sep 17 00:00:00 2001
From: Nick Larsen <nick.larsen@stackable.tech>
Date: Tue, 27 May 2025 15:18:26 +0200
Subject: [PATCH 15/17] chore: Reuse existing error variant, and reformat it

---
 rust/stackable-cockpit/src/utils/k8s/client.rs | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/rust/stackable-cockpit/src/utils/k8s/client.rs b/rust/stackable-cockpit/src/utils/k8s/client.rs
index 7b3f220f..51fca425 100644
--- a/rust/stackable-cockpit/src/utils/k8s/client.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/client.rs
@@ -50,18 +50,16 @@ pub enum Error {
     #[snafu(display("failed to run GVK discovery"))]
     GVKDiscoveryRun { source: kube::error::Error },
 
-    #[snafu(display("GVK {gvk:?} is not known"))]
-    GVKUnknown { gvk: GroupVersionKind },
-
     #[snafu(display("failed to deploy manifest because type of object {object:?} is not set"))]
     ObjectType { object: DynamicObject },
 
-    #[snafu(display("failed to deploy manifest because GVK {group}/{kind}@{version} cannot be resolved",
+    // Using output close to Display for ObjectRef https://docs.rs/kube-runtime/0.99.0/src/kube_runtime/reflector/object_ref.rs.html#292-296
+    #[snafu(display("failed to resolve GVK: {kind}.{version}.{group}",
         group = gvk.group,
         version = gvk.version,
         kind = gvk.kind
     ))]
-    DiscoveryResolve { gvk: GroupVersionKind },
+    GVKResolve { gvk: GroupVersionKind },
 
     #[snafu(display("failed to convert byte string into UTF-8 string"))]
     ByteStringConvert { source: FromUtf8Error },
@@ -134,7 +132,7 @@ impl Client {
             let (resource, capabilities) = self
                 .resolve_gvk(&gvk)
                 .await?
-                .context(GVKUnknownSnafu { gvk })?;
+                .context(GVKResolveSnafu { gvk })?;
 
             let api: Api<DynamicObject> = match capabilities.scope {
                 Scope::Cluster => {
@@ -176,7 +174,7 @@ impl Client {
             let (resource, _) = self
                 .resolve_gvk(&gvk)
                 .await?
-                .context(GVKUnknownSnafu { gvk: gvk.clone() })?;
+                .with_context(|| GVKResolveSnafu { gvk: gvk.clone() })?;
 
             // CRDs are cluster scoped
             let api: Api<DynamicObject> = Api::all_with(self.client.clone(), &resource);
@@ -189,7 +187,7 @@ impl Client {
                 object.metadata.resource_version = resource.resource_version();
                 api.replace(&object.name_any(), &PostParams::default(), &object)
                     .await
-                    .context(KubeClientReplaceSnafu { gvk })?;
+                    .with_context(|_| KubeClientReplaceSnafu { gvk })?;
             } else {
                 // Create CRD if a previous version wasn't found
                 api.create(&PostParams::default(), &object)

From 96df19872c7ad6bdc0bfc380948c435951ae688a Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Tue, 27 May 2025 16:47:44 +0200
Subject: [PATCH 16/17] use Debug impl for String in Errors

---
 rust/stackable-cockpit/src/engine/minikube/mod.rs   |  2 +-
 rust/stackable-cockpit/src/platform/demo/spec.rs    |  4 ++--
 rust/stackable-cockpit/src/platform/service.rs      | 12 ++++++------
 rust/stackable-cockpit/src/platform/stacklet/mod.rs |  2 +-
 rust/stackable-cockpit/src/utils/k8s/client.rs      |  8 ++++----
 rust/stackable-cockpit/src/utils/params.rs          |  2 +-
 rust/stackablectl/src/cmds/demo.rs                  |  6 +++---
 rust/stackablectl/src/cmds/operator.rs              |  4 ++--
 rust/stackablectl/src/cmds/release.rs               |  4 ++--
 rust/stackablectl/src/cmds/stack.rs                 |  2 +-
 10 files changed, 23 insertions(+), 23 deletions(-)

diff --git a/rust/stackable-cockpit/src/engine/minikube/mod.rs b/rust/stackable-cockpit/src/engine/minikube/mod.rs
index dab6e959..308de131 100644
--- a/rust/stackable-cockpit/src/engine/minikube/mod.rs
+++ b/rust/stackable-cockpit/src/engine/minikube/mod.rs
@@ -10,7 +10,7 @@ use crate::{
 #[derive(Debug, Snafu)]
 pub enum Error {
     #[snafu(display(
-        "failed to determine if a Minikube cluster named '{cluster_name}' already exists"
+        "failed to determine if a Minikube cluster named {cluster_name:?} already exists"
     ))]
     CheckCluster {
         source: std::io::Error,
diff --git a/rust/stackable-cockpit/src/platform/demo/spec.rs b/rust/stackable-cockpit/src/platform/demo/spec.rs
index e2903516..2f031e89 100644
--- a/rust/stackable-cockpit/src/platform/demo/spec.rs
+++ b/rust/stackable-cockpit/src/platform/demo/spec.rs
@@ -29,13 +29,13 @@ pub type DemoParameter = Parameter;
 
 #[derive(Debug, Snafu)]
 pub enum Error {
-    #[snafu(display("no stack named '{name}'"))]
+    #[snafu(display("no stack named {name:?}"))]
     NoSuchStack { name: String },
 
     #[snafu(display("demo resource requests error"), context(false))]
     DemoResourceRequests { source: ResourceRequestsError },
 
-    #[snafu(display("cannot install demo in namespace '{requested}', only '{}' supported", supported.join(", ")))]
+    #[snafu(display("cannot install demo in namespace {requested:?}, only '{}' supported", supported.join(", ")))]
     UnsupportedNamespace {
         requested: String,
         supported: Vec<String>,
diff --git a/rust/stackable-cockpit/src/platform/service.rs b/rust/stackable-cockpit/src/platform/service.rs
index f82d50fe..73ff576c 100644
--- a/rust/stackable-cockpit/src/platform/service.rs
+++ b/rust/stackable-cockpit/src/platform/service.rs
@@ -20,22 +20,22 @@ pub enum Error {
     #[snafu(display("failed to fetch data from Kubernetes API"))]
     KubeClientFetch { source: k8s::Error },
 
-    #[snafu(display("missing namespace for service '{service}'"))]
+    #[snafu(display("missing namespace for service {service:?}"))]
     MissingServiceNamespace { service: String },
 
-    #[snafu(display("missing spec for service '{service}'"))]
+    #[snafu(display("missing spec for service {service:?}"))]
     MissingServiceSpec { service: String },
 
-    #[snafu(display("failed to get status of node '{node_name}'"))]
+    #[snafu(display("failed to get status of node {node_name:?}"))]
     GetNodeStatus { node_name: String },
 
-    #[snafu(display("failed to get address of node '{node_name}'"))]
+    #[snafu(display("failed to get address of node {node_name:?}"))]
     GetNodeAddress { node_name: String },
 
-    #[snafu(display("failed to find an ExternalIP or InternalIP for node '{node_name}'"))]
+    #[snafu(display("failed to find an ExternalIP or InternalIP for node {node_name:?}"))]
     NoIpForNode { node_name: String },
 
-    #[snafu(display("failed to find node '{node_name}' in node_name_ip_mapping"))]
+    #[snafu(display("failed to find node {node_name:?} in node_name_ip_mapping"))]
     NodeMissingInIpMapping { node_name: String },
 }
 
diff --git a/rust/stackable-cockpit/src/platform/stacklet/mod.rs b/rust/stackable-cockpit/src/platform/stacklet/mod.rs
index b2f915c0..1279ddc4 100644
--- a/rust/stackable-cockpit/src/platform/stacklet/mod.rs
+++ b/rust/stackable-cockpit/src/platform/stacklet/mod.rs
@@ -50,7 +50,7 @@ pub enum Error {
     #[snafu(display("failed to fetch data from the Kubernetes API"))]
     KubeClientFetch { source: k8s::Error },
 
-    #[snafu(display("no namespace set for custom resource '{crd_name}'"))]
+    #[snafu(display("no namespace set for custom resource {crd_name:?}"))]
     CustomCrdNamespace { crd_name: String },
 
     #[snafu(display("failed to deserialize cluster conditions from JSON"))]
diff --git a/rust/stackable-cockpit/src/utils/k8s/client.rs b/rust/stackable-cockpit/src/utils/k8s/client.rs
index 51fca425..e678b71b 100644
--- a/rust/stackable-cockpit/src/utils/k8s/client.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/client.rs
@@ -64,19 +64,19 @@ pub enum Error {
     #[snafu(display("failed to convert byte string into UTF-8 string"))]
     ByteStringConvert { source: FromUtf8Error },
 
-    #[snafu(display("missing namespace for service '{service}'"))]
+    #[snafu(display("missing namespace for service {service:?}"))]
     MissingServiceNamespace { service: String },
 
     #[snafu(display("failed to retrieve cluster information"))]
     ClusterInformation { source: cluster::Error },
 
-    #[snafu(display("invalid or empty secret data in '{secret_name}'"))]
+    #[snafu(display("invalid or empty secret data in {secret_name:?}"))]
     InvalidSecretData { secret_name: String },
 
-    #[snafu(display("no username key in credentials secret '{secret_name}'"))]
+    #[snafu(display("no username key in credentials secret {secret_name:?}"))]
     NoUsernameKey { secret_name: String },
 
-    #[snafu(display("no password key in credentials secret '{secret_name}'"))]
+    #[snafu(display("no password key in credentials secret {secret_name:?}"))]
     NoPasswordKey { secret_name: String },
 }
 
diff --git a/rust/stackable-cockpit/src/utils/params.rs b/rust/stackable-cockpit/src/utils/params.rs
index 623aeac4..6526b7be 100644
--- a/rust/stackable-cockpit/src/utils/params.rs
+++ b/rust/stackable-cockpit/src/utils/params.rs
@@ -34,7 +34,7 @@ pub enum IntoParametersError {
     #[snafu(display("failed to parse raw parameter"))]
     RawParse { source: RawParameterParseError },
 
-    #[snafu(display("invalid parameter '{parameter}', expected one of {expected}"))]
+    #[snafu(display("invalid parameter {parameter:?}, expected one of {expected:?}"))]
     InvalidParameter { parameter: String, expected: String },
 }
 
diff --git a/rust/stackablectl/src/cmds/demo.rs b/rust/stackablectl/src/cmds/demo.rs
index 9480e3dd..2b433035 100644
--- a/rust/stackablectl/src/cmds/demo.rs
+++ b/rust/stackablectl/src/cmds/demo.rs
@@ -125,13 +125,13 @@ pub enum CmdError {
     #[snafu(display("failed to serialize JSON output"))]
     SerializeJsonOutput { source: serde_json::Error },
 
-    #[snafu(display("no demo with name '{name}'"))]
+    #[snafu(display("no demo with name {name:?}"))]
     NoSuchDemo { name: String },
 
-    #[snafu(display("no stack with name '{name}'"))]
+    #[snafu(display("no stack with name {name:?}"))]
     NoSuchStack { name: String },
 
-    #[snafu(display("no release '{release}'"))]
+    #[snafu(display("no release {release:?}"))]
     NoSuchRelease { release: String },
 
     #[snafu(display("failed to get latest release"))]
diff --git a/rust/stackablectl/src/cmds/operator.rs b/rust/stackablectl/src/cmds/operator.rs
index 84c269cd..501203b9 100644
--- a/rust/stackablectl/src/cmds/operator.rs
+++ b/rust/stackablectl/src/cmds/operator.rs
@@ -141,7 +141,7 @@ pub enum CmdError {
         version: String,
     },
 
-    #[snafu(display("unknown repository name '{name}'"))]
+    #[snafu(display("unknown repository name {name:?}"))]
     UnknownRepoName { name: String },
 
     #[snafu(display("Helm error"))]
@@ -159,7 +159,7 @@ pub enum CmdError {
     #[snafu(display("failed to create Kubernetes client"))]
     KubeClientCreate { source: k8s::Error },
 
-    #[snafu(display("failed to create namespace '{namespace}'"))]
+    #[snafu(display("failed to create namespace {namespace:?}"))]
     NamespaceCreate {
         source: namespace::Error,
         namespace: String,
diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 2d83ad26..00d26ab0 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -140,7 +140,7 @@ pub enum CmdError {
     #[snafu(display("failed to build release list"))]
     BuildList { source: list::Error },
 
-    #[snafu(display("no release '{release}'"))]
+    #[snafu(display("no release {release:?}"))]
     NoSuchRelease { release: String },
 
     #[snafu(display("failed to install release"))]
@@ -158,7 +158,7 @@ pub enum CmdError {
     #[snafu(display("failed to create Kubernetes client"))]
     KubeClientCreate { source: k8s::Error },
 
-    #[snafu(display("failed to create namespace '{namespace}'"))]
+    #[snafu(display("failed to create namespace {namespace:?}"))]
     NamespaceCreate {
         source: namespace::Error,
         namespace: String,
diff --git a/rust/stackablectl/src/cmds/stack.rs b/rust/stackablectl/src/cmds/stack.rs
index 01816929..6860c04f 100644
--- a/rust/stackablectl/src/cmds/stack.rs
+++ b/rust/stackablectl/src/cmds/stack.rs
@@ -121,7 +121,7 @@ pub enum CmdError {
     #[snafu(display("failed to serialize JSON output"))]
     SerializeJsonOutput { source: serde_json::Error },
 
-    #[snafu(display("no release '{release}'"))]
+    #[snafu(display("no release {release:?}"))]
     NoSuchRelease { release: String },
 
     #[snafu(display("failed to get latest release"))]

From 1572f2febc5b860e269ec95320d30a7ccd60ad55 Mon Sep 17 00:00:00 2001
From: xeniape <xenia.fischer@stackable.tech>
Date: Tue, 27 May 2025 20:08:45 +0200
Subject: [PATCH 17/17] refactor unnamed interpolations

---
 rust/stackable-cockpit/src/helm.rs            | 48 +++++--------------
 .../src/platform/cluster/resource_request.rs  | 10 ++--
 .../src/platform/credentials.rs               |  7 ++-
 .../src/platform/demo/spec.rs                 |  2 +-
 .../src/platform/stack/spec.rs                |  2 +-
 .../src/platform/stacklet/mod.rs              |  5 +-
 .../src/utils/k8s/conditions.rs               | 18 +++++--
 .../stackable-cockpit/src/utils/k8s/labels.rs |  2 +-
 rust/stackable-cockpit/src/utils/mod.rs       |  2 +-
 rust/stackable-cockpit/src/utils/params.rs    |  2 +-
 rust/stackablectl/src/cmds/debug.rs           |  4 +-
 rust/stackablectl/src/cmds/demo.rs            | 27 +++++++----
 rust/stackablectl/src/cmds/operator.rs        | 17 ++++---
 rust/stackablectl/src/cmds/release.rs         | 20 ++++++--
 rust/stackablectl/src/cmds/stack.rs           | 27 +++++++----
 rust/stackablectl/src/cmds/stacklet.rs        | 16 ++++---
 rust/stackablectl/src/output/mod.rs           |  2 +-
 rust/stackablectl/src/output/result.rs        |  6 +--
 rust/xtask/src/docs.rs                        |  4 +-
 web/build.rs                                  |  4 +-
 20 files changed, 128 insertions(+), 97 deletions(-)

diff --git a/rust/stackable-cockpit/src/helm.rs b/rust/stackable-cockpit/src/helm.rs
index c43578d3..4cca0bba 100644
--- a/rust/stackable-cockpit/src/helm.rs
+++ b/rust/stackable-cockpit/src/helm.rs
@@ -122,8 +122,7 @@ impl Display for InstallReleaseStatus {
             } => {
                 write!(
                     f,
-                    "The release {} ({}) is already installed (requested {}), skipping.",
-                    release_name, current_version, requested_version
+                    "The release {release_name} ({current_version}) is already installed (requested {requested_version}), skipping."
                 )
             }
             InstallReleaseStatus::ReleaseAlreadyInstalledUnspecified {
@@ -132,16 +131,11 @@ impl Display for InstallReleaseStatus {
             } => {
                 write!(
                     f,
-                    "The release {} ({}) is already installed and no specific version was requested, skipping.",
-                    release_name, current_version
+                    "The release {release_name} ({current_version}) is already installed and no specific version was requested, skipping."
                 )
             }
             InstallReleaseStatus::Installed(release_name) => {
-                write!(
-                    f,
-                    "The release {} was successfully installed.",
-                    release_name
-                )
+                write!(f, "The release {release_name} was successfully installed.")
             }
         }
     }
@@ -157,17 +151,12 @@ impl Display for UninstallReleaseStatus {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             UninstallReleaseStatus::NotInstalled(release_name) => {
-                write!(
-                    f,
-                    "The release {} is not installed, skipping.",
-                    release_name
-                )
+                write!(f, "The release {release_name} is not installed, skipping.")
             }
             UninstallReleaseStatus::Uninstalled(release_name) => {
                 write!(
                     f,
-                    "The release {} was successfully uninstalled.",
-                    release_name
+                    "The release {release_name} was successfully uninstalled."
                 )
             }
         }
@@ -282,10 +271,7 @@ fn install_release(
     );
 
     if let Some(error) = helm_sys::to_helm_error(&result) {
-        error!(
-            "Go wrapper function go_install_helm_release encountered an error: {}",
-            error
-        );
+        error!("Go wrapper function go_install_helm_release encountered an error: {error}");
 
         return Err(Error::InstallRelease {
             source: InstallReleaseError::HelmWrapper { error },
@@ -312,10 +298,7 @@ pub fn uninstall_release(
         let result = helm_sys::uninstall_helm_release(release_name, namespace, suppress_output);
 
         if let Some(err) = helm_sys::to_helm_error(&result) {
-            error!(
-                "Go wrapper function go_uninstall_helm_release encountered an error: {}",
-                err
-            );
+            error!("Go wrapper function go_uninstall_helm_release encountered an error: {err}");
 
             return Err(Error::UninstallRelease { error: err });
         }
@@ -325,10 +308,7 @@ pub fn uninstall_release(
         ));
     }
 
-    info!(
-        "The Helm release {} is not installed, skipping.",
-        release_name
-    );
+    info!("The Helm release {release_name} is not installed, skipping.");
 
     Ok(UninstallReleaseStatus::NotInstalled(
         release_name.to_string(),
@@ -352,10 +332,7 @@ pub fn list_releases(namespace: &str) -> Result<Vec<Release>, Error> {
     let result = helm_sys::list_helm_releases(namespace);
 
     if let Some(err) = helm_sys::to_helm_error(&result) {
-        error!(
-            "Go wrapper function go_helm_list_releases encountered an error: {}",
-            err
-        );
+        error!("Go wrapper function go_helm_list_releases encountered an error: {err}");
 
         return Err(Error::ListReleases { error: err });
     }
@@ -381,10 +358,7 @@ pub fn add_repo(repository_name: &str, repository_url: &str) -> Result<(), Error
     let result = helm_sys::add_helm_repository(repository_name, repository_url);
 
     if let Some(err) = helm_sys::to_helm_error(&result) {
-        error!(
-            "Go wrapper function go_add_helm_repo encountered an error: {}",
-            err
-        );
+        error!("Go wrapper function go_add_helm_repo encountered an error: {err}");
 
         return Err(Error::AddRepo { error: err });
     }
@@ -403,7 +377,7 @@ where
     let url = Url::parse(repo_url.as_ref()).context(UrlParseSnafu)?;
     let url = url.join(HELM_REPO_INDEX_FILE).context(UrlParseSnafu)?;
 
-    debug!("Using {} to retrieve Helm index file", url);
+    debug!("Using {url} to retrieve Helm index file");
 
     // TODO (Techassi): Use the FileTransferClient for that
     let index_file_content = reqwest::get(url)
diff --git a/rust/stackable-cockpit/src/platform/cluster/resource_request.rs b/rust/stackable-cockpit/src/platform/cluster/resource_request.rs
index b254bd75..d3ada92c 100644
--- a/rust/stackable-cockpit/src/platform/cluster/resource_request.rs
+++ b/rust/stackable-cockpit/src/platform/cluster/resource_request.rs
@@ -32,8 +32,10 @@ impl Display for ResourceRequests {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(
             f,
-            "CPU: {}, Memory: {}, PVC space: {}",
-            self.cpu.0, self.memory.0, self.pvc.0
+            "CPU: {cpu}, Memory: {memory}, PVC space: {pvc}",
+            cpu = self.cpu.0,
+            memory = self.memory.0,
+            pvc = self.pvc.0
         )
     }
 }
@@ -67,8 +69,8 @@ pub enum ResourceRequestsError {
 #[derive(Debug, Snafu)]
 pub enum ResourceRequestsValidationError {
     #[snafu(display(
-        "The {object_name} requires {} CPU core(s), but there are only {} CPU core(s) available in the cluster",
-        required.as_cpu_count(), available.as_cpu_count()
+        "The {object_name} requires {required_cpu} CPU core(s), but there are only {available_cpu} CPU core(s) available in the cluster",
+        required_cpu = required.as_cpu_count(), available_cpu = available.as_cpu_count()
     ))]
     InsufficientCpu {
         available: CpuQuantity,
diff --git a/rust/stackable-cockpit/src/platform/credentials.rs b/rust/stackable-cockpit/src/platform/credentials.rs
index f03caaa9..5b96da02 100644
--- a/rust/stackable-cockpit/src/platform/credentials.rs
+++ b/rust/stackable-cockpit/src/platform/credentials.rs
@@ -25,7 +25,12 @@ pub struct Credentials {
 
 impl Display for Credentials {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}:{}", self.username, self.password)
+        write!(
+            f,
+            "{username}:{password}",
+            username = self.username,
+            password = self.password
+        )
     }
 }
 
diff --git a/rust/stackable-cockpit/src/platform/demo/spec.rs b/rust/stackable-cockpit/src/platform/demo/spec.rs
index 2f031e89..6c4c5589 100644
--- a/rust/stackable-cockpit/src/platform/demo/spec.rs
+++ b/rust/stackable-cockpit/src/platform/demo/spec.rs
@@ -35,7 +35,7 @@ pub enum Error {
     #[snafu(display("demo resource requests error"), context(false))]
     DemoResourceRequests { source: ResourceRequestsError },
 
-    #[snafu(display("cannot install demo in namespace {requested:?}, only '{}' supported", supported.join(", ")))]
+    #[snafu(display("cannot install demo in namespace {requested:?}, only {supported:?} supported", supported = supported.join(", ")))]
     UnsupportedNamespace {
         requested: String,
         supported: Vec<String>,
diff --git a/rust/stackable-cockpit/src/platform/stack/spec.rs b/rust/stackable-cockpit/src/platform/stack/spec.rs
index e95fb628..c5fd2939 100644
--- a/rust/stackable-cockpit/src/platform/stack/spec.rs
+++ b/rust/stackable-cockpit/src/platform/stack/spec.rs
@@ -49,7 +49,7 @@ pub enum Error {
 
     /// This error indicates that the stack doesn't support being installed in
     /// the provided namespace.
-    #[snafu(display("unable install stack in namespace {requested:?}, only '{}' supported", supported.join(", ")))]
+    #[snafu(display("unable install stack in namespace {requested:?}, only {supported:?} supported", supported = supported.join(", ")))]
     UnsupportedNamespace {
         requested: String,
         supported: Vec<String>,
diff --git a/rust/stackable-cockpit/src/platform/stacklet/mod.rs b/rust/stackable-cockpit/src/platform/stacklet/mod.rs
index 1279ddc4..7c4cd449 100644
--- a/rust/stackable-cockpit/src/platform/stacklet/mod.rs
+++ b/rust/stackable-cockpit/src/platform/stacklet/mod.rs
@@ -197,6 +197,9 @@ fn gvk_from_product_name(product_name: &str) -> GroupVersionKind {
     GroupVersionKind {
         group: format!("{product_name}.stackable.tech"),
         version: "v1alpha1".into(),
-        kind: format!("{}Cluster", product_name.capitalize()),
+        kind: format!(
+            "{product_name}Cluster",
+            product_name = product_name.capitalize()
+        ),
     }
 }
diff --git a/rust/stackable-cockpit/src/utils/k8s/conditions.rs b/rust/stackable-cockpit/src/utils/k8s/conditions.rs
index 06abc174..82f52118 100644
--- a/rust/stackable-cockpit/src/utils/k8s/conditions.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/conditions.rs
@@ -43,7 +43,11 @@ impl ConditionsExt for Vec<Condition> {
         self.iter()
             .map(|c| {
                 DisplayCondition::new(
-                    format!("{}: {}", c.type_, c.status),
+                    format!(
+                        "{condition_type}: {status}",
+                        condition_type = c.type_,
+                        status = c.status
+                    ),
                     Some(c.message.clone()),
                     c.is_good(),
                 )
@@ -57,7 +61,11 @@ impl ConditionsExt for Vec<DeploymentCondition> {
         self.iter()
             .map(|c| {
                 DisplayCondition::new(
-                    format!("{}: {}", c.type_, c.status),
+                    format!(
+                        "{condition_type}: {status}",
+                        condition_type = c.type_,
+                        status = c.status
+                    ),
                     c.message.clone(),
                     c.is_good(),
                 )
@@ -79,7 +87,11 @@ impl ConditionsExt for Vec<StatefulSetCondition> {
         self.iter()
             .map(|c| {
                 DisplayCondition::new(
-                    format!("{}: {}", c.type_, c.status),
+                    format!(
+                        "{condition_type}: {status}",
+                        condition_type = c.type_,
+                        status = c.status
+                    ),
                     c.message.clone(),
                     c.is_good(),
                 )
diff --git a/rust/stackable-cockpit/src/utils/k8s/labels.rs b/rust/stackable-cockpit/src/utils/k8s/labels.rs
index 7f9d5b17..d79c127b 100644
--- a/rust/stackable-cockpit/src/utils/k8s/labels.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/labels.rs
@@ -37,7 +37,7 @@ pub trait ListParamsExt {
 impl ListParamsExt for ListParams {
     fn add_label(&mut self, label: impl Into<String>) {
         match self.label_selector.as_mut() {
-            Some(labels) => labels.push_str(format!(",{}", label.into()).as_str()),
+            Some(labels) => labels.push_str(format!(",{label}", label = label.into()).as_str()),
             None => self.label_selector = Some(label.into()),
         }
     }
diff --git a/rust/stackable-cockpit/src/utils/mod.rs b/rust/stackable-cockpit/src/utils/mod.rs
index a42bcc0d..ee2018a4 100644
--- a/rust/stackable-cockpit/src/utils/mod.rs
+++ b/rust/stackable-cockpit/src/utils/mod.rs
@@ -8,5 +8,5 @@ pub mod templating;
 
 /// Returns the name of the operator used in the Helm repository.
 pub fn operator_chart_name(name: &str) -> String {
-    format!("{}-operator", name)
+    format!("{name}-operator")
 }
diff --git a/rust/stackable-cockpit/src/utils/params.rs b/rust/stackable-cockpit/src/utils/params.rs
index 6526b7be..d688009d 100644
--- a/rust/stackable-cockpit/src/utils/params.rs
+++ b/rust/stackable-cockpit/src/utils/params.rs
@@ -106,7 +106,7 @@ pub enum RawParameterParseError {
 
 impl Display for RawParameter {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}={}", self.name, self.value)
+        write!(f, "{name}={value}", name = self.name, value = self.value)
     }
 }
 
diff --git a/rust/stackablectl/src/cmds/debug.rs b/rust/stackablectl/src/cmds/debug.rs
index cf0feaa1..83ad165c 100644
--- a/rust/stackablectl/src/cmds/debug.rs
+++ b/rust/stackablectl/src/cmds/debug.rs
@@ -340,8 +340,8 @@ impl Drop for AsyncStdin {
         let status = unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_SETFL, self.old_flags) };
         if status == -1 {
             panic!(
-                "unable to revert stdin flags: {}",
-                std::io::Error::last_os_error()
+                "unable to revert stdin flags: {error}",
+                error = std::io::Error::last_os_error()
             );
         }
     }
diff --git a/rust/stackablectl/src/cmds/demo.rs b/rust/stackablectl/src/cmds/demo.rs
index 2b433035..bffc38a0 100644
--- a/rust/stackablectl/src/cmds/demo.rs
+++ b/rust/stackablectl/src/cmds/demo.rs
@@ -302,7 +302,10 @@ async fn describe_cmd(
 
             result
                 .with_command_hint(
-                    format!("stackablectl demo install {}", args.demo_name),
+                    format!(
+                        "stackablectl demo install {demo_name}",
+                        demo_name = args.demo_name
+                    ),
                     "install the demo",
                 )
                 .with_command_hint("stackablectl demo list", "list all available demos")
@@ -398,11 +401,11 @@ async fn install_cmd(
     })?;
 
     let operator_cmd = format!(
-        "stackablectl operator installed{}",
-        if args.namespaces.operator_namespace != DEFAULT_OPERATOR_NAMESPACE {
+        "stackablectl operator installed{option}",
+        option = if args.namespaces.operator_namespace != DEFAULT_OPERATOR_NAMESPACE {
             format!(
-                " --operator-namespace {}",
-                args.namespaces.operator_namespace
+                " --operator-namespace {namespace}",
+                namespace = args.namespaces.operator_namespace
             )
         } else {
             "".into()
@@ -410,9 +413,12 @@ async fn install_cmd(
     );
 
     let stacklet_cmd = format!(
-        "stackablectl stacklet list{}",
-        if args.namespaces.namespace != DEFAULT_NAMESPACE {
-            format!(" --namespace {}", args.namespaces.namespace)
+        "stackablectl stacklet list{option}",
+        option = if args.namespaces.namespace != DEFAULT_NAMESPACE {
+            format!(
+                " --namespace {namespace}",
+                namespace = args.namespaces.namespace
+            )
         } else {
             "".into()
         }
@@ -421,7 +427,10 @@ async fn install_cmd(
     output
         .with_command_hint(operator_cmd, "display the installed operators")
         .with_command_hint(stacklet_cmd, "display the installed stacklets")
-        .with_output(format!("Installed demo '{}'", args.demo_name));
+        .with_output(format!(
+            "Installed demo {demo_name:?}",
+            demo_name = args.demo_name
+        ));
 
     Ok(output.render())
 }
diff --git a/rust/stackablectl/src/cmds/operator.rs b/rust/stackablectl/src/cmds/operator.rs
index 501203b9..cb2e903c 100644
--- a/rust/stackablectl/src/cmds/operator.rs
+++ b/rust/stackablectl/src/cmds/operator.rs
@@ -292,7 +292,10 @@ async fn describe_cmd(args: &OperatorDescribeArgs, cli: &Cli) -> Result<String,
 
             result
                 .with_command_hint(
-                    format!("stackablectl operator install {}", args.operator_name),
+                    format!(
+                        "stackablectl operator install {operator_name}",
+                        operator_name = args.operator_name
+                    ),
                     "install the operator",
                 )
                 .with_command_hint("stackablectl operator list", "list all available operators")
@@ -343,9 +346,9 @@ async fn install_cmd(args: &OperatorInstallArgs, cli: &Cli) -> Result<String, Cm
             "list installed operators",
         )
         .with_output(format!(
-            "Installed {} {}",
-            args.operators.len(),
-            if args.operators.len() == 1 {
+            "Installed {num_of_operators} {suffix}",
+            num_of_operators = args.operators.len(),
+            suffix = if args.operators.len() == 1 {
                 "operator"
             } else {
                 "operators"
@@ -374,9 +377,9 @@ fn uninstall_cmd(args: &OperatorUninstallArgs, cli: &Cli) -> Result<String, CmdE
             "list remaining installed operators",
         )
         .with_output(format!(
-            "Uninstalled {} {}",
-            args.operators.len(),
-            if args.operators.len() == 1 {
+            "Uninstalled {num_of_operators} {suffix}",
+            num_of_operators = args.operators.len(),
+            suffix = if args.operators.len() == 1 {
                 "operator"
             } else {
                 "operators"
diff --git a/rust/stackablectl/src/cmds/release.rs b/rust/stackablectl/src/cmds/release.rs
index 00d26ab0..cad3b538 100644
--- a/rust/stackablectl/src/cmds/release.rs
+++ b/rust/stackablectl/src/cmds/release.rs
@@ -293,7 +293,10 @@ async fn describe_cmd(
 
                 result
                     .with_command_hint(
-                        format!("stackablectl release install {}", args.release),
+                        format!(
+                            "stackablectl release install {release}",
+                            release = args.release
+                        ),
                         "install the release",
                     )
                     .with_command_hint("stackablectl release list", "list all available releases")
@@ -353,7 +356,10 @@ async fn install_cmd(
                     "stackablectl operator installed",
                     "list installed operators",
                 )
-                .with_output(format!("Installed release '{}'", args.release));
+                .with_output(format!(
+                    "Installed release {release:?}",
+                    release = args.release
+                ));
 
             Ok(output.render())
         }
@@ -436,7 +442,10 @@ async fn upgrade_cmd(
                     "stackablectl operator installed",
                     "list installed operators",
                 )
-                .with_output(format!("Upgraded to release '{}'", args.release));
+                .with_output(format!(
+                    "Upgraded to release {release:?}",
+                    release = args.release
+                ));
 
             Ok(output.render())
         }
@@ -464,7 +473,10 @@ async fn uninstall_cmd(
 
             result
                 .with_command_hint("stackablectl release list", "list available releases")
-                .with_output(format!("Uninstalled release '{}'", args.release));
+                .with_output(format!(
+                    "Uninstalled release {release:?}",
+                    release = args.release
+                ));
 
             Ok(result.render())
         }
diff --git a/rust/stackablectl/src/cmds/stack.rs b/rust/stackablectl/src/cmds/stack.rs
index 6860c04f..a027166d 100644
--- a/rust/stackablectl/src/cmds/stack.rs
+++ b/rust/stackablectl/src/cmds/stack.rs
@@ -291,7 +291,10 @@ fn describe_cmd(
 
                 result
                     .with_command_hint(
-                        format!("stackablectl stack install {}", args.stack_name),
+                        format!(
+                            "stackablectl stack install {stack_name}",
+                            stack_name = args.stack_name
+                        ),
                         "install the stack",
                     )
                     .with_command_hint("stackablectl stack list", "list all available stacks")
@@ -361,11 +364,11 @@ async fn install_cmd(
                 })?;
 
             let operator_cmd = format!(
-                "stackablectl operator installed{}",
-                if args.namespaces.operator_namespace != DEFAULT_OPERATOR_NAMESPACE {
+                "stackablectl operator installed{option}",
+                option = if args.namespaces.operator_namespace != DEFAULT_OPERATOR_NAMESPACE {
                     format!(
-                        " --operator-namespace {}",
-                        args.namespaces.operator_namespace
+                        " --operator-namespace {namespace}",
+                        namespace = args.namespaces.operator_namespace
                     )
                 } else {
                     "".into()
@@ -373,9 +376,12 @@ async fn install_cmd(
             );
 
             let stacklet_cmd = format!(
-                "stackablectl stacklet list{}",
-                if args.namespaces.namespace != DEFAULT_NAMESPACE {
-                    format!(" --namespace {}", args.namespaces.namespace)
+                "stackablectl stacklet list{option}",
+                option = if args.namespaces.namespace != DEFAULT_NAMESPACE {
+                    format!(
+                        " --namespace {namespace}",
+                        namespace = args.namespaces.namespace
+                    )
                 } else {
                     "".into()
                 }
@@ -384,7 +390,10 @@ async fn install_cmd(
             output
                 .with_command_hint(operator_cmd, "display the installed operators")
                 .with_command_hint(stacklet_cmd, "display the installed stacklets")
-                .with_output(format!("Installed stack '{}'", args.stack_name));
+                .with_output(format!(
+                    "Installed stack {stack_name:?}",
+                    stack_name = args.stack_name
+                ));
 
             Ok(output.render())
         }
diff --git a/rust/stackablectl/src/cmds/stacklet.rs b/rust/stackablectl/src/cmds/stacklet.rs
index 332d45a0..62c8aff2 100644
--- a/rust/stackablectl/src/cmds/stacklet.rs
+++ b/rust/stackablectl/src/cmds/stacklet.rs
@@ -191,7 +191,7 @@ async fn list_cmd(args: &StackletListArgs, cli: &Cli) -> Result<String, CmdError
                 .with_output(format!(
                     "{table}{errors}",
                     errors = if !error_list.is_empty() {
-                        format!("\n\n{}", error_list.join("\n"))
+                        format!("\n\n{error_list}", error_list = error_list.join("\n"))
                     } else {
                         "".into()
                     }
@@ -230,11 +230,13 @@ async fn credentials_cmd(args: &StackletCredentialsArgs) -> Result<String, CmdEr
                 .add_row(vec!["PASSWORD", &credentials.password]);
 
             let output = format!(
-                "Credentials for {} ({}) in namespace '{}':",
-                args.product_name, args.stacklet_name, args.namespace
+                "Credentials for {product_name} ({stacklet_name}) in namespace {namespace:?}:",
+                product_name = args.product_name,
+                stacklet_name = args.stacklet_name,
+                namespace = args.namespace
             );
 
-            Ok(format!("{}\n\n{}", output, table))
+            Ok(format!("{output}\n\n{table}"))
         }
         None => Ok("No credentials".into()),
     }
@@ -280,7 +282,7 @@ fn render_condition_error(
 ) -> Option<String> {
     if !is_good.unwrap_or(true) {
         let message = message.unwrap_or("-".into());
-        return Some(format!("[{}]: {}", error_index, message));
+        return Some(format!("[{error_index}]: {message}"));
     }
 
     None
@@ -294,7 +296,7 @@ fn color_condition(condition: &str, is_good: Option<bool>, error_index: usize) -
             if is_good {
                 condition.to_owned()
             } else {
-                format!("{}: See [{}]", condition, error_index)
+                format!("{condition}: See [{error_index}]")
             }
         }
         None => condition.to_owned(),
@@ -308,6 +310,6 @@ fn render_errors(errors: Vec<String>) -> Option<String> {
     } else if errors.len() == 1 {
         Some(errors[0].clone())
     } else {
-        Some(format!("{}\n---\n", errors.join("\n")))
+        Some(format!("{errors}\n---\n", errors = errors.join("\n")))
     }
 }
diff --git a/rust/stackablectl/src/output/mod.rs b/rust/stackablectl/src/output/mod.rs
index 9053ca87..20ef91ef 100644
--- a/rust/stackablectl/src/output/mod.rs
+++ b/rust/stackablectl/src/output/mod.rs
@@ -55,7 +55,7 @@ where
         let mut report = String::new();
 
         // Print top most error
-        write!(report, "An unrecoverable error occured: {}\n\n", self)?;
+        write!(report, "An unrecoverable error occured: {self}\n\n")?;
         writeln!(
             report,
             "Caused by these errors (recent errors listed first):"
diff --git a/rust/stackablectl/src/output/result.rs b/rust/stackablectl/src/output/result.rs
index 28409bc9..1f36d397 100644
--- a/rust/stackablectl/src/output/result.rs
+++ b/rust/stackablectl/src/output/result.rs
@@ -55,9 +55,9 @@ impl ResultContext {
         description: impl Into<String>,
     ) -> &mut Self {
         self.command_hints.push(format!(
-            "Use \"{}\" to {}.",
-            command.into(),
-            description.into()
+            "Use \"{command}\" to {description}.",
+            command = command.into(),
+            description = description.into()
         ));
 
         self
diff --git a/rust/xtask/src/docs.rs b/rust/xtask/src/docs.rs
index 6f7f4b8b..d36feb65 100644
--- a/rust/xtask/src/docs.rs
+++ b/rust/xtask/src/docs.rs
@@ -43,8 +43,8 @@ pub fn generate() -> Result<(), GenDocsError> {
             .unwrap()
             .join(DOCS_BASE_PATH)
             .join(format!(
-                "{}.adoc",
-                if cmd.get_name() == cli.get_name() {
+                "{name}.adoc",
+                name = if cmd.get_name() == cli.get_name() {
                     "index"
                 } else {
                     cmd.get_name()
diff --git a/web/build.rs b/web/build.rs
index 3e8de5e0..40145831 100644
--- a/web/build.rs
+++ b/web/build.rs
@@ -47,8 +47,8 @@ fn main() {
     }
     write!(
         File::create(out_dir.join("vite-asset-map.rs")).unwrap(),
-        "{}",
-        asset_map.build()
+        "{asset_map}",
+        asset_map = asset_map.build()
     )
     .unwrap();
 }