Commit Graph

22 Commits

Author SHA1 Message Date
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 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 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 223075e602 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>
2026-04-21 07:09:21 +02:00
marcus d9f0e75d0a Fix get_container_status: clean name + dual-format response handling
DSM response has two top-level keys:
  details → Docker Engine inspect (State, NetworkSettings, Mounts)
  profile → DSM format (image, port_bindings)

_format_container_detail now reads:
  Status/Running/StartedAt from details.State
  Image               from profile.image
  IP addresses        from details.NetworkSettings.Networks
  Port bindings       from profile.port_bindings
  Mounts              from details.Mounts

Also: debug_container_response tool removed, json/sys imports cleaned up.
27 container tests all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 06:59:50 +02:00
marcus 584d53e6e4 Fix get_container_status: clean name + dual-format response handling
- get_container_status now strips hash prefix from user input and calls
  SYNO.Docker.Container/get with the clean name (e.g. 'jenkins'), not the
  hash-prefixed form — the get endpoint accepts only the clean name
- _format_container_detail: unwraps 'container' wrapper key if present
  (DSM may return {"container": {State, Config, ...}} at the data level)
- Flat-format fallback: reads lowercase 'status'/'image' fields when
  Docker Engine nested format (State/Config) is absent
- Diagnostic stderr logging for data_keys, unwrap, status, image
- 25 container tests all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:49:30 +02:00
marcus c8cda5ef2b 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>
2026-04-13 21:43:02 +02:00
marcus 6fa35e1b48 Remove pull_image + list_registries; mark Gruppen 6+7 as entfällt
DSM methods for SYNO.Docker.Image/pull and SYNO.Docker.Registry/get
did not behave as expected in production testing against the NAS.
Tools deregistered, modules deleted, tests removed, CLAUDE.md updated.
Tool count: 19 → 17.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 20:03:17 +02:00
marcus 5fe8f5bc73 Add pull_image + list_registries; remove Gruppe 5 (no Volume API)
- pull_image: SYNO.Docker.Image/pull with repository+tag split via
  rpartition; polls image list every 3 s until image appears, 120 s timeout
- list_registries: SYNO.Docker.Registry/get; shows name, URL, active marker
- Gruppe 5 (Volumes) removed from roadmap — SYNO.Docker.Volume does not exist
- CLAUDE.md: tool count 17 → 19, Volumes section removed
- 28 tests all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 19:28:45 +02:00
marcus 59f7fc1d6c Fix create_network + delete_network: POST with DevTools-confirmed format
create_network:
  - Switch from GET request() to post_request()
  - name, subnet, gateway, iprange → json.dumps(value) per DSM convention
  - disable_masquerade=json.dumps(False) added as required fixed param

delete_network:
  - Switch from GET request() to post_request() with method "remove"
    (was "delete" — wrong method name)
  - Send full network object from list response as
    networks=json.dumps([{...}]) including all DSM fields plus _key=id
    (ipv6_gateway, ipv6_subnet, ipv6_iprange, disable_masquerade with
    safe defaults for fields absent from the list response)

Tests updated: mock post_request, assert correct method/params structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:56:21 +02:00
marcus 4cee16922f Add list_networks, create_network, delete_network tools (Gruppe 4)
list_networks:
  SYNO.Docker.Network/list → shows name, driver, subnet, gateway,
  attached containers. Response key is "network" (not "networks").

create_network(name, driver, subnet, gateway, ip_range, enable_ipv6, confirmed):
  Dry-run preview without confirmed=True. Passes enable_ipv6 as
  json.dumps(bool) per N4S4 reference. Optional params (subnet,
  gateway, iprange) omitted from request when not provided.

delete_network(name, confirmed):
  Validates network exists and has no attached containers before
  deleting. Clear error listing attached container names if blocked.

15 unit tests covering all paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:42:10 +02:00
marcus 6bdd2bcb6a Add system_df and system_prune tools (Gruppe 3)
system_df:
  Assembles disk-usage report from SYNO.Docker.Image/list and
  SYNO.Docker.Container/list. Reports image count/size/reclaimable
  (images not referenced by any container), container running/stopped.
  Gracefully degrades when one API is unavailable.

system_prune:
  Without confirmed=True: lists dangling/unused images and stopped
  containers with sizes (dry-run preview).
  With confirmed=True: calls SYNO.Docker.Utils/prune and reports
  reclaimed space from the response (SpaceReclaimed field).

10 unit tests: stats counts, reclaimable detection, preview content,
confirmed execution, missing-response-field graceful handling, API error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:36:49 +02:00
marcus a8da306ce5 Add container_stats tool (Gruppe 2); remove rename_container
container_stats(container_name) calls SYNO.Docker.Container/stats,
locates the entry by name (stripping the DSM-added leading slash),
and reports:
  - CPU %  (standard Docker formula: cpu_delta / system_delta * cpus * 100)
  - Memory used / limit  (human-readable)
  - Network I/O rx / tx  (summed across all interfaces)
  - Block I/O read / write  (from io_service_bytes_recursive)

Gracefully handles first-poll (precpu system_cpu_usage absent → 0%).
7 unit tests covering: found, CPU formula, memory format, slash-strip,
not-found, API error, no-precpu fallback.

rename_container removed: DSM Container Manager offers no rename API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:29:17 +02:00
marcus 5edd051830 Fix delete_image: POST with images JSON array (DevTools-confirmed format)
DSM Container Manager rejects name+tag and sha256 id params (error 114).
Browser DevTools capture shows the correct call is:

  POST SYNO.Docker.Image / delete / version=1
  images=[{"repository":"nouchka/sqlite3","tags":["latest"]}]

Changes:
- Add DsmClient.post_request() for form-encoded POST requests
- delete_image now calls post_request with version=1 and the images
  JSON array built from the resolved repository name and tag
- Remove unused docker error-code dict from _error_message()
- Tests mock post_request and assert the images param content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:19:22 +02:00
marcus 2b1e2ead7d Fix delete_image: use sha256 image ID instead of name+tag
DSM SYNO.Docker.Image/delete returns error 114 when called with name+tag.
The API expects the sha256 hash from the image list (field "id") as the
"id" parameter.

- Look up the sha256 hash from SYNO.Docker.Image/list (already fetched
  for the in-use check), then pass params={"id": img_hash}
- Guard against missing hash with a clear error message
- Updated tests to assert id param is sent, name/tag are absent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:03:18 +02:00
marcus 24c3059e83 Fix delete_image: use rpartition for name:tag splitting
partition(":") split at the first colon, so registry-prefixed images
like "ghcr.io/open-webui/open-webui:v0.8.10" produced name="ghcr"
instead of the correct repository name, causing DSM error 114.

rpartition(":") always splits at the last colon, correctly handling:
- plain images: "nginx:1.24" → name="nginx", tag="1.24"
- namespaced: "nouchka/sqlite3:latest" → correct split
- registry URLs: "ghcr.io/foo/bar:v1" → name="ghcr.io/foo/bar", tag="v1"
- no tag: "nginx" → name="nginx", tag="latest" (fallback)

Added regression test verifying correct params sent to DSM delete API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 17:51:02 +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