Fix Jenkins-update flow: redeploy_project pull + delete_image safety + delete_container
Bug fixes from product test: 1. redeploy_project: BUILD_FAILED now includes explicit image pull (stop → pull → start) 2. delete_image: Distinguishes running vs stopped containers, suggests system_prune for stopped refs 3. New tool delete_container: Verify stopped state before deletion, confirmation required Tests added for all three paths plus stopped-container edge cases. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -312,6 +312,58 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N
|
||||
|
||||
return "\n".join(result_lines)
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_container(container_name: str, confirmed: bool = False) -> str:
|
||||
"""Delete a container.
|
||||
|
||||
Container must be stopped before deletion. Without confirmed=True,
|
||||
returns a preview only.
|
||||
|
||||
Args:
|
||||
container_name: Name of the container.
|
||||
confirmed: Must be True to proceed. Set to True to confirm deletion.
|
||||
"""
|
||||
if not confirmed:
|
||||
return (
|
||||
f"Preview: would delete container '{container_name}'.\n"
|
||||
f"Call this tool again with confirmed=True to proceed."
|
||||
)
|
||||
|
||||
resolved_name = await _resolve_container_name(client, container_name)
|
||||
display_name = _strip_hash_prefix(resolved_name)
|
||||
|
||||
try:
|
||||
data = await client.request(
|
||||
"SYNO.Docker.Container",
|
||||
"get",
|
||||
params={"name": resolved_name},
|
||||
)
|
||||
except Exception as e:
|
||||
return f"Error inspecting container '{container_name}': {e}"
|
||||
|
||||
if not data:
|
||||
return f"Container '{container_name}' not found."
|
||||
|
||||
details = data.get("details", {}) or {}
|
||||
state = details.get("State", {}) or {}
|
||||
running = state.get("Running", False)
|
||||
|
||||
if running:
|
||||
return (
|
||||
f"Cannot delete '{display_name}': container is still running.\n"
|
||||
f"Stop the container first with stop_project or stop_container."
|
||||
)
|
||||
|
||||
try:
|
||||
await client.request(
|
||||
"SYNO.Docker.Container",
|
||||
"delete",
|
||||
params={"name": resolved_name},
|
||||
)
|
||||
return f"Deleted container '{display_name}'."
|
||||
except Exception as e:
|
||||
return f"Error deleting '{display_name}': {e}"
|
||||
|
||||
|
||||
def _container_in_project(container: dict[str, Any], project_name: str) -> bool:
|
||||
"""Check if a container belongs to a project based on its labels."""
|
||||
@@ -350,9 +402,7 @@ def _format_container_detail(name: str, data: dict[str, Any]) -> str:
|
||||
lines.append(f" Exit code: {state.get('ExitCode', '?')}")
|
||||
|
||||
# IP addresses from all attached networks
|
||||
networks: dict[str, Any] = (
|
||||
details.get("NetworkSettings", {}) or {}
|
||||
).get("Networks", {}) or {}
|
||||
networks: dict[str, Any] = (details.get("NetworkSettings", {}) or {}).get("Networks", {}) or {}
|
||||
for net_name, net in networks.items():
|
||||
ip = (net or {}).get("IPAddress", "")
|
||||
if ip:
|
||||
|
||||
@@ -217,7 +217,8 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
|
||||
img_hash = target.get("id", "")
|
||||
|
||||
# Check if image is in use by any container
|
||||
in_use_by: list[str] = []
|
||||
in_use_running: list[str] = []
|
||||
in_use_stopped: list[str] = []
|
||||
try:
|
||||
ctr_data = await client.request(
|
||||
"SYNO.Docker.Container",
|
||||
@@ -231,12 +232,25 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
|
||||
ctr_img_id == img_hash or (hash_prefix and ctr_img_id.startswith(hash_prefix))
|
||||
):
|
||||
ctr_name = ctr.get("name") or ctr.get("Names", ["?"])[0]
|
||||
in_use_by.append(ctr_name)
|
||||
status = ctr.get("status", ctr.get("state", "")).lower()
|
||||
if status == "running":
|
||||
in_use_running.append(ctr_name)
|
||||
else:
|
||||
in_use_stopped.append(ctr_name)
|
||||
except Exception as e:
|
||||
logger.debug("Could not fetch containers for in-use check: %s", e)
|
||||
|
||||
if in_use_by:
|
||||
return f"Cannot delete '{display_name}': image is used by " + ", ".join(in_use_by)
|
||||
if in_use_running:
|
||||
return (
|
||||
f"Cannot delete '{display_name}': image is used by running container(s): "
|
||||
+ ", ".join(in_use_running)
|
||||
)
|
||||
|
||||
if in_use_stopped:
|
||||
return (
|
||||
f"Cannot delete '{display_name}': image is used by stopped container '{in_use_stopped[0]}'.\n"
|
||||
f"Delete the container first or run system_prune to clean up stopped containers."
|
||||
)
|
||||
|
||||
if not confirmed:
|
||||
return (
|
||||
|
||||
@@ -122,7 +122,7 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
|
||||
Checks the current project status to determine the correct action:
|
||||
- RUNNING → stop, then start
|
||||
- STOPPED → start directly (nothing to stop)
|
||||
- BUILD_FAILED → force-stop, then start
|
||||
- BUILD_FAILED → stop, pull images, then start
|
||||
|
||||
This operation will briefly take the project offline.
|
||||
Requires confirmation before executing.
|
||||
@@ -153,19 +153,23 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
|
||||
await client.request("SYNO.Docker.Project", "start", params={"id": project_id})
|
||||
results.append(" Project started.")
|
||||
|
||||
elif status in ("RUNNING", "BUILD_FAILED", ""):
|
||||
if status == "RUNNING":
|
||||
results.append("Step 1/2: Stopping project...")
|
||||
elif status == "BUILD_FAILED":
|
||||
results.append("Step 1/3: Stopping failed build...")
|
||||
with contextlib.suppress(Exception):
|
||||
await client.request("SYNO.Docker.Project", "stop", params={"id": project_id})
|
||||
results.append(" Project stopped.")
|
||||
elif status == "BUILD_FAILED":
|
||||
results.append("Step 1/2: Stopping failed build...")
|
||||
with contextlib.suppress(Exception):
|
||||
await client.request(
|
||||
"SYNO.Docker.Project", "stop", params={"id": project_id}
|
||||
)
|
||||
results.append(" Build stopped.")
|
||||
results.append(" Build stopped.")
|
||||
results.append("Step 2/3: Pulling updated images...")
|
||||
with contextlib.suppress(Exception):
|
||||
await client.request("SYNO.Docker.Image", "pull", params={"id": project_id})
|
||||
results.append(" Images pulled.")
|
||||
results.append("Step 3/3: Starting project...")
|
||||
await client.request("SYNO.Docker.Project", "start", params={"id": project_id})
|
||||
results.append(" Project started.")
|
||||
|
||||
elif status in ("RUNNING", ""):
|
||||
results.append("Step 1/2: Stopping project...")
|
||||
await client.request("SYNO.Docker.Project", "stop", params={"id": project_id})
|
||||
results.append(" Project stopped.")
|
||||
results.append("Step 2/2: Starting project...")
|
||||
await client.request("SYNO.Docker.Project", "start", params={"id": project_id})
|
||||
results.append(" Project started.")
|
||||
|
||||
Reference in New Issue
Block a user