feat(worker): add Docker Compose discovery and management
Некоторые проверки не удались
CI / test (push) Failing after 6s
Docker / Build and publish worker image (push) Failing after 8s
Некоторые проверки не удались
CI / test (push) Failing after 6s
Docker / Build and publish worker image (push) Failing after 8s
Add an internal/compose package that discovers Compose projects via `docker compose ls` + `docker ps` labels (grouped by com.docker.compose.project/service) and enriches each container with `docker inspect` ports/mounts and Traefik router labels. Management runs `docker compose` in each project's working directory for up/down/stop/restart/pull plus per-service variants and log tails. Wire it into the webapp: a 60s ComposeRefresher (constructed in New, started in Start, stopped in Close), a /compose list + detail + logs HTML surface, and /web/api/compose/* JSON endpoints (list, detail, logs, project/service lifecycle). Browser lifecycle POSTs are session+CSRF protected; the /web/api/* variants accept HTTP basic auth. WORKER_COMPOSE_ENABLED defaults on (false to disable). Tests cover discovery parsing/grouping/traefik/summary, the action allowlists, and the full handler surface (list/detail/logs HTML+API, CSRF enforcement, disabled/unknown-action rejection, audit writes, success+failure exec paths) via a stub Docker binary.
Этот коммит содержится в:
44
internal/webapp/templates/compose.html
Обычный файл
44
internal/webapp/templates/compose.html
Обычный файл
@@ -0,0 +1,44 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Docker Compose projects</h1>
|
||||
{{if not .Enabled}}
|
||||
<p class="muted">Compose management is disabled (WORKER_COMPOSE_ENABLED=false). Re-enable it and restart the worker to see projects here.</p>
|
||||
{{else}}
|
||||
<p class="muted">Discovered from <code>docker compose ls</code> and <code>docker ps</code> labels, refreshed every 60s. Lifecycle actions (up/down/stop/restart/pull) run <code>docker compose</code> in each project's working directory.</p>
|
||||
{{if .LastAt.IsZero}}
|
||||
<p class="muted">No refresh yet — the first scan runs within 60s of start.</p>
|
||||
{{else}}
|
||||
<p class="muted">Last refresh: {{fmtTime .LastAt}}</p>
|
||||
{{end}}
|
||||
{{range .Snapshot.Errors}}
|
||||
<p class="muted">⚠ {{.}}</p>
|
||||
{{end}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Status</th>
|
||||
<th>Services</th>
|
||||
<th>Containers</th>
|
||||
<th>Running</th>
|
||||
<th>Compose file</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Snapshot.Projects}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.Status}}</td>
|
||||
<td>{{.ServiceCount}}</td>
|
||||
<td>{{.ContainerCount}}</td>
|
||||
<td>{{.RunningCount}}</td>
|
||||
<td>{{.ConfigFiles}}</td>
|
||||
<td><a href="/compose/{{.Name}}">details</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="7" class="muted">No Compose projects discovered.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</section>{{end}}
|
||||
81
internal/webapp/templates/compose_detail.html
Обычный файл
81
internal/webapp/templates/compose_detail.html
Обычный файл
@@ -0,0 +1,81 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>{{.Project.Name}}</h1>
|
||||
<p class="muted">Working dir: <code>{{.Project.WorkingDir}}</code>{{if .Project.ConfigFiles}} · compose file: <code>{{.Project.ConfigFiles}}</code>{{end}} · last refresh: {{fmtTime .LastAt}}</p>
|
||||
|
||||
{{if .ActionMessage}}
|
||||
<p class="{{if .ActionOK}}muted{{else}}muted{{end}}">{{if .ActionOK}}✓{{else}}⚠{{end}} {{.ActionMessage}}</p>
|
||||
{{end}}
|
||||
|
||||
<h2>Project actions</h2>
|
||||
<div class="cards">
|
||||
<form action="/compose/{{.Project.Name}}/up" method="post"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button type="submit">Up</button></form>
|
||||
<form action="/compose/{{.Project.Name}}/restart" method="post"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button type="submit">Restart</button></form>
|
||||
<form action="/compose/{{.Project.Name}}/pull" method="post"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button type="submit">Pull</button></form>
|
||||
<form action="/compose/{{.Project.Name}}/stop" method="post"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button type="submit">Stop</button></form>
|
||||
<form action="/compose/{{.Project.Name}}/down" method="post"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button type="submit">Down</button></form>
|
||||
<a href="/compose/{{.Project.Name}}/logs"><button type="button">Logs</button></a>
|
||||
</div>
|
||||
|
||||
<h2>Services ({{.Project.ServiceCount}})</h2>
|
||||
<table>
|
||||
<thead><tr><th>Service</th><th>Container</th><th>Image</th><th>State</th><th>Status</th><th>Health</th><th>PID</th><th>Ports</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{$root := .}}
|
||||
{{range .Project.Services}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{range .Containers}}{{.Name}}<br>{{end}}</td>
|
||||
<td>{{range .Containers}}{{.Image}}<br>{{end}}</td>
|
||||
<td>{{range .Containers}}{{.State}}<br>{{end}}</td>
|
||||
<td>{{range .Containers}}{{.Status}}<br>{{end}}</td>
|
||||
<td>{{range .Containers}}{{.Health}}<br>{{end}}</td>
|
||||
<td>{{range .Containers}}{{.PID}}<br>{{end}}</td>
|
||||
<td>{{range .Containers}}{{range .Ports}}{{.HostPort}}<br>{{end}}{{end}}</td>
|
||||
<td>
|
||||
<form action="/compose/{{$root.Project.Name}}/service/{{.Name}}/up" method="post" style="display:inline"><input type="hidden" name="csrf_token" value="{{$root.CSRFToken}}"><button type="submit">up</button></form>
|
||||
<form action="/compose/{{$root.Project.Name}}/service/{{.Name}}/restart" method="post" style="display:inline"><input type="hidden" name="csrf_token" value="{{$root.CSRFToken}}"><button type="submit">restart</button></form>
|
||||
<form action="/compose/{{$root.Project.Name}}/service/{{.Name}}/stop" method="post" style="display:inline"><input type="hidden" name="csrf_token" value="{{$root.CSRFToken}}"><button type="submit">stop</button></form>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="9" class="muted">No services discovered for this project.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{if .Project.GroupedMounts}}
|
||||
<h2>Mounts</h2>
|
||||
<table>
|
||||
<thead><tr><th>Source</th><th>Destination</th><th>Type</th><th>Used by</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Project.GroupedMounts}}
|
||||
<tr>
|
||||
<td>{{.Source}}</td>
|
||||
<td>{{.Destination}}</td>
|
||||
<td>{{.Type}}</td>
|
||||
<td>{{.SharedMountSummary}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
{{if .Project.AllTraefikRoutes}}
|
||||
<h2>Traefik routes</h2>
|
||||
<table>
|
||||
<thead><tr><th>Router</th><th>Service</th><th>Container</th><th>Hostnames</th><th>Prefixes</th><th>Rule</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Project.AllTraefikRoutes}}
|
||||
<tr>
|
||||
<td>{{.RouterName}}</td>
|
||||
<td>{{.Service}}</td>
|
||||
<td>{{.Container}}</td>
|
||||
<td>{{range .Hostnames}}{{.}}<br>{{end}}</td>
|
||||
<td>{{range .PathPrefixes}}{{.}}<br>{{end}}</td>
|
||||
<td><code>{{.Rule}}</code></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</section>{{end}}
|
||||
6
internal/webapp/templates/compose_logs.html
Обычный файл
6
internal/webapp/templates/compose_logs.html
Обычный файл
@@ -0,0 +1,6 @@
|
||||
{{define "body"}}<section class="card">
|
||||
<h1>Compose logs: {{.Project}}</h1>
|
||||
<p class="muted">Last 300 lines from <code>docker compose logs --no-color --tail 300</code>. Streaming tails are not supported in the web UI yet.</p>
|
||||
<pre class="logs">{{if .Output}}{{.Output}}{{else}}<span class="muted">(no output)</span>{{end}}</pre>
|
||||
<p><a href="/compose/{{.Project}}">← back to {{.Project}}</a></p>
|
||||
</section>{{end}}
|
||||
@@ -14,6 +14,7 @@
|
||||
<nav>
|
||||
<a href="/overview">Overview</a>
|
||||
<a href="/apps">Apps</a>
|
||||
<a href="/compose">Compose</a>
|
||||
<a href="/checks">Checks</a>
|
||||
<a href="/notifications">Notifications</a>
|
||||
<a href="/logs">Logs</a>
|
||||
|
||||
Ссылка в новой задаче
Block a user