You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A transient host-port conflict permanently degrades $OBOL_CONFIG_DIR/k3d.yaml. Once obol stack up falls back to ephemeral ingress ports, the stack never returns to 80/8080 — not on restart, not on cluster recreation — even after the conflicting process is long gone. The only recovery is obol stack init --force.
It also silently drops the stack from four ingress mappings to two.
Impact
Every user-facing instruction, doc, and the obol stack up epilogue itself says to visit http://obol.stack:8080. After this fires, that URL is wrong forever and the stack answers on a random high port instead. A user who ran two stacks once — or had anything on port 80 during a single stack up — is left permanently off the documented URL with no obvious way back, because k3d.yaml is not a file most people know to inspect.
Observed in practice: an orphaned older k3d cluster held 80/8080, so a new stack came up on 56854. After deleting the orphan and recreating the cluster, the new cluster still bound 56854 — the ports were free, but the defaults were no longer in the config to be reconsidered.
Root cause
rewriteConflictingPorts (internal/stack/backend_k3d.go:289) is one-directional. It removes conflicting mappings and appends an ephemeral fallback, but never restores a default mapping once the port frees up:
ensureK3dPortsAvailable (internal/stack/backend_k3d.go:439) then persists the result on every stack up, and its own doc comment frames it as handling drift between init and up — but the drift only ever flows one way:
updated:=stripConflictingPorts(original, u, false, "")
ifupdated!=original {
os.WriteFile(configPath, []byte(updated), 0o600)
}
The ratchet:
stack up with 80/8080 occupied → both 80:80 and 8080:80 blocks are deleted; hasMapping[80] is false, so pickPort() appends an ephemeral 56854:80. Same for 443. Persisted.
Ports free up. Next stack up reads a config that now contains only56854:80. That port is available, so hasMapping[80] = true and nothing changes. The defaults are never reconsidered — they are no longer in the file to be checked.
The 4→2 reduction falls out of the same loop: the template ships 80:80, 8080:80, 443:443, 8443:443, but the recovery path adds at most one mapping per container port.
Reproduction
# occupy the default ingress ports (e.g. a second k3d cluster, or any listener)
obol stack up # → "Default ingress ports are in use — use http://obol.stack:56854 instead"
grep -A1 "port:"$OBOL_CONFIG_DIR/k3d.yaml
# - port: 56854:80# - port: 56855:443 # 8080:80 and 8443:443 are gone# free the ports, then fully recreate the cluster
k3d cluster delete <the-other-cluster>
k3d cluster delete obol-stack-<id>
obol stack up
docker port k3d-obol-stack-<id>-serverlb
# 80/tcp -> 0.0.0.0:56854 # still the fallback, ports 80/8080 are free and unused
Recovery, currently the only way:
obol stack init --force # preserves the stack ID, regenerates all four mappings
obol stack up
Suggested fix
Make the rewrite reconsider defaults each run instead of mutating a shrinking config. Concretely: derive the port mappings from the embedded template on every up, apply conflict-stripping to that, and treat the on-disk k3d.yaml as derived state rather than the source of truth for ports. Then a freed port is automatically reclaimed on the next stack up.
Two smaller pieces worth fixing alongside:
Preserve the secondary mappings — after a conflict the stack should still get both 8080-style and primary mappings where the ports allow, not collapse to one per container port.
The fallback warning is printed once, at the moment of the conflict. Since the condition is now sticky, obol stack up should keep surfacing why it is not on :8080 on subsequent runs, and say how to reset.
Notes
--force already has the right concept via k3dOwnedHostPorts (a cluster's own ports are not foreign conflicts); the gap is specifically that freed foreign ports are never re-acquired.
Summary
A transient host-port conflict permanently degrades
$OBOL_CONFIG_DIR/k3d.yaml. Onceobol stack upfalls back to ephemeral ingress ports, the stack never returns to80/8080— not on restart, not on cluster recreation — even after the conflicting process is long gone. The only recovery isobol stack init --force.It also silently drops the stack from four ingress mappings to two.
Impact
Every user-facing instruction, doc, and the
obol stack upepilogue itself says to visithttp://obol.stack:8080. After this fires, that URL is wrong forever and the stack answers on a random high port instead. A user who ran two stacks once — or had anything on port 80 during a singlestack up— is left permanently off the documented URL with no obvious way back, becausek3d.yamlis not a file most people know to inspect.Observed in practice: an orphaned older k3d cluster held
80/8080, so a new stack came up on56854. After deleting the orphan and recreating the cluster, the new cluster still bound56854— the ports were free, but the defaults were no longer in the config to be reconsidered.Root cause
rewriteConflictingPorts(internal/stack/backend_k3d.go:289) is one-directional. It removes conflicting mappings and appends an ephemeral fallback, but never restores a default mapping once the port frees up:ensureK3dPortsAvailable(internal/stack/backend_k3d.go:439) then persists the result on everystack up, and its own doc comment frames it as handling drift betweeninitandup— but the drift only ever flows one way:The ratchet:
stack upwith80/8080occupied → both80:80and8080:80blocks are deleted;hasMapping[80]is false, sopickPort()appends an ephemeral56854:80. Same for443. Persisted.stack upreads a config that now contains only56854:80. That port is available, sohasMapping[80] = trueand nothing changes. The defaults are never reconsidered — they are no longer in the file to be checked.The 4→2 reduction falls out of the same loop: the template ships
80:80,8080:80,443:443,8443:443, but the recovery path adds at most one mapping per container port.Reproduction
Recovery, currently the only way:
obol stack init --force # preserves the stack ID, regenerates all four mappings obol stack upSuggested fix
Make the rewrite reconsider defaults each run instead of mutating a shrinking config. Concretely: derive the port mappings from the embedded template on every
up, apply conflict-stripping to that, and treat the on-diskk3d.yamlas derived state rather than the source of truth for ports. Then a freed port is automatically reclaimed on the nextstack up.Two smaller pieces worth fixing alongside:
8080-style and primary mappings where the ports allow, not collapse to one per container port.obol stack upshould keep surfacing why it is not on:8080on subsequent runs, and say how to reset.Notes
--forcealready has the right concept viak3dOwnedHostPorts(a cluster's own ports are not foreign conflicts); the gap is specifically that freed foreign ports are never re-acquired.