Fix container hash-prefix + status-aware redeploy

Bug 1: Container name hash-prefix (e.g. f93cb8b504f7_jenkins)
- _strip_hash_prefix(): strips 12-char hex prefix and leading slash
- _resolve_container_name(): looks up actual DSM name from container list
- Applied in list_containers (display), container_stats (matching),
  get_container_status/get_container_logs/exec_in_container (lookup)

Bug 2: redeploy_project DSM 2101/1202 on wrong project state
- Fetch project status before acting
- RUNNING     → stop then start
- STOPPED     → start directly (nothing to stop)
- BUILD_FAILED → suppress stop error, then start
- Other       → return error with workaround hint

36 tests all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 21:43:02 +02:00
parent 6fa35e1b48
commit c8cda5ef2b
5 changed files with 419 additions and 51 deletions
+40 -34
View File
@@ -2,11 +2,13 @@
from __future__ import annotations
import contextlib
import logging
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP
from mcp_synology_container.config import AppConfig
from mcp_synology_container.dsm_client import DsmClient
@@ -33,7 +35,7 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
return "No projects found."
lines = ["Projects:", ""]
for project_id, proj in sorted(projects.items(), key=lambda x: x[1].get("name", "")):
for _project_id, proj in sorted(projects.items(), key=lambda x: x[1].get("name", "")):
name = proj.get("name", "?")
status = proj.get("status", "?")
path = proj.get("path", "?")
@@ -115,7 +117,12 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
@mcp.tool()
async def redeploy_project(project_name: str, confirmed: bool = False) -> str:
"""Redeploy a project: pull latest images, stop, and restart.
"""Redeploy a project by stopping and restarting it.
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
This operation will briefly take the project offline.
Requires confirmation before executing.
@@ -126,10 +133,8 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
"""
if not confirmed:
return (
f"Redeploying project '{project_name}' will:\n"
f" 1. Pull latest images\n"
f" 2. Stop all containers\n"
f" 3. Restart with new images\n\n"
f"Redeploying project '{project_name}' will stop and restart all its "
f"containers (auto-detects current state).\n\n"
f"Call this tool again with confirmed=True to proceed."
)
@@ -138,43 +143,44 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
return f"Project '{project_name}' not found."
project_id = project.get("id", "")
status = (project.get("status") or "").upper()
results = []
try:
# Step 1: Pull latest images via build (triggers compose pull)
results.append("Step 1/3: Pulling latest images...")
try:
await client.request(
"SYNO.Docker.Project",
"build",
params={"id": project_id, "force": "true"},
if status == "STOPPED":
results.append("Project is STOPPED — starting directly.")
results.append("Step 1/1: Starting project...")
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...")
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("Step 2/2: Starting project...")
await client.request("SYNO.Docker.Project", "start", params={"id": project_id})
results.append(" Project started.")
else:
return (
f"Cannot redeploy '{project_name}': unexpected status '{status}'.\n"
f"Workaround: use stop_project + start_project separately."
)
results.append(" Images pulled.")
except Exception as e:
results.append(f" Warning: pull step failed ({e}), continuing with restart.")
# Step 2: Stop the project
results.append("Step 2/3: Stopping project...")
await client.request(
"SYNO.Docker.Project",
"stop",
params={"id": project_id},
)
results.append(" Project stopped.")
# Step 3: Start the project
results.append("Step 3/3: Starting project...")
await client.request(
"SYNO.Docker.Project",
"start",
params={"id": project_id},
)
results.append(" Project started.")
results.append(f"\nProject '{project_name}' redeployed successfully.")
except Exception as e:
results.append(f"Error during redeploy: {e}")
results.append("Workaround: use stop_project + start_project separately.")
return "\n".join(results)