feat(mgmt): public Management API — granted stacks manage a template's tenants - #19
Merged
Merged
Conversation
This was referenced Aug 10, 2026
…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
force-pushed
the
wt/watchtower-public-api-1f5a0e
branch
from
August 10, 2026 13:16
8ee6f45 to
7893a63
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 aStackTemplateit has been granted: list, provision under a subdomain, inspect status, redeploy, stream logs, and (with theAllowDeletecapability) 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
WATCHTOWER_APP_TOKEN) — injection/rotation already solved. Authorization: a new operator-managedTemplateManagementGrantrow (admin-only RPC:templates.listGrants/grantManagement/revokeManagement); the token alone grants nothing new.404for ungranted templates (never confirms existence); tenant lookups always constrained by the granted template id; Docker/log access goes through a synthesizedAppApiCaller, so all compose-project scoping applies unchanged; env values, deploy output, tokens and credentials never appear in any response.TenantProvisioningServiceextracted fromtemplates.addTenant; newTenantTeardownService— the first real teardown path (compose down→ row delete → Caddy reload,409while a deploy is active) — also behind the newtemplates.removeTenant.SseLogStreaming, shared byte-identically by/api/app/logsand the tenant logs endpoint.Reviewer notes
AppApiEnabledon 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).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 onMgmtApiTests.ComposeCliService.DownProjectAsync,CaddyManager.ApplyAsync,DeployQueueService.Enqueueare now virtual and substituted assembly-wide inWatchtowerApiFactory(rationale in its comment). Teardown ordering is pinned by probes sampling DB state at call time (mutation-tested).rpc-schema.jsondiff is purely additive: the four newtemplates.*methods.Verification
dotnet build Watchtower.slnx --configuration Release— 0 warnings, 0 errorsdotnet test— 207 (Application.Tests) + 117 (Api.Tests) passed, 0 failednpm run build(tsc + vite) clean