Skip to content
SHC Docs

Plan: deployment poll consumes recorder rows + small DNS/config knobs

Status: items 1–3 IMPLEMENTED (cacec86a / bc3061ed / 34140dfe — DNS e2e 5/5 incl. live TTL assert; full unit suite green). Item 4 remains a documented policy (consumer-by-consumer, no code until a consumer needs it).

1. Deployment poll reads recorder-maintained container rows

Section titled “1. Deployment poll reads recorder-maintained container rows”

The per-node container_recorder_node already maintains fresh container state TWO ways (events = low-latency push; 2s tick = authoritative re-sync), writing both the dqlite container rows (cluster-wide via raft) and the in-memory metrics StatsCache. But the leader’s 1m DeploymentPoller ignores all of it: containerCacheReaderAdapter (internal/daemon/services_seams_health.go) re-queries docker live, by label — and the leader’s local docker only sees the leader’s own containers, so the rollup is blind to stacks running on other nodes. The adapter’s own comment admits the gap (“the metrics module’s stats cache is still in progress”). Python’s poll consumed its stats cache.

The flow becomes: events → rows (push); per-node 2s tick → rows (hard re-sync); leader 1m poll → pure consumer + rollup over the rows.

  • New health.ContainerCacheReader implementation backed by the app module’s container rows: DeploymentRecord.IDContainerRepo.ListServicesForDeploymentListContainersForService → project ContainerModel (docker_status / docker_health / stats columns) into health.ContainerSnapshot.
  • The poller’s seam takes only project; the deployment ID is on the record the poller already holds — extend the read path so the DB-backed reader receives the deployment identity (either a widened interface or a per-deployment closure), without breaking the existing ContainersForProject contract for other consumers.
  • Fallback: when the DB has zero rows for the deployment (recorder lag on a brand-new stack, or pre-recorder upgrades), fall back to the existing live-docker label query. This preserves today’s leader-local behavior exactly and is strictly better off-leader.
  • Orphan reaping (liveIDs) keys on the same snapshot — DB rows carry container_id, so semantics hold.
  • DB-backed reader: seed deployment/service/container rows via the repos (sqlite harness), assert snapshot projection incl. health + stats; empty-DB → fallback invoked (stub).
  • Poller integration: existing poll tests re-run unchanged (interface compatible); add one asserting a deployment whose rows live “on another node” (rows present, local docker empty) still rolls up healthy — the multinode-correctness case.

Service.GetRecordForHost + GET /api/dns/record exist; the CLI tree lacks the verb. Add shc dns get <hostname> [--provider] with the same daemon-first dispatch as create/delete/validate (in-process → /api/dns/record → X503031DNSCFG; 404 → “no record found” exit 1).

3. Scoped DNS record knobs: dns.ttl / dns.proxied

Section titled “3. Scoped DNS record knobs: dns.ttl / dns.proxied”

Records are minted with hardcoded ttl=300, proxied=false (dns_lifecycle.go consts). Resolve both through the same 5-level config cascade as dns.target (deployment > stack > environment > tenant > global), read at record-create time in createDeploymentDNSRecords (defaults preserved when unset). SyncRecordsForHost already reconciles ttl/proxied drift in place, so changed config converges on the next deploy/recover of the stack. No --host flag syntax — config keys only. proxied only has meaning on Cloudflare (orange-cloud); other providers ignore it.

4. Policy: scoped config for koanf-tree consumers (no code yet)

Section titled “4. Policy: scoped config for koanf-tree consumers (no code yet)”

The daemon-wide koanf tree only loads the GLOBAL DB tier (the loader’s tenant/stack/env ctx extraction is stubbed), so cfg.String readers — notably provider factories — never see tenant/env/deployment-scoped rows, while configmod.Service.Get resolves all 5 tiers. The fix is NOT per-request tree rebuilds; it is consumer-by-consumer: a reader that has scope identity in hand (e.g. provider-name selection during a deploy) should consult Service.Get with that scope. No current consumer is blocked (deploys carry explicit --dns-provider + persisted DNSProvider), so this stays a policy note until one is.