layout: preflight refuses a sidecar env another route owns as its instance env (plan 4 final review)
A route named <h>-sidecar owns <h>-sidecar.env as its instance env, which h's plan would move as h's sidecar env. Refused in both directions, as the .config collision is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+38
−1
internal/layout/preflight.go+13 −1internal/layout/preflight_test.go+25 −0
diff --git a/internal/layout/preflight.go b/internal/layout/preflight.go| index 38593b5d2..e38983eac 100644 |
| --- a/internal/layout/preflight.go |
| +++ b/internal/layout/preflight.go |
| @@ -18,7 +18,8 @@ import ( |
| // rendered bundle UNLESS a route named <host>.config exists, in which case |
| // it is that route's instance env (SyncExec's own collision rule). Moving |
| // it with host would take another route's file; reverting onto it would |
| -// overwrite one. Either is a refusal naming both owners. |
| +// overwrite one. Either is a refusal naming both owners. <host>-sidecar.env |
| +// is shared the same way with a route named <host>-sidecar. |
| func Preflight(p *Plan, routes []registry.Route) error { |
| hosts := map[string]bool{} |
| for _, rt := range routes { |
| @@ -32,6 +33,17 @@ func Preflight(p *Plan, routes []registry.Route) error { |
| return fmt.Errorf("layout: %s: its env file %s.env is owned by both %s (its instance env) and %s (its bundle); refusing to move it", p.Host, p.Host, p.Host, owner) |
| } |
| } |
| + // The sidecar env collides the same way: <host>-sidecar.env is host's |
| + // sidecar env, and also the instance env of a route named |
| + // <host>-sidecar. |
| + if other := p.Host + "-sidecar"; hosts[other] { |
| + return fmt.Errorf("layout: %s: %s-sidecar.env is owned by both %s (its sidecar env) and %s (its instance env); refusing to move it", p.Host, p.Host, p.Host, other) |
| + } |
| + if strings.HasSuffix(p.Host, "-sidecar") { |
| + if owner := strings.TrimSuffix(p.Host, "-sidecar"); hosts[owner] { |
| + return fmt.Errorf("layout: %s: its env file %s.env is owned by both %s (its instance env) and %s (its sidecar env); refusing to move it", p.Host, p.Host, p.Host, owner) |
| + } |
| + } |
| // The activator binds a control socket beside every exec listener at |
| // the listener's path plus ".control". A nested address that leaves no |
| // room for it would make the host unwakeable after the move, so it is |
diff --git a/internal/layout/preflight_test.go b/internal/layout/preflight_test.go| index b066db997..bcbcd2b7b 100644 |
| --- a/internal/layout/preflight_test.go |
| +++ b/internal/layout/preflight_test.go |
| @@ -39,6 +39,31 @@ func TestPreflightRefusesAConfigEnvOwnedByAnotherRoute(t *testing.T) { |
| } |
| } |
| +// The same collision through the sidecar env: a route named <h>-sidecar |
| +// owns <h>-sidecar.env as its instance env, and h's plan would move it as |
| +// h's sidecar env. |
| +func TestPreflightRefusesASidecarEnvOwnedByAnotherRoute(t *testing.T) { |
| + routes := []registry.Route{{Host: "x.example.com"}, {Host: "x.example.com-sidecar"}} |
| + p, _ := PlanFor("/data", "/run/carlos", "x.example.com", ModeNested) |
| + err := Preflight(p, routes) |
| + if err == nil || !strings.Contains(err.Error(), "x.example.com-sidecar.env") || !strings.Contains(err.Error(), "x.example.com-sidecar (its instance env)") { |
| + t.Fatalf("ambiguous sidecar env accepted: %v", err) |
| + } |
| + if err := Preflight(p, []registry.Route{{Host: "x.example.com"}}); err != nil { |
| + t.Fatalf("unambiguous plan refused: %v", err) |
| + } |
| + r, _ := PlanFor("/data", "/run/carlos", "x.example.com", ModeFlat) |
| + if err := Preflight(r, routes); err == nil { |
| + t.Fatal("revert onto another route's sidecar env accepted") |
| + } |
| + // The other direction: the host named <owner>-sidecar has an env file |
| + // that is also <owner>'s sidecar env. |
| + c, _ := PlanFor("/data", "/run/carlos", "x.example.com-sidecar", ModeNested) |
| + if err := Preflight(c, routes); err == nil || !strings.Contains(err.Error(), "x.example.com (its sidecar env)") { |
| + t.Fatalf("an env file that is another route's sidecar env accepted: %v", err) |
| + } |
| +} |
| + |
| // A nested socket's control twin must fit a unix socket address. The |
| // socket dir here is long enough that the nested address overflows it. |
| func TestPreflightRefusesANestedSocketTooLongForItsControlTwin(t *testing.T) { |