v0.292.0: cloudflared readiness health check (R-841) — tunnel --metrics localhost:20241 run + Docker healthcheck on cloudflared's own /ready; the host agent reports running/not_running/unknown from State.Health
gates / gates (push) Successful in 28s
gates / gates (push) Successful in 28s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -983,7 +983,7 @@ The controller stands up its own base stack — **traefik** (reverse proxy), **c
|
||||
|
||||
`Manager.EnsureBaseStack()` creates the `traefik-public` network and the `felhom-tunnel` network, writes the forwarded-header clean-up, then deploys traefik → cloudflared → filebrowser under `${stacks_dir}/<name>`. It is:
|
||||
- **single-flight** (a `TryLock` guard — it's called from both first boot and every health tick, so overlapping runs must not race on the same stack dir),
|
||||
- **idempotent, and it reconciles** — filebrowser is skipped when running (never overwriting its compose, preserving the storage mounts `SyncFileBrowserMounts` manages); **traefik and cloudflared are rewritten and recreated when their rendered files differ from the ones on disk** (v0.286.0 — a release that changes their template reaches every installed box; equal files → nothing). A traefik rewrite that would drop the running certificate resolver is refused and logged.
|
||||
- **idempotent, and it reconciles** — filebrowser is skipped when running (never overwriting its compose, preserving the storage mounts `SyncFileBrowserMounts` manages); **traefik and cloudflared are rewritten and recreated when their rendered files differ from the ones on disk** (v0.286.0 — a release that changes their template reaches every installed box; equal files → nothing). cloudflared carries a Docker health check on its own `/ready` endpoint (v0.292.0, R-841): `healthy` only while the tunnel has a connection — the host agent reports that state to the hub. A traefik rewrite that would drop the running certificate resolver is refused and logged.
|
||||
- **non-fatal** (logs, never crashes the controller).
|
||||
|
||||
cloudflared is only deployed when a tunnel token is configured. **Triggers**: a first-boot goroutine (after stack init) and an unconditional call on every `system-health` tick (self-heal — cheap when healthy thanks to the idempotency). `monitor.EffectiveProtected` mirrors the cloudflared condition so a LAN-only node (no tunnel token) doesn't report a perpetual "protected container not running" FAIL.
|
||||
|
||||
@@ -178,8 +178,8 @@ func TestCloudflaredRender(t *testing.T) {
|
||||
if !strings.Contains(compose, CloudflaredImage) {
|
||||
t.Errorf("expected pinned cloudflared image %q", CloudflaredImage)
|
||||
}
|
||||
if !strings.Contains(compose, "command: tunnel run") {
|
||||
t.Error("expected `command: tunnel run`")
|
||||
if !strings.Contains(compose, "command: tunnel --metrics localhost:20241 run") { // R-841: fixed metrics address
|
||||
t.Error("expected `command: tunnel --metrics localhost:20241 run`")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,17 @@ services:
|
||||
image: {{.Image}}
|
||||
container_name: cloudflared
|
||||
restart: unless-stopped
|
||||
command: tunnel run
|
||||
# R-841: metrics on the container's own loopback at a FIXED port, and a health check that asks cloudflared itself
|
||||
# whether the tunnel is CONNECTED (`/ready`: 200 with readyConnections > 0, else 503 — measured 2026-10-04: with a
|
||||
# wrong token the container stays "running" while /ready is 503). The host agent reads the result with
|
||||
# `docker inspect` (State.Health) and reports the tunnel as running | not_running | unknown.
|
||||
command: tunnel --metrics localhost:20241 run
|
||||
healthcheck:
|
||||
test: ["CMD", "cloudflared", "tunnel", "--metrics", "localhost:20241", "ready"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
environment:
|
||||
- TUNNEL_TOKEN={{.CFTunnelToken}}
|
||||
dns:
|
||||
|
||||
@@ -282,3 +282,23 @@ func TestReconcileFileBrowserImage_MovesOnlyTheImage(t *testing.T) {
|
||||
t.Fatalf("an up-to-date file browser was touched: err %v calls %+v", err, *calls)
|
||||
}
|
||||
}
|
||||
|
||||
// R-841: the cloudflared compose carries a health check that asks cloudflared whether the tunnel is CONNECTED, and
|
||||
// pins the metrics address it asks. Red-proof: drop the healthcheck block from the template and this fails.
|
||||
func TestRenderCloudflared_HasAReadinessHealthcheck(t *testing.T) {
|
||||
for _, tunnel := range []bool{false, true} {
|
||||
f, err := infra.RenderCloudflared(infra.CloudflaredData{CFTunnelToken: "tok", Tunnel: tunnel})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := f["docker-compose.yml"].Content
|
||||
for _, want := range []string{
|
||||
"command: tunnel --metrics localhost:20241 run",
|
||||
`test: ["CMD", "cloudflared", "tunnel", "--metrics", "localhost:20241", "ready"]`,
|
||||
} {
|
||||
if !strings.Contains(c, want) {
|
||||
t.Fatalf("tunnel=%v: compose lacks %q:\n%s", tunnel, want, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user