Keep projects in Terminating until their resources drain - #752
Conversation
…ces drain Project purge force-finalized namespaces and treated "no namespaces left" as cleanup complete. Neither is safe for consumers: force-finalizing removes a namespace ahead of its contents, and the completion check ignored the default namespace entirely, so a project could finish deleting while objects in it still carried finalizers that would now never run. Namespaces are now deleted and left to the namespace controller wired for each project control plane, and a project is only complete when its namespaces are gone and the namespaces that cannot be deleted hold nothing. While resources remain, the ResourceCleanup condition names them and the finalizers holding them, and two metrics report how long the project has been waiting and how many resources are holding it. The project control plane is now torn down after cleanup completes rather than at the start of deletion, so consumers finalize against a control plane that still answers. Refs #750 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTE43NjiyoTnSn2bAzDS2o
Project creation requires organization parent information in the request, so the test now creates its projects through the organization control plane like the other project tests, and cleans up anything a failed run left behind so it can be re-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTE43NjiyoTnSn2bAzDS2o
Both tests created and deleted the cluster-scoped User "user-admin" and run concurrently, so one could delete it while the other was waiting for it to be ready. project-creation now uses its own user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTE43NjiyoTnSn2bAzDS2o
mattdjenkinson
left a comment
There was a problem hiding this comment.
Can we check if/how much this affects the deleting in the portal before a release is tagged?
|
@mattdjenkinson this does mean it'll take time to delete projects now. The time taken will depend on how many resources are in the projects. Agree we should see what the UX in the portal is and make sure it handles situations where it may take time to delete the project. |
|
@mattdjenkinson I loaded a project with a 1000 DNS resources and issued a deletion. As expected it took ~1 minute for the deletion to complete. The portal didn't give me any indication the project was actively deleting which was a little confusing. The portal should be checking if the deletion timestamp is set and show a little spinning indictor that it's actively being deleted. Or maybe we hide those projects by default with the option of seeing projects being deleted? The API response does include this which we can use to show additional information to users if needed (probably unnecessary) - lastTransitionTime: "2026-08-07T16:58:58Z"
message: 'Waiting for project resources to be removed: configmaps "default/billing-export-job"
(finalizers: billing.example.com/drain-pending)'
observedGeneration: 2
reason: CleanupAwaitingCompletion
status: "True"
type: ResourceCleanup |
|
@mattdjenkinson created datum-cloud/cloud-portal#1419 and datum-cloud/staff-portal#618 as follow ups. |
|
Thanks for this Scot. Will get something wired up asap. |
Closes #750.
Impact
Deleting a project could strand resources that operators created for it. The project finished deleting while objects inside it still held finalizers, so those finalizers never ran.
For network-services-operator, gateway configuration outlived the project. It kept propagating to the edge and serving traffic, and NSO held a finalizer it could never release, failing reconcile forever.
A project now stays in
Terminatinguntil its control plane is empty, and reports what blocks it. Consumers finish their work against a control plane that still answers. A project with nothing stuck still deletes in seconds.What changed
Namespaces are no longer force-finalized. Kubernetes guarantees a namespace outlives its contents, and consumers resolve state through it. Namespaces are deleted and left to the namespace controller each project control plane already runs.
The completion check covers
default. It previously accepted "no namespaces left". The API server refuses to deletedefault, so resources held there stayed invisible and the project completed on top of them. Completion now requiresdefaultto be empty.The
ResourceCleanupcondition names the blocker:Two per-project metrics back it and clear on completion:
milo_resourcemanager_project_deletion_pending_secondsandmilo_resourcemanager_project_deletion_blocking_resources.The project control plane is deleted after cleanup rather than before, so it no longer disappears while a consumer is still finalizing against it.
Deletion stays asynchronous, as #553 made it. A stuck project polls on its own and holds up nobody else.
There is no escape hatch. A stuck project means a consumer bug, which should be visible and fixed rather than overridden. If operators need one later, make it an explicit gated admin action.
Testing
New e2e
project-deletion-blocked-resourcescovers both places project resources live: held indefault, and held in a project namespace. Neither project may be removed while its resource is held, and both must delete once the finalizers are released.Against unmodified Milo it fails as expected. The
defaultcase loses its project 45 seconds after deletion while the held ConfigMap remains.Verified end to end against network-services-operator on a two-cluster environment. The gateway finalizer runs while the namespace exists, every downstream object is removed, and the project deletes cleanly.
While getting the new test green, project-creation turned out to share the cluster-scoped User
user-adminwith project-deletion. The two run concurrently, so one could delete it while the other waited for it to be ready — project-creation now uses its own user.Unit tests cover the completion check.
Note on #538
#538 fixes the field the force-finalize path clears. This PR removes that path. Force-finalizing is inert today because the guard returns early on a successful get, so repairing it would arm the behaviour #750 describes.