Skip to content

feat: Add uninstall demo/stack feature#429

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

feat: Add uninstall demo/stack feature#429
xeniape wants to merge 23 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.

Comment on lines -88 to -91
parameters.insert("STACK".to_owned(), stack_name.into());
if let Some(demo_name) = demo_name {
parameters.insert("DEMO".to_owned(), demo_name.into());
}
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.

question: Just that we are on the same track: These are now added earlier and are part of the passed parameters, right?

question: Out of curiosity, what was the reason you did that? I assume the demo and stack name are needed earlier in the call chain now?

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.

I didn't add them in install_manifests because it seemed like this function is "outside of the scope of demos and stacks" and might be also used for something else. Along with the comment above that templating shouldn't be a concern here in the future. Since @sbernauer pulled that parameter addition from this PR while I was gone, I don't know what his thoughts were, we didn't coordinate. But I'm fine with also moving it back into install_manifests.

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

@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()) {
if value.eq(&label.value().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: We could potentially use deref here to save an allocation.

Suggested change
if value.eq(&label.value().to_string()) {
if value.eq(label.value().deref()) {

};

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).

},
)?;
}
ManifestSpec::PlainYaml(_) => {}
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.

Ah yeah, didn't even remember this ADR. I think the current approach is fine, but slightly harder to wrap your head around, because we delete different things in different places.

If we would handle deletions of manifests here, the "source of deletion" is more focused (at least that is my current impression). We could then finalize the deletion by deleting the namespace.

If we decide to keep it as is, a comment should definitely be added here.

Comment on lines +335 to +339
capability.supports_operation(discovery::verbs::LIST)
&& match namespace {
Some(_) => capability.scope == kube::discovery::Scope::Namespaced,
None => capability.scope == kube::discovery::Scope::Cluster,
}
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.

What do you think about renaming scope_matching to capability_scope_matches?

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