Skip to content

feat: Add uninstall demo/stack feature#429

Open
xeniape wants to merge 29 commits intomainfrom
feat/add-demo-stack-deletion
Open

feat: Add uninstall demo/stack feature#429
xeniape wants to merge 29 commits intomainfrom
feat/add-demo-stack-deletion

Conversation

@xeniape
Copy link
Copy Markdown
Member

@xeniape xeniape commented Mar 12, 2026

Description

Part of #187

This PR adds the uninstall subcommands to demo and stack commands. It implements the MVP features mentioned here #187 (comment)

The flag of skipping operators and CRD deletion was optional and I wasn't sure about the naming or if needs to be one command or two. Can also be just removed, added it because it was not much additional work.

Tested with all current demos/stacks. Some demos have edge cases in the deletion, will document that in the parent issue.

This PR also contains the DEMO/STACK parameter addition for stackabletech/demos#374 (comment) stripped out into #432

Definition of Done Checklist

  • Not all of these items are applicable to all PRs, the author should update this template to only leave the boxes in that are relevant
  • Please make sure all these things are done and tick the boxes

Author

  • Changes are OpenShift compatible
  • CRD changes approved
  • Helm chart can be installed and deployed operator works
  • Integration tests passed (for non trivial changes)

Reviewer

  • Code contains useful comments
  • (Integration-)Test cases added
  • Documentation added or updated
  • Changelog updated
  • Cargo.toml only contains references to git tags (not specific commits or branches)

Acceptance

  • Feature Tracker has been updated
  • Proper release label has been added

@xeniape xeniape self-assigned this Mar 12, 2026
@xeniape xeniape moved this to Development: Waiting for Review in Stackable Engineering Mar 12, 2026
@Techassi Techassi self-requested a review March 23, 2026 15:32
@NickLarsenNZ NickLarsenNZ moved this from Development: Waiting for Review to Development: In Review in Stackable Engineering Mar 25, 2026

[NOTE]
====
The uninstall command deletes the namespace the demo was installed in. Therefore it is not possible to uninstall demos in the `default` namespace.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be opt-in behaviour. Or rather, if we created the namespace, it gets deleted.

There could be existing namespaces that shouldn't be deleted.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One such example is if we have the monitoring stack installed, and want to add and remove demos.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to #187 (comment) "Update after a discussion on 2025-11-26:" it was decided the first version provides no option to be less destructive. Not sure which meeting that was or if it makes sense to change the requirements now.

Copy link
Copy Markdown
Member Author

@xeniape xeniape Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were also problems with deleting everything without deleting the namespace, see stackabletech/hdfs-operator#761 (comment). So maybe for the first version / MVP it is fine for now like this? But feel free to disagree

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left an idea below in how we could do it: #429 (comment)

But of course, if this implements what we agreed on, that's fine.
It does feel a little dangerous though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree it feels dangerous, tried to avoid it at first but ran into the problems above, which unfortunately won't be solved by your idea I think. The manifests we would delete with that approach were also deleted with the help of the labels, so would be the same situation, unless I misunderstood something 😞

Another thing might be adding a confirmation dialogue like the one I added to the installation for the namespace. That it asks if the user really wants to uninstall since that would delete the namespace, and cancel the deletion otherwise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0f3cf7f (this PR) Maybe this addresses your concern a bit, let me know what you think :)


[NOTE]
====
The uninstall command deletes the namespace the stack was installed in. Therefore it is not possible to uninstall stacks in the `default` namespace.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as my comment about demos.

Copy link
Copy Markdown
Member

@NickLarsenNZ NickLarsenNZ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a couple of comments about namespace deletion.

Comment on lines +240 to +252
// Delete demo namespace
client
.delete_object(
&uninstall_parameters.demo_namespace,
&ApiResource::from_gvk(&GroupVersionKind {
group: "".to_owned(),
version: "v1".to_owned(),
kind: "Namespace".to_owned(),
}),
None,
)
.await
.context(DeleteObjectSnafu)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where I think we should have a check to see if the namespace was "owned" by the demo (via labels/annotations).

Copy link
Copy Markdown
Member

@NickLarsenNZ NickLarsenNZ Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we could consider is reading in the demo, get the GVKs and Helm charts, and uninstall in reverse order before uninstalling the namespace (if it was created by the demo).

Same would apply for stacks.

That could be a follow-up PR, but I think at least we should be careful with namespace deletion.

Comment on lines +238 to +250
// Delete stack namespace
client
.delete_object(
&uninstall_parameters.stack_namespace,
&ApiResource::from_gvk(&GroupVersionKind {
group: "".to_owned(),
version: "v1".to_owned(),
kind: "Namespace".to_owned(),
}),
None,
)
.await
.context(DeleteObjectSnafu)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

Copy link
Copy Markdown
Member

@Techassi Techassi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments and questions.

Comment on lines +240 to +252
// Delete demo namespace
client
.delete_object(
&uninstall_parameters.demo_namespace,
&ApiResource::from_gvk(&GroupVersionKind {
group: "".to_owned(),
version: "v1".to_owned(),
kind: "Namespace".to_owned(),
}),
None,
)
.await
.context(DeleteObjectSnafu)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I think we should abstract that away in an associated function called delete_namespace similar to what we have to create a namespace. See:

pub async fn create_namespace(&self, name: String) -> Result<()> {
let namespace_api: Api<Namespace> = Api::all(self.client.clone());
namespace_api
.create(
&PostParams::default(),
&Namespace {
metadata: ObjectMeta {
name: Some(name),
..Default::default()
},
..Default::default()
},
)
.await
.context(KubeClientPatchSnafu)?;
Ok(())
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, thanks! e71437a (this PR)

Copy link
Copy Markdown
Member

@Techassi Techassi Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason why we are not using Api<Namespace> instead? I guess one upside to to doing it like currently implemented is that we don't need to clone the client for a single function call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client is cloned anyway in the delete_object function called by delete_namespace. I think I didn't use Api<Namespace> was because I wanted to reuse the delete_object function (before everything was moved into delete_namespace). Could possibly also rewrite that one to use Api<DynamicObject> instead of ApiResource. I don't remember exactly anymore, but I think the resulting code just looked better using ApiResource and that's what the discovery already provided. But no strong opinions here.

While writing, tried implementing it since then the Api creation could be moved from delete_object to delete_all_object_with_label, so one client clone per object list, instead of object function call. This doc for DynamicObject:

/// A dynamic representation of a kubernetes object
///
/// This will work with any non-list type object.

made me think, that if a function accepts Api<DynamicObject>, I could pass it a Api<Namespace> (or any other Object type). But seems to be not compatible. So I think I'm abandoning that implementation again.

Could also not use delete_object instead, but then I would just re-implement what's in that function anyway.

Just noting down thoughts here while trying it out, not trying to argue against it. Here the non-working diff because of Api<DynamicObject> <-> Api<Namespace> incompatibility.

diff --git a/rust/stackable-cockpit/src/utils/k8s/client.rs b/rust/stackable-cockpit/src/utils/k8s/client.rs
index 79f5338..779ec7f 100644
--- a/rust/stackable-cockpit/src/utils/k8s/client.rs
+++ b/rust/stackable-cockpit/src/utils/k8s/client.rs
@@ -358,13 +358,20 @@ impl Client {
                 continue;
             };
 
+            let object_api = match namespace {
+                Some(namespace) => {
+                    Api::<DynamicObject>::namespaced_with(self.client.clone(), namespace, &api_resource)
+                }
+                None => Api::<DynamicObject>::all_with(self.client.clone(), &api_resource),
+            };
+
             for object in object_list {
                 if let Some(value) = object.labels().get(&label.key().to_string()) {
                     if value.eq(&label.value().to_string()) {
                         self.delete_object(
                             &object.metadata.name.unwrap(),
-                            &api_resource,
-                            object.metadata.namespace.as_deref(),
+                            &api_resource.kind,
+                            object_api.clone(),
                         )
                         .await?;
                     }
@@ -379,17 +386,10 @@ impl Client {
     pub async fn delete_object(
         &self,
         object_name: &str,
-        api_resource: &ApiResource,
-        namespace: Option<&str>,
+        object_kind: &str,
+        object_api: Api<DynamicObject>,
     ) -> Result<(), Error> {
-        Span::current().pb_set_message(&format!("Deleting {} {}", api_resource.kind, object_name));
-
-        let object_api = match namespace {
-            Some(namespace) => {
-                Api::<DynamicObject>::namespaced_with(self.client.clone(), namespace, api_resource)
-            }
-            None => Api::<DynamicObject>::all_with(self.client.clone(), api_resource),
-        };
+        Span::current().pb_set_message(&format!("Deleting {} {}", object_kind, object_name));
 
         kube::runtime::wait::delete::delete_and_finalize(
             object_api,
@@ -574,14 +574,11 @@ impl Client {
 
     /// Deletes a [`Namespace`] with `name` in the cluster.
     pub async fn delete_namespace(&self, name: String) -> Result<()> {
+        let namespace_api: Api<Namespace> = Api::all(self.client.clone());
         self.delete_object(
             &name,
-            &ApiResource::from_gvk(&GroupVersionKind {
-                group: "".to_owned(),
-                version: "v1".to_owned(),
-                kind: "Namespace".to_owned(),
-            }),
-            None,
+            "Namespace",
+            namespace_api,
         )
         .await
     }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client is cloned anyway in the delete_object function called by delete_namespace.

Ah I see. Then I think we should use the same style as in create_namespace via Api.

Comment on lines +416 to +438
let demo_namespace = tracing_indicatif::suspend_tracing_indicatif(
|| -> Result<String, CmdError> {
if args.namespaces.namespace == DEFAULT_NAMESPACE {
if Confirm::new()
.with_prompt(
format!(
"Demos installed in the '{DEFAULT_NAMESPACE}' namespace cannot be uninstalled with stackablectl. Install the demo in the '{demo_namespace}' namespace instead?",
demo_namespace = args.demo_name.clone())
)
.default(true)
.interact()
.context(ConfirmDialogSnafu)? {
// User selected to install in suggested namespace
Ok(args.demo_name.clone())
} else {
// User selected to install in default namespace
Ok(args.namespaces.namespace.clone())
}
} else {
Ok(args.namespaces.namespace.clone())
}
},
)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Well that's a mouthful to read. Can we somehow make this easier to grasp?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I looked at the code too long at this point and unable to see it, but could you give me more information on what is making it hard to grasp. At least I currently don't know how to make it shorter. Or are there explanations missing somewhere? Or just the whole "formatting" of it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0f3cf7f (this PR) Tried rearranging it a bit, let me know if know if that makes it any better

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel like it does make it better, sorry 🙈

What might help on the other hand is to define the closure outside of the tracing_indicatif::suspend_tracing_indicatif call.

If that also doesn't help, we can leave it as is.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

@Techassi Techassi Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already like this way better. I think we can now also switch to the previous style of assigning demo_namespace which should make it even cleaner.

Copy link
Copy Markdown
Member

@Techassi Techassi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a few small comments left, mostly looks nice!

};

for object in object_list {
if let Some(value) = object.labels().get(&label.key().to_string()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: This is kinda cumbersome to use, but could be improved by stackabletech/operator-rs#1182 (if we decide to move forward with it).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked PR was merged btw, but not yet released.

@xeniape xeniape requested a review from Techassi March 30, 2026 13:31
xeniape and others added 2 commits March 30, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development: In Review

Development

Successfully merging this pull request may close these issues.

3 participants