fix(dinghy-layer): prune orphaned config files on initial scan - #110
Conversation
A recreated VIRTUAL_HOST container could keep a stale backend endpoint because its old per-container-ID config file survived when the die event was missed, and the initial scan only added files. HandleInitialScan now reconciles the dynamic directory against the running containers, removing config files this service owns whose container no longer exists. The prune matches only the generated <short-id>.yaml files and is skipped when any container fails to inspect, so it never deletes a live route. Closes: sparkfabrik#109 Assisted-by: claude-code/claude-opus-4-8
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
…parkfabrik#109 Reproduce the missed die event scenario in the integration suite: stop dinghy_layer, recreate a VIRTUAL_HOST container, restart dinghy_layer, and assert the orphaned config file is pruned while the recreated container config and shared files survive. Refs: sparkfabrik#109 Assisted-by: claude-code/claude-fable-5
paolomainardi
left a comment
There was a problem hiding this comment.
🤖 This was written by an AI agent on behalf of @paolomainardi.
Review summary
The fix is correct and narrowly scoped. Verified against the code:
- No concurrency race: the event loop runs the initial scan to completion before subscribing to Docker events, so reconciliation cannot delete a config written by a concurrent start event.
- Prune scope is safe: the
^[0-9a-f]{12}\.yaml$pattern matches only files generated by this service.auto-tls.yml, certificate configs, andmiddlewares/are untouched. - The scan-error guard is conservative: any inspect or write failure skips reconciliation entirely, so a transient failure cannot prune a live route.
- Swallowing
reconcileConfigserrors is right, since returning them would kill the service on a filesystem hiccup.
End-to-end verification
Reproduced the bug and verified the fix locally, and added an e2e test to the integration suite (commit 2c5446b):
- On
main, the orphaned config file survives a dinghy-layer restart (bug reproduced). - On this branch, the restart prunes the orphan and the recreated container routes correctly.
- Full
make testsuite passes (7/7) including the new case, andgo test -race ./...is green.
Non-blocking follow-up idea
After a Docker daemon restart the event stream resubscribes with a 5s backoff; a die event lost in that window recreates an orphan until the next restart. Running reconciliation after each event-stream reconnect would close that gap.
User description
What this fixes
A container with
VIRTUAL_HOSTthat is recreated with a new IP could keep a stale backend endpoint in its Traefik route. The URL then returned502while the backend was healthy, and no proxy or container restart reconciled it. Only a full teardown that removed thetraefik_dynamicvolume cleared it.Closes #109.
Root cause
dinghy-layerwrites one Traefik dynamic config file per container, named by the container ID, and removes that file only on the containerdieevent. The Traefik service inside each file is keyed by the container name, not the ID.HandleInitialScanwrites a file for each running managed container but never prunes. When adieevent is missed (for exampledinghy-layerwas not running at the moment the old container was removed), the old<id>.yamlsurvives as an orphan. Two files then define the same Traefik service name, the file provider merges them, and the stale endpoint can win. A restart does not help, because the scan only adds files, so only wiping the volume cleared the orphan.The change
HandleInitialScannow reconciles the dynamic directory against the running containers. It collects the config file name of every container it processes into akeepset, then calls a newreconcileConfigsthat removes any config file this service owns whose container is no longer present.The prune is deliberately narrow.
^[0-9a-f]{12}\.yaml$, the 12-character short container ID plus.yaml. Certificate configs, the entrypoint-generatedauto-tls.yml, and themiddlewares/subdirectory sharing the volume are left untouched.keep, so pruning in that state could delete a live route. The scan reconciles only when it saw every container cleanly; otherwise the next restart heals the orphan.processContainernow returns the base name of the file it wrote (empty when it skips the container), which is what feeds thekeepset.This makes
spark-http-proxy restartrecover a stale state that previously needed a full teardown.Verification
go test -race ./...passes. New unit tests cover the file-name matcher (TestIsDinghyConfigFile) and the reconcile behavior: an orphan is removed while the current file and the protectedauto-tls.yml, certificate, andmiddlewares/entries survive (TestReconcileConfigsRemovesOnlyOrphans), dry-run keeps files, and a missing directory is a no-op.gofmt -l ./cmd ./pkgandgo vet ./...are clean.make test(Docker integration suite) passes locally.PR Type
Bug fix, Tests, Documentation
Description
Prune orphaned
dinghy-layerconfigs after scansPreserve shared Traefik files and directories
Skip pruning when container inspection fails
Add reconciliation and filename matcher coverage
Diagram Walkthrough
File Walkthrough
main.go
Reconcile stale generated Traefik container configurationscmd/dinghy-layer/main.go
.yamlorphan files.main_test.go
Test orphaned configuration reconciliation safeguardscmd/dinghy-layer/main_test.go
CHANGELOG.md
Document stale backend configuration fixCHANGELOG.md
#109and affected502behavior.