Skip to content

feat: container update lifecycle hooks (pre_update, post_update, pre_stop, pre_rollback, post_rollback) - #217

Merged
Quenary merged 15 commits into
Quenary:mainfrom
canblmz1:feat/container-update-hooks
Aug 1, 2026
Merged

Quenary merged 15 commits into
Quenary:mainfrom
canblmz1:feat/container-update-hooks

Conversation

@canblmz1

Copy link
Copy Markdown
Contributor

Summary

Closes #214.

Implements the container update lifecycle hooks feature agreed in the issue thread: users can configure shell commands per container that run inside that container at specific points of the update/rollback flow, so they can do things like pg_dump before an update without Tugtainer knowing anything about databases.

  • Agent: new guarded POST /api/container/exec/{name_or_id} endpoint, runs sh -c "<command>" inside a container via python_on_whales. Refuses with 403 unless ALLOW_EXEC=true on that agent (default false).
  • Backend: single hooks JSON column on containers (not per-hook columns, so future hook types never need another migration) with hook names pre_update, post_update, pre_stop, pre_rollback, post_rollback. Writes are rejected with 403 unless ALLOW_HOOKS=true on the backend (default false), and the executor never issues exec calls at all when the flag is off, even if stale hook data exists in the db from a previously-enabled period.
  • Executor wiring in execute_update_plan:
    • pre_stop runs for every container about to be stopped; pre_update additionally for containers actually being updated. A failure aborts that container's update through the existing errors/_can_update() mechanism — no new abort path needed, the container is simply left running.
    • post_update, pre_rollback, post_rollback are report-only (logged, never block).
    • pre_rollback fires while the failed/unhealthy container is still alive, right before it gets stopped — both from the healthcheck-fail branch and from the exception/cleanup branch (only if the container is actually still running at that point).
  • Frontend: new "Hooks" panel in the container card (one textarea per hook, one shell command per line), hidden for protected containers and hidden entirely unless the backend reports ALLOW_HOOKS=true.

Both ALLOW_HOOKS and ALLOW_EXEC default to false and are documented in .env.example and the new README "Hooks" section.

Test plan

  • pytest (agent + backend + shared): 79 passed, 1 pre-existing unrelated failure (test_workdir_logic, a Windows path-separator issue in container_util, reproduces on main without this branch, unrelated to hooks)
  • ruff check .: clean
  • mypy backend agent shared: clean
  • Alembic migration verified up/down/up on a scratch sqlite db
  • ng test (frontend): 126/126 passed
  • ng lint: clean
  • prettier --check .: clean

Design decisions were discussed and agreed with @Quenary in #214 before implementation, including the ALLOW_HOOKS/ALLOW_EXEC two-layer gate.

Can added 15 commits July 30, 2026 22:47
Two-layer gate for the upcoming container update hooks feature:
ALLOW_HOOKS (backend, default false) is the feature gate, ALLOW_EXEC
(agent, default false) is defense in depth so a compromised/misbehaving
backend can't get arbitrary exec on agents that haven't opted in.
No allowlist here (unlike RunCommandRequestBodySchema/command_validator) -
this schema is for the arbitrary-command-by-design exec endpoint, gated
instead by the agent's ALLOW_EXEC flag.
Runs sh -c "<command>" inside a container via python_on_whales'
container.execute(). Refuses with 403 unless ALLOW_EXEC=true; a
non-zero exit is already turned into a 424 with stdout/stderr by the
existing DockerException handler.
Single JSON field (not per-hook columns) so new hook types never need
another migration. Discussed and agreed in
Quenary#214.
…d endpoint

PATCH /containers/{host_id}/{c_name} now rejects a hooks payload with
403 when ALLOW_HOOKS is off. GET /containers/hooks_enabled lets the
frontend decide whether to show the hooks form at all.
pre_update/pre_stop failures abort that container's update via the
existing errors/_can_update mechanism (no new abort path needed).
post_update/pre_rollback/post_rollback are report-only. pre_rollback
fires while the failed container is still alive, both from the
healthcheck-fail branch and the exception/cleanup branch.
@Quenary

Quenary commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Great work!
I really appreciate keeping the changes in separate commits. It makes reviewing the PR much more convenient.

@Quenary
Quenary merged commit f807199 into Quenary:main Aug 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Appetite for a pre-update data layer (DB dump + volume snapshot) alongside image rollback?

2 participants