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”Problem
Section titled “Problem”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.
Design
Section titled “Design”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.ContainerCacheReaderimplementation backed by the app module’s container rows:DeploymentRecord.ID→ContainerRepo.ListServicesForDeployment→ListContainersForService→ projectContainerModel(docker_status / docker_health / stats columns) intohealth.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 existingContainersForProjectcontract 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.
2. shc dns get
Section titled “2. shc dns get”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.