Files
package-updater/internal/master/web/templates/service.html
T
shcizo 41a619b316
Docker Build / docker (push) Successful in 38s
feat: master/agent phase 3 — webhook and auto mode (#8) (#22)
## Summary
- `POST /update` on the master (outside OIDC, bearer `UPDATER_API_KEY`; unset → 503). Matches `image` against `image_repo` on all agents, marks `available_*` on every match, starts webhook-triggered jobs for `auto` services targeting `<image_repo>:<tag>` (followed tag when no tag is given), and waits for them up to `UPDATE_TIMEOUT` (default 5m). 404 on no match, 500 if any auto job failed or timed out, otherwise 200.
- Manual/auto toggle per service on the overview and service page (`POST /services/{id}/mode`, CSRF); auto is blocked for self, `build:` and container services with the reason shown. "Update available" badge and an "Available" row.
- `FinishJob` clears `available_*` only when the successful job was created for that exact ref, after it was reported. Migration `0003_webhook.sql`, phase-3 plan doc, README / gitea-action README / CLAUDE.md / deploy example updated. `action.yml` unchanged.

Closes #8

## Behaviour changes for CI
- Mixed results now return 500 (the old 207 is gone); no match returns 404 instead of 200.
- With a `tag`, Compose services get pinned to `<image_repo>:<tag>` via `compose.override.yaml`; the tag must be a pushed tag.
- `busy` (job already active) and `refused` (not updatable) are reported but return 200.
- The reverse proxy read timeout must exceed `UPDATE_TIMEOUT`.

## Test plan
- [x] `go test ./...` and `go test -race ./internal/agent/... ./internal/master/... ./internal/e2e/`
- [x] `go test -tags integration ./internal/master/store/` against throwaway Postgres
- [x] UI verified in browser (mode toggle, disabled reason, badge via SSE, Available row)
- [ ] Manual: webhook with manual service → `marked` + badge
- [ ] Manual: webhook with auto service → waits, `succeeded`, badge cleared, override pinned
- [ ] Manual: wrong token 401, unknown image 404, offline agent 500
- [ ] Gitea action pointed at the master's `/update` in a real CI run

Reviewed-on: #22
2026-10-12 20:03:22 +00:00

59 lines
2.8 KiB
HTML

{{define "content"}}
<h1>{{.Service.DisplayName}}</h1>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<table class="details">
<tr><th>Kind</th><td>{{.Service.Kind}}</td></tr>
<tr><th>Agent</th><td>{{if .Service.AgentName}}{{.Service.AgentName}}{{else}}Swarm{{end}}</td></tr>
<tr><th>Image</th><td><code>{{shortRef .Service.ActualRef}}</code></td></tr>
{{if .Service.ActualDigest}}<tr><th>Digest</th><td><code>{{.Service.ActualDigest}}</code></td></tr>{{end}}
<tr><th>Status</th><td class="status status-{{.Service.Status}}">{{.Service.Status}}</td></tr>
{{if .Service.DesiredRef}}<tr><th>Desired image</th><td><code>{{shortRef .Service.DesiredRef}}</code></td></tr>{{end}}
{{if .Service.AvailableRef}}<tr><th>Available</th><td><code>{{.Service.AvailableRef}}</code> <span class="muted">via {{.Service.AvailableSource}}{{with .Service.AvailableAt}}, {{.Format "2006-01-02 15:04"}}{{end}}</span></td></tr>{{end}}
<tr><th>Mode</th><td>{{.Service.UpdateMode}}</td></tr>
{{if .ConfigFiles}}<tr><th>Config files</th><td>{{range .ConfigFiles}}<code>{{.}}</code><br>{{end}}</td></tr>{{end}}
</table>
{{if .Reason}}
<p>Update unavailable: {{.Reason}}.</p>
{{else}}
<form method="post" action="/services/{{.Service.ID}}/update">
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
<button type="submit">Update</button>
</form>
<p class="muted">Pulls the current tag again and redeploys the service.</p>
{{end}}
<form method="post" action="/services/{{.Service.ID}}/mode">
<input type="hidden" name="csrf_token" value="{{.CSRF}}">
{{if eq .Service.UpdateMode "auto"}}<input type="hidden" name="mode" value="manual">
<button type="submit">Make manual</button>
<p class="muted">The webhook will only mark new versions as available.</p>
{{else}}<input type="hidden" name="mode" value="auto">
<button type="submit"{{if .AutoBlocked}} disabled{{end}}>Make auto</button>
<p class="muted">{{if .AutoBlocked}}Auto unavailable: {{.AutoBlocked}}.{{else}}The webhook will update this service as soon as CI pushes a new image.{{end}}</p>
{{end}}
</form>
<h2>Update history</h2>
<div id="history" hx-get="/fragments/services/{{.Service.ID}}/jobs" hx-trigger="sse:jobs" hx-swap="innerHTML">
{{template "history" .}}
</div>
{{end}}
{{define "history"}}
<table>
<thead><tr><th>Job</th><th>Status</th><th>From → to</th><th>Requested by</th><th>Created</th><th>Duration</th></tr></thead>
<tbody>
{{range .Jobs}}
<tr>
<td><a href="/jobs/{{.ID}}">#{{.ID}}</a></td>
<td class="status status-job-{{.Status}}">{{.Status}}</td>
<td><code>{{shortRef .FromRef}}</code> → <code>{{shortRef .ToRef}}</code></td>
<td>{{.RequestedBy}}</td>
<td>{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
<td>{{duration .}}</td>
</tr>
{{else}}
<tr><td colspan="6">No updates yet.</td></tr>
{{end}}
</tbody>
</table>
{{end}}