Skip to content

feat(mgmt): public Management API — granted stacks manage a template's tenants - #19

Merged
swimmesberger merged 5 commits into
mainfrom
wt/watchtower-public-api-1f5a0e
Aug 10, 2026
Merged

feat(mgmt): public Management API — granted stacks manage a template's tenants#19
swimmesberger merged 5 commits into
mainfrom
wt/watchtower-public-api-1f5a0e

Conversation

@swimmesberger

Copy link
Copy Markdown
Owner

What

Adds the public Management API (/api/mgmt/*): a token-authenticated REST surface through which a deployed stack — typically a product vendor's own central-management UI, itself running as a Watchtower stack — manages the tenants of a StackTemplate it has been granted: list, provision under a subdomain, inspect status, redeploy, stream logs, and (with the AllowDelete capability) deprovision with an opt-in volume purge.

Why

Watchtower acts as the background orchestrator for product-specific management UIs that provision customer tenants (e.g. customer4.example.com). Neither existing surface fits: the admin JSON-RPC API would hand the vendor UI operator credentials (a leaked token owns the Docker host), and the App API is self-only by design. See ADR-0009.

How

  • Credential: the caller stack's existing App API token (WATCHTOWER_APP_TOKEN) — injection/rotation already solved. Authorization: a new operator-managed TemplateManagementGrant row (admin-only RPC: templates.listGrants/grantManagement/revokeManagement); the token alone grants nothing new.
  • Uniform 404 for ungranted templates (never confirms existence); tenant lookups always constrained by the granted template id; Docker/log access goes through a synthesized AppApiCaller, so all compose-project scoping applies unchanged; env values, deploy output, tokens and credentials never appear in any response.
  • TenantProvisioningService extracted from templates.addTenant; new TenantTeardownService — the first real teardown path (compose down → row delete → Caddy reload, 409 while a deploy is active) — also behind the new templates.removeTenant.
  • SSE log streaming extracted to SseLogStreaming, shared byte-identically by /api/app/logs and the tenant logs endpoint.
  • Operator UI: Management API card on the template detail page; per-tenant removal with typed-slug confirmation.
  • Docs: docs/public-mgmt-api.md, ADR-0009, README pointers.

Reviewer notes

  • The teardown active-deploy check is a check, not a lock; the narrow race (deploy enqueued in the instant after it passes) leaves orphaned containers, documented as accepted residual risk in ADR-0009 with the cleanup command.
  • AppApiEnabled on the calling stack is the single kill switch for both public surfaces; a tenant's own flag doesn't hide it from its manager (documented).
  • The two Docker-touching endpoints (GET tenant status, logs) have no happy-path tests: the Docker socket path is hard-coded, so 200-vs-503 depends on the runner having a daemon — recorded in a remark on MgmtApiTests.
  • Test seams: ComposeCliService.DownProjectAsync, CaddyManager.ApplyAsync, DeployQueueService.Enqueue are now virtual and substituted assembly-wide in WatchtowerApiFactory (rationale in its comment). Teardown ordering is pinned by probes sampling DB state at call time (mutation-tested).
  • rpc-schema.json diff is purely additive: the four new templates.* methods.

Verification

  • dotnet build Watchtower.slnx --configuration Release — 0 warnings, 0 errors
  • dotnet test — 207 (Application.Tests) + 117 (Api.Tests) passed, 0 failed
  • Schema export byte-stable; npm run build (tsc + vite) clean

…s tenants

A vendor's central-management UI, itself deployed as a Watchtower stack, can
now drive the tenants of a StackTemplate it has been granted: list, provision
under a subdomain, inspect status, redeploy, stream logs, and (with the
AllowDelete capability) deprovision — via the token-authenticated REST surface
/api/mgmt/*, without ever holding operator credentials.

- Credential: the caller stack's existing App API token (WATCHTOWER_APP_TOKEN);
  authorization is a new operator-managed TemplateManagementGrant row. No grant
  or unknown template answers a uniform 404.
- Admin RPC: templates.listGrants / grantManagement / revokeManagement
  (Admin-role-gated, upsert preserves CreatedAt) + templates.removeTenant.
- New TenantProvisioningService (extracted from templates.addTenant) and
  TenantTeardownService — the first real teardown path: compose down before
  the row delete, Caddy reload after, active deploys refused with 409.
- SSE log streaming extracted to SseLogStreaming, shared byte-identically by
  /api/app/logs and the new tenant logs endpoint.
- Operator UI: Management API card on the template page (grants list,
  per-row AllowDelete toggle, revoke) and per-tenant removal with typed
  confirmation and an explicit opt-in volume purge.
- Docs: docs/public-mgmt-api.md + ADR-0009; README pointers.
- Tests: grant/provisioning/teardown unit suites and 36 endpoint tests,
  including uniform-404, cross-template isolation, and teardown-order pins.
…elete, picker states, accessible remove tooltip

- Track Allow-delete pending per row (one observer's .variables only sees the latest call)
- Optimistic setQueryData flip with snapshot rollback on error
- Distinguish picker loading vs error vs no-grantable-stacks states
- Shared remove-tenant button: keyboard-focusable disabled-reason tooltip, present in table and mobile card
…a KubeSolo

Records the direction that Watchtower will drive the Kube API instead of
docker compose. Targeting the API (not a distribution) keeps Watchtower
theoretically cluster-capable while features continue to target single-node
workloads; KubeSolo (Portainer, single-node K8s with clustering removed) is
the practical deployment, not a ceiling. Docker + Compose remains the
shipping runtime; stack-definition format is deferred to a migration ADR.
Adds a direction note to scaling-beyond-one-node.md reframing its
Swarm-vs-k3s analysis.
The transition runs the Docker and Kube engines side by side behind the
same seam interfaces, making them contracts designed against both runtimes.
Feature parity is not promised: new features may be Kube-only, and Docker
support is expected to fade out, to be recorded in a superseding ADR.
No lowest-common-denominator interfaces: when a feature needs something
Docker cannot do or carry, evolve the interface to the Kube-shaped
capability and leave the Docker engine behind on it (reported as
unsupported, not emulated). The weaker implementation constrains nothing.
@swimmesberger
swimmesberger force-pushed the wt/watchtower-public-api-1f5a0e branch from 8ee6f45 to 7893a63 Compare August 10, 2026 13:16
@swimmesberger
swimmesberger merged commit 5cecb4e into main Aug 10, 2026
1 check 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.

1 participant