Skip to content

Commit de9be03

Browse files
github-actions[bot]anneschuth
authored andcommitted
feat: add admin orphan-report and orphan-confirm commands
Implements two new upstream endpoints: - GET /api/v2/admin/orphans/report → `zad admin orphan-report` - POST /api/v2/admin/orphans/confirm → `zad admin orphan-confirm` The report command is read-only and shows the classified orphan sweep. The confirm command marks selected items for grace-period deletion via repeatable --item TYPE:NAME (or TYPE:NAME:REALM for keycloak_client).
1 parent 0fa2289 commit de9be03

3 files changed

Lines changed: 89 additions & 2 deletions

File tree

src/zad_cli/api/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,16 @@ def delete_admin_mark(self, mark_id: str) -> dict:
413413
"""Remove a specific deletion mark without purging the resource."""
414414
return self._async_request("DELETE", f"/v2/admin/marked-for-deletion/{mark_id}")
415415

416+
def get_orphan_report(self) -> dict:
417+
"""Run the orphan sweep and return the classification report."""
418+
response = self._request("GET", "/v2/admin/orphans/report")
419+
return response.json()
420+
421+
def confirm_orphans(self, payload: dict) -> dict:
422+
"""Mark confirmed orphan candidates for grace-period deletion."""
423+
response = self._request("POST", "/v2/admin/orphans/confirm", json=payload)
424+
return response.json()
425+
416426
# --- Metrics ---
417427

418428
def health(self) -> dict:

src/zad_cli/commands/admin.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
"""Admin commands: list, delete."""
1+
"""Admin commands: list, delete, orphan-report, orphan-confirm."""
22

33
from __future__ import annotations
44

5+
from typing import Annotated
6+
57
import typer
68

79
from zad_cli.helpers import confirm_action, get_helpers, handle_api_errors, render_dry_run
@@ -60,3 +62,74 @@ def delete(
6062
result = client.delete_admin_mark(mark_id)
6163
formatter.render(result)
6264
formatter.render_success(f"Deletion mark '{mark_id}' removed.")
65+
66+
67+
@app.command("orphan-report")
68+
@handle_api_errors
69+
def orphan_report(ctx: typer.Context) -> None:
70+
"""Show the orphan sweep report (read-only).
71+
72+
Inventories PostgreSQL databases, Keycloak realms/clients and MinIO
73+
buckets, classified against live project files. Performs zero mutations.
74+
To mark orphans for deletion, use [bold]zad admin orphan-confirm[/bold].
75+
76+
[bold]Example:[/bold]
77+
78+
$ zad admin orphan-report
79+
"""
80+
client, formatter = get_helpers(ctx)
81+
result = client.get_orphan_report()
82+
formatter.render(result)
83+
84+
85+
@app.command("orphan-confirm")
86+
@handle_api_errors
87+
def orphan_confirm(
88+
ctx: typer.Context,
89+
items: Annotated[
90+
list[str] | None,
91+
typer.Option("--item", help="Item to confirm as TYPE:NAME or TYPE:NAME:REALM, repeatable"),
92+
] = None,
93+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
94+
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be sent without making the API call"),
95+
) -> None:
96+
"""Mark confirmed orphan candidates for grace-period deletion.
97+
98+
Each item is specified as TYPE:NAME (or TYPE:NAME:REALM for keycloak_client).
99+
Valid types: postgresql_database, postgresql_user, minio_bucket, keycloak_client.
100+
101+
Run [bold]zad admin orphan-report[/bold] first to see candidates.
102+
103+
[bold]Example:[/bold]
104+
105+
$ zad admin orphan-confirm --item postgresql_database:regel_k4c_pr104
106+
$ zad admin orphan-confirm --item minio_bucket:old-bucket --item postgresql_user:stale_user
107+
"""
108+
client, formatter = get_helpers(ctx)
109+
110+
if not items:
111+
formatter.render_error("At least one --item is required.")
112+
raise typer.Exit(1)
113+
114+
parsed: list[dict] = []
115+
for raw in items:
116+
parts = raw.split(":", 2)
117+
if len(parts) < 2:
118+
formatter.render_error(f"Invalid item format '{raw}'. Expected TYPE:NAME or TYPE:NAME:REALM.")
119+
raise typer.Exit(1)
120+
entry: dict = {"type": parts[0], "name": parts[1]}
121+
if len(parts) == 3:
122+
entry["realm"] = parts[2]
123+
parsed.append(entry)
124+
125+
payload = {"items": parsed}
126+
127+
if dry_run:
128+
render_dry_run(formatter, "POST", "/v2/admin/orphans/confirm", payload)
129+
return
130+
131+
confirm_action(f"Mark {len(parsed)} orphan(s) for grace-period deletion?", yes)
132+
133+
result = client.confirm_orphans(payload)
134+
formatter.render(result)
135+
formatter.render_success(f"Confirmed {len(parsed)} orphan(s) for deletion.")

tests/test_backwards_compat.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def run_help(*args: str) -> subprocess.CompletedProcess:
9999
"metrics": ["health", "overview", "cpu", "memory", "pods", "network", "query"],
100100
"config": ["init", "set", "get", "list", "path"],
101101
"open": ["project", "portal", "domains"],
102-
"admin": ["list", "delete"],
102+
"admin": ["list", "delete", "orphan-report", "orphan-confirm"],
103103
}
104104

105105

@@ -126,7 +126,9 @@ def test_cli_commands_not_removed():
126126
"add_component_to_deployment",
127127
"add_service",
128128
"backup_bucket",
129+
"confirm_orphans",
129130
"delete_admin_mark",
131+
"get_orphan_report",
130132
"backup_database",
131133
"backup_namespace",
132134
"backup_project",
@@ -198,7 +200,9 @@ def test_client_public_methods_not_removed():
198200
# Changing these would break callers that pass arguments positionally.
199201
EXPECTED_METHOD_MIN_ARGS: dict[str, int] = {
200202
"add_component": 2,
203+
"confirm_orphans": 1,
201204
"delete_admin_mark": 1,
205+
"get_orphan_report": 0,
202206
"list_admin_marked": 0,
203207
"list_pvc_snapshots": 3,
204208
"restore_deployment_resource": 3,

0 commit comments

Comments
 (0)