Commit Graph

18 Commits

Author SHA1 Message Date
marcus 24b97338ba fix: v0.4.1 — remove pause_container / unpause_container (DSM unsupported)
Live test on this DSM firmware: SYNO.Docker.Container has no pause/
unpause method ("Method does not exist"). The Container Manager GUI
action menu only exposes Start / Stop / Force-Stop / Restart / Reset —
pause/resume simply isn't a feature here.

The two tools were briefly shipped in 0.4.0 (implemented by symmetry
with the verified stop call) and have now been removed rather than
left as a broken surface. The remaining lifecycle tools
(start_container, stop_container, restart_container) are unaffected.

Tool count: 33 → 31. Closes #7 (won't fix — DSM limitation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:52:52 +02:00
marcus 12d532da7b feat: v0.4.0 — welle A (8 new tools: container lifecycle, inspect_image, system_overview)
Closes #1, #4, #6, #7.

Container lifecycle (#1, #7):
- start_container, stop_container, restart_container, pause_container,
  unpause_container — all via SYNO.Docker.Container with JSON-encoded
  name parameter, routed through _resolve_container_name for hash-
  prefix resolution. stop is live-verified; the other four are
  implemented by symmetry on the same API surface.

inspect_image (#4):
- Returns full image detail (layers, env, ports, entrypoint/cmd,
  labels) via SYNO.Docker.Image/get. Accepts name:tag, registry-
  prefixed names, and bare hashes. Defensive response parsing
  handles both wrapped (details.*) and flat envelopes.

system_overview (#6):
- Aggregates CPU %, RAM, network and block I/O across all running
  containers plus running/stopped counts. No new DSM endpoint —
  composed from list + stats, reusing the container_stats CPU
  formula. Per-source errors are non-fatal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:40:11 +02:00
marcus 8adcf93b6a fix: v0.3.3 — delete_container params (error 114) + delete_project orphan guard
Bug 1 — delete_container (DSM error 114):
SYNO.Docker.Container/delete requires three parameters: name
(JSON-encoded), force=false, and preserve_profile=false. Previously
only a bare `name` string was sent, causing DSM to reject the call
with error 114. Added the two missing fields and JSON-encode name to
match the DSM convention. The connector-side running-container guard
is unchanged; force stays hard-coded to false.

Bug 2 — delete_project orphan containers:
Production test revealed that DSM does NOT reject Project/delete on a
running project — it silently removes the registration and leaves the
containers running without any project context. The previous
implementation tried to handle this via a caught SynologyError that
never actually fires. Fix: check the project status from _find_project
connector-side before issuing any DSM call; if RUNNING, return an
error pointing at stop_project. The delete request is never sent for
a running project.

The corresponding unit test (test_delete_project_running_returns_stop_hint)
was a false positive — it mocked a DSM rejection that real DSM never
produces. Replaced with test_delete_project_running_blocked_connector_side
which asserts that client.request("delete") is never called when the
project is RUNNING.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:38:36 +02:00
marcus 3f73ed0aef feat: v0.3.2 — delete_project tool
Closes the project lifecycle (create → start/stop/redeploy → delete).
The tool calls SYNO.Docker.Project/delete with the UUID JSON-encoded
as the `id` parameter (per DSM convention) and removes only the
Container Manager registration — the project folder and compose
file remain on the NAS. This mirrors DSM's own "Delete project"
behaviour, not a bug; the success message states the folder was
preserved so the user is not surprised.

Safety:
- Project-name validation runs before any I/O.
- A `_find_project` pre-flight returns "not found" with a clear
  message rather than letting DSM reject an unknown UUID.
- No automatic stop. If the project is RUNNING and DSM rejects
  the delete, the response tells the user to `stop_project` first
  rather than silently halting containers under the guise of a
  "delete" call.
- Requires confirmed=True; preview shows name, UUID, status, full
  path, and share path so the user can verify before deleting.

Tests cover preview-only, not-found, invalid-name, happy path
(verifies the UUID is JSON-encoded in the delete call), and the
running-project rejection path that surfaces the stop_project hint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:29:09 +02:00
marcus 801dbe15dc feat: v0.3.1 — create_project tool
Adds `create_project` for registering a new Container Manager project
from a compose YAML string. Three-step flow that mirrors the DSM
"Create Project" wizard:

  1. SYNO.FileStation.CreateFolder with force_parent=true (idempotent
     — does not fail if the folder already exists, and creates missing
     intermediate directories). Without this step, Docker.Project/create
     fails with DSM error 2100.
  2. SYNO.Docker.Project/create (form-encoded POST; JSON-encoded string
     parameters per DSM convention) returns the new project UUID.
  3. trigger_build_stream + _wait_for_project_running, reusing the
     existing image-pull / start / poll machinery (including the
     BUILD_FAILED early-exit from welle 2).

Safety:
- Project-name validation (Welle-1 regex) runs before any I/O.
- Compose content is YAML-parsed and must contain a top-level
  `services` key before any side effects.
- A pre-flight list_projects check rejects duplicate names with a
  clear message rather than leaving an orphaned folder on the NAS.
- share_path defaults to compose_base_path + project_name (e.g.
  /volume1/docker + myapp → /docker/myapp); a caller-supplied value
  overrides it.
- Requires confirmed=True; the preview shows the resolved share path
  and the service count parsed from the compose content.
- DSM error 2100 surfaces as "target folder issue" with the attempted
  path. A build_stream failure after a successful Project/create tells
  the user the project is registered-but-not-started and points at
  redeploy_project for recovery.

Tests cover preview-only, already-exists, happy path (with parameter
JSON-encoding assertions), explicit share_path, malformed YAML,
missing services key, invalid project name, error 2100, and
build_stream failure after registration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:13:18 +02:00
marcus 13e10fa52f feat: v0.3.0 — review welle 2 (M-4, M-5, M-6)
Three resilience and honesty fixes from the v0.2.8 review. Minor
version bump because redeploy_project and system_prune return
different strings.

M-4: trigger_build_stream now converts every non-ReadTimeout
httpx.HTTPError (ConnectError, ConnectTimeout, WriteError,
RemoteProtocolError, ...) into a SynologyError with a clear
message. Previously only ReadTimeout was handled; everything else
propagated as a raw httpx exception. redeploy_project now tracks
whether stop was actually issued and, when build_stream fails after
a successful stop, tells the user the project is in STOPPED state
and recommends start_project / retry rather than the misleading
"use stop + start separately" workaround.

M-5: _wait_for_project_running exits early on BUILD_FAILED / ERROR
(new _TERMINAL_FAILURE_STATUSES frozenset). DSM signals these
statuses within seconds of a failed image pull; the old polling
loop kept waiting up to 5 minutes for RUNNING. redeploy_project
now surfaces the terminal status with a BUILD_FAILED-specific hint
to update_image_tag.

M-6: system_prune preview now enumerates user-created networks
that have no containers attached (excluding the three built-in
networks bridge/host/none, which Docker never prunes). Previously
the preview noted "Unused networks: (not counted)" even though
SYNO.Docker.Utils/prune does delete them — users could lose
networks they had not been warned about.

Tests:
- 2 new dsm_client tests: ConnectError and RemoteProtocolError
  both raise SynologyError, not raw httpx exceptions.
- 2 new project tests: recovery hint after stop+build_stream
  failure (RUNNING case); old workaround retained for the
  STOPPED case where no stop was issued.
- 3 new polling tests: BUILD_FAILED and ERROR each trigger early
  exit; redeploy_project surfaces BUILD_FAILED with update_image_tag
  hint.
- 2 new system_prune preview tests: counts unused networks
  correctly, excludes built-ins; network-fetch failure is non-fatal.

245 tests pass. ruff check + ruff format clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:57:20 +02:00
marcus 661460bfd9 fix: v0.2.9 — review welle 1 (C-1, C-2, M-3)
C-1: __version__ now derived from package metadata via
importlib.metadata.version() so pyproject.toml is the single source of
truth. Previously stuck at "0.1.0" since the initial release.

C-2: Backfill CHANGELOG entries for 0.2.7 and 0.2.8 (both releases had
shipped without changelog updates) and add a 0.2.9 entry covering this
welle.

M-3: Reject project names containing path separators or other unsafe
characters before they reach _find_compose_path. Previously a name like
"../../etc" could traverse out of compose_base_path when the project was
not yet registered with Container Manager. Adds _validate_project_name
(regex ^[a-zA-Z0-9_-]+$, applied in read_compose, update_compose,
update_image_tag, update_env_var) plus parametrized tests for valid and
unsafe names and one rejection test per tool. 236 tests pass.

Also: ruff format autofix on three pre-existing files (cli.py,
config.py, test_config.py) — cosmetic only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:07:00 +02:00
marcus 4caac3a6c7 fix: v0.2.8 — init cooldown to prevent repeated failed-login hammering
Cache failed `_ensure_initialized` outcomes for 60 seconds so that
repeated tool calls during a credential outage (wrong password,
IP-blocked 407, DNS failure) don't keep hammering DSM. Each caller
gets the same exception raised from the cache until the cooldown
window expires, after which a fresh attempt is made.

- Adds INIT_ERROR_COOLDOWN module constant (60.0 s).
- Adds self._init_error / self._init_error_until state on DsmClient.
- Re-raises cached error inside the init lock, emits warning log on
  cache entry.

Addresses M4 from the 0.2.7 review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:06:35 +02:00
marcus ad199674e7 fix: v0.2.7 — remove -> str annotations and trim docstrings to reduce tools/list payload
FastMCP generates outputSchema for every tool with a return annotation,
roughly doubling the tools/list payload size. Multi-line docstrings with
Args/Returns sections add further bulk that Claude Desktop must parse.

- Strip -> str from all 23 @mcp.tool() functions
- Trim every tool docstring to a single descriptive line (≤100 chars)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 09:04:17 +02:00
marcus 7b1d7be5d7 fix: v0.2.6 — trigger_build_stream truly fire-and-forget
Claude Desktop times out tool calls after ~4 minutes. The previous
implementation read the first SSE chunk before returning, which could
block for the entire image-pull duration.

Now: send the GET request, wait for HTTP response headers (status
check only), close the connection immediately — never read SSE events.
DSM starts the build on receipt of the request and continues
server-side. ReadTimeout on headers is caught and ignored (request
already sent). Removes the _json import added in 0.2.5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 08:44:25 +02:00
marcus ebe3baba78 feat: v0.2.5 — redeploy via build_stream (proper DSM image pull)
Replace the delete-before-start workaround with the real mechanism:
SYNO.Docker.Project/build_stream is what the DSM "Erstellen" button calls
(confirmed via DevTools). It pulls updated images and starts the project.

DsmClient.trigger_build_stream(project_id): fires a streaming GET to
build_stream, reads the first SSE chunk to confirm DSM accepted the
request, then closes. ReadTimeout is swallowed (build running server-side).
Immediate JSON error responses are parsed and raised as SynologyError.

redeploy_project simplified from 4 steps to 3:
  1. Stop (skip for STOPPED, suppress for BUILD_FAILED)
  2. trigger_build_stream — DSM pulls images + starts project
  3. Poll for RUNNING (timeout raised from 30s → 5min for large pulls)
build_stream errors are now fatal (abort with clear message).

Removes _read_compose_images_for_project, _try_delete_image and their
json/yaml/re imports — no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 08:32:03 +02:00
marcus bafa327412 feat: v0.2.4 — image delete workaround + auto-version env-var update
redeploy_project: replace broken SYNO.Docker.Image/pull with a unified
4-step delete-before-start flow for all project states (RUNNING, STOPPED,
BUILD_FAILED). Reads image tags from the project's compose.yaml via
FileStation before stopping, deletes each cached image (non-fatal), then
starts the project so DSM auto-pulls the latest version. Polls for RUNNING
as before.

update_image_tag: auto-update env vars whose value equals the numeric
version prefix of the old tag when the new tag shares the same
<digits>-<suffix> pattern (e.g. JENKINS_VERSION=2.558 → 2.560 when tag
changes 2.558-jdk21 → 2.560-jdk21). Preview mode lists the pending
auto-updates. Only triggers when the var exists and the pattern matches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 07:57:57 +02:00
marcus ae36a9fbac v0.2.3: scrub operator-specific details from CLAUDE.md
Remove hostnames, concrete container names, image tags, personal notes,
and the completed task backlog. Replace with generic DSM quirks reference,
implementation rules, and tool inventory — suitable for a public connector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 07:26:56 +02:00
marcus 7de4b56962 v0.2.2: BUILD_FAILED pull failure aborts redeploy with clear message
Remove contextlib.suppress from the image pull step in the BUILD_FAILED
redeploy path. A failed pull (e.g. non-existent tag) now immediately
returns an actionable error pointing to update_image_tag instead of
silently continuing and starting the project with stale/missing image.

Also bumps version 0.2.1 → 0.2.2 and adds CHANGELOG entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 07:22:36 +02:00
marcus 5cff7d8506 v0.2.1: redeploy_project post-start polling (30s timeout)
DSM starts containers asynchronously - start_project returns immediately
while containers are still initialising. Adds _wait_for_project_running:
polls SYNO.Docker.Project/list every 2s up to 30s after issuing start.
Reports RUNNING on success; emits a warning instead of failure on timeout
so callers can still verify with get_project_status.

Applies to all three redeploy paths (RUNNING, STOPPED, BUILD_FAILED).

Also bumps version 0.2.0 → 0.2.1 and adds CHANGELOG entry.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-04-21 07:17:36 +02:00
marcus 81d5acd83e Release 0.2.0: README, CHANGELOG, version bump
- README: complete 22-tool reference table across 6 categories;
  updated feature list to reflect all additions and bug fixes
- CHANGELOG: added with [0.2.0] and [0.1.0] entries
- pyproject.toml: 0.1.0 → 0.2.0
- CLAUDE.md: corrected tool count 17 → 22

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 07:11:08 +02:00
marcus 06735bb447 Add list_images and delete_image tools (Gruppe 1)
- list_images: lists all local Docker images sorted by size desc,
  shows size (human-readable), creation date, in-use marker, and
  update-available marker; gracefully handles container list failure
- delete_image: accepts name:tag or image hash, blocks deletion when
  image is in use by a container, requires confirmed=True to execute;
  default shows a dry-run preview
- 16 unit tests covering all paths (mock DSM client)
- ruff format + check clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 17:44:57 +02:00
marcus a0c1b6ed93 Initial implementation 2026-04-13 14:22:37 +02:00