Delete program cascades to its deployments (no more dead-end gate)
A program and its 1:1 tool/static deployment are one thing to the user, so
'Delete program' now just works instead of refusing. DELETE /config/programs/
{name}?cascade=true tears down each referencing deployment (manager-aware:
uninstall a tool from PATH, stop+disable a service/job, drop a static route),
removes them and the program, then converges the runtime (prune orphan units,
regenerate the Caddyfile, reload the gateway). Without cascade it still 409s
(safe API default). The dashboard drops the 'can't remove while deployed' block,
labels the action 'Delete program', and the confirm names exactly what will go.
Verified live: throwaway tool → cascade delete removes program + path deployment,
real config untouched; plain delete of a referenced program still 409s.
castle-api 57 passed.
This commit is contained in:
@@ -73,9 +73,17 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane
|
||||
}
|
||||
|
||||
const handleDelete = async (compName: string) => {
|
||||
// Deleting a program cascades: its deployments are torn down and removed too
|
||||
// (a program and its 1:1 tool/static deployment are one thing). Deleting a
|
||||
// service/job deployment is a plain removal (keeps the program).
|
||||
const url =
|
||||
configSection === "programs"
|
||||
? `/config/programs/${compName}?cascade=true`
|
||||
: `/config/${writeSection}/${compName}`
|
||||
try {
|
||||
await apiClient.delete(`/config/${writeSection}/${compName}`)
|
||||
await apiClient.delete(url)
|
||||
qc.invalidateQueries({ queryKey: [configSection] })
|
||||
qc.invalidateQueries({ queryKey: ["programs"] })
|
||||
navigate("/")
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
|
||||
@@ -161,14 +161,25 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
|
||||
saved={saved}
|
||||
onSave={handleSave}
|
||||
onDelete={onDelete ? () => onDelete(program.id) : undefined}
|
||||
deleteLabel="Remove program"
|
||||
confirmMessage={`Remove program "${program.id}" from castle.yaml? (Source on disk is untouched.)`}
|
||||
deleteBlocked={
|
||||
program.services.length + program.jobs.length > 0
|
||||
? "Programs with active jobs or services cannot be removed — delete those first."
|
||||
: undefined
|
||||
}
|
||||
deleteLabel="Delete program"
|
||||
confirmMessage={deleteConfirm(program)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Deleting a program cascades to its deployments (tear down + remove), so the
|
||||
* confirm names exactly what will go. Source on disk is always left untouched. */
|
||||
function deleteConfirm(program: ProgramDetail): string {
|
||||
const deps = [...program.services, ...program.jobs]
|
||||
const kind = program.kind
|
||||
const own =
|
||||
kind === "tool"
|
||||
? " This uninstalls it from PATH."
|
||||
: kind === "static"
|
||||
? " This stops serving it."
|
||||
: ""
|
||||
const also =
|
||||
deps.length > 0 ? ` Its deployments (${deps.join(", ")}) are torn down and removed too.` : ""
|
||||
return `Delete "${program.id}"?${own}${also} (Source on disk is untouched.)`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user