| index b170ff6e8..16455f514 100644 |
| --- a/internal/host/agent.go |
| +++ b/internal/host/agent.go |
| @@ -15,13 +15,26 @@ |
| // read by statusLoop. CARLOS_STATUS_BUCKET names the S3 bucket each |
| // box writes its own status/boxes/<label>.json snapshot to and (when |
| // it wins the lease) publishes status/fleet.json + the viewer assets |
| -// to; unset -> the loop never starts, logging one line and returning — |
| -// the same "inert until configured" convention adoptLoop uses for |
| +// to. When CARLOS_STATUS_BUCKET is unset, it DEFAULTS to whatever |
| +// CARLOS_DEPLOYMENT_BUCKET is set to (resolveStatusBucket) — the edge |
| +// and console both serve the fleet status page out of the same |
| +// deployment bucket adoptLoop already publishes releases to, so the |
| +// common single-bucket deployment needs to set only one env var; an |
| +// explicit CARLOS_STATUS_BUCKET still always wins, for the deployments |
| +// that deliberately want a second, independent bucket. Both unset -> |
| +// the loop never starts, logging one line and returning — the same |
| +// "inert until configured" convention adoptLoop uses for |
| // CARLOS_DEPLOYMENT_BUCKET/_DIR, and litestream's own env-gated |
| // replication before that. CARLOS_STATUS_PREFIX and CARLOS_STATUS_REGION |
| // feed lease.Acquire (the single-writer election for the publish half |
| // of each tick); both may be empty (region falls back to the AWS |
| // default chain). |
| +// HONEST LIMIT: the CARLOS_DEPLOYMENT_BUCKET fallback only ever |
| +// resolves to an S3 bucket. A deployment adopting from |
| +// CARLOS_DEPLOYMENT_DIR (a plain directory — tests, or a single-box |
| +// no-AWS deployment) has nothing for the fallback to resolve to, so |
| +// DIR-based deployments do not publish fleet status yet; give them |
| +// their own CARLOS_STATUS_BUCKET explicitly if they want the page. |
| // - STATUS_BOX_REGION — an operator-set, coarse region word (e.g. |
| // "EU · Ireland") carried into status.Probes.Region and published |
| // verbatim on this box's snapshot; "" omits the field. Deliberately |
| @@ -168,6 +181,30 @@ func adoptLoop(reg *registry.Registry) { |
| } |
| } |
| |
| +// resolveStatusBucket applies the status/deployment bucket defaulting |
| +// documented in this file's package comment: an explicit CARLOS_STATUS_BUCKET |
| +// always wins, and when it's unset the status loop falls back to whatever |
| +// CARLOS_DEPLOYMENT_BUCKET is set to, so the common single-bucket deployment |
| +// is one env var instead of two that must be kept in sync by hand. Returns |
| +// "" when neither is set — the loop stays disabled, same as before this fix. |
| +// Deliberately does NOT fall back to CARLOS_DEPLOYMENT_DIR: the status page |
| +// has no directory-store serving path (the edge/console read status/ out of |
| +// an S3 bucket, not a local directory), so a DIR-based deployment simply |
| +// doesn't get this fallback — see the "HONEST LIMIT" note in the package |
| +// comment above. |
| +// |
| +// Takes a getenv func rather than reading os.Getenv itself so the defaulting |
| +// logic is testable without mutating process-wide environment variables — |
| +// t.Setenv serializes tests within a package and the host package already |
| +// has other env-reading tests; a plain map/func seam avoids that coupling |
| +// entirely. |
| +func resolveStatusBucket(getenv func(string) string) string { |
| + if b := getenv("CARLOS_STATUS_BUCKET"); b != "" { |
| + return b |
| + } |
| + return getenv("CARLOS_DEPLOYMENT_BUCKET") |
| +} |
| + |
| // statusLoop runs the fleet status page's 5-minute tick, forever, once armed |
| // — see the package doc comment above for the full env contract. Missing |
| // CARLOS_STATUS_BUCKET disables the loop with one log line, the same |
| @@ -190,9 +227,9 @@ func adoptLoop(reg *registry.Registry) { |
| // other tick failure. |
| func statusLoop(reg *registry.Registry) { |
| ctx := context.Background() |
| - bucket := os.Getenv("CARLOS_STATUS_BUCKET") |
| + bucket := resolveStatusBucket(os.Getenv) |
| if bucket == "" { |
| - log.Printf("agent: status loop disabled: CARLOS_STATUS_BUCKET not set") |
| + log.Printf("agent: status loop disabled: neither CARLOS_STATUS_BUCKET nor CARLOS_DEPLOYMENT_BUCKET is set (CARLOS_DEPLOYMENT_DIR deployments don't publish status yet — set CARLOS_STATUS_BUCKET explicitly)") |
| return |
| } |
| prefix := os.Getenv("CARLOS_STATUS_PREFIX") |
| @@ -204,7 +241,12 @@ func statusLoop(reg *registry.Registry) { |
| log.Printf("agent: status loop disabled: %v", err) |
| return |
| } |
| - log.Printf("agent: status loop armed (5min + jitter)") |
| + // The effective bucket is worth a log line on its own: it's easy to set |
| + // CARLOS_DEPLOYMENT_BUCKET and CARLOS_STATUS_BUCKET to different values |
| + // by accident (or on purpose) and end up with the agent writing status |
| + // to a bucket the edge/console never look at — this line is the one |
| + // place an operator can confirm which bucket actually won. |
| + log.Printf("agent: status loop armed (5min + jitter), publishing to bucket %s", bucket) |
| |
| // Jittered on the same shape as adoptLoop's delay: every box on the same |
| // 5-minute cadence would all hit the lease and the bucket in the same |