zdd plan 2 on main: adapt to instancectl c.do's header return (0bb57a31) and #256 D's WorkingDirectory (b8941639) for the generation template
The generation template's %I is <id>:<n>, so the instance's data directory comes from the per-generation runtime drop-in (keyed by host id, like the slice and data mounts). The template's one WorkingDirectory=/data/instances applies only if the drop-in did not, and the control service refuses the start before that (drop-in slice check); /data is an empty tmpfs there, so the chdir fails closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+13
−4
internal/host/units/carlos-instance-gen@.service+7 −0internal/instancectl/client.go+4 −4internal/instancectl/gen.go+1 −0internal/instancectl/server_gen_test.go+1 −0
CI passed — run details
diff --git a/internal/host/units/carlos-instance-gen@.service b/internal/host/units/carlos-instance-gen@.service| index 9a40e4f92..cf30f3330 100644 |
| --- a/internal/host/units/carlos-instance-gen@.service |
| +++ b/internal/host/units/carlos-instance-gen@.service |
| @@ -19,6 +19,13 @@ After=carlos-edge.service data-instances.mount run-carlos-instances.mount run-ca |
| [Service] |
| Type=simple |
| ExecStart=/opt/carlos/carlos instance-exec run %I |
| +# The tenant runs in its own data directory (#256 D). %I here is |
| +# <id>:<n>, not the host's id, so the real directory comes from the |
| +# generation's runtime drop-in, which overrides this line. This one only |
| +# applies if the drop-in did not, and then the start is already refused |
| +# (the control service checks the drop-in's slice first), and /data is an |
| +# empty tmpfs, so the chdir fails closed. |
| +WorkingDirectory=/data/instances |
| ExecStopPost=/opt/carlos/carlos instance-exit run %I |
| User=carlos |
| Group=carlos |
diff --git a/internal/instancectl/client.go b/internal/instancectl/client.go| index b954b7acd..9d495efd5 100644 |
| --- a/internal/instancectl/client.go |
| +++ b/internal/instancectl/client.go |
| @@ -184,7 +184,7 @@ func (c *Client) StartGen(ctx context.Context, host string, n uint64, memoryMax |
| q := genValues(host, n) |
| q.Set("memory_max", strconv.FormatInt(memoryMax, 10)) |
| var out StartResponse |
| - if status, err := c.do(ctx, DeadlineControl, http.MethodPost, "/start", q, &out); err != nil { |
| + if status, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/start", q, &out); err != nil { |
| if status == statusGenUnproven { |
| return "", fmt.Errorf("%w: %w", ErrGenUnproven, err) |
| } |
| @@ -200,12 +200,12 @@ func (c *Client) StopGen(ctx context.Context, host string, n uint64, kill bool) |
| if kill { |
| q.Set("kill", "1") |
| } |
| - _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/stop", q, nil) |
| + _, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/stop", q, nil) |
| return err |
| } |
| func (c *Client) ResetGen(ctx context.Context, host string, n uint64) error { |
| - _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/reset", genValues(host, n), nil) |
| + _, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/reset", genValues(host, n), nil) |
| return err |
| } |
| @@ -213,7 +213,7 @@ func (c *Client) ResetGen(ctx context.Context, host string, n uint64) error { |
| // §7.3): one tier, or two summed during an overlap. |
| func (c *Client) Parent(ctx context.Context, host string, memoryMax int64) error { |
| q := url.Values{"host": {host}, "memory_max": {strconv.FormatInt(memoryMax, 10)}} |
| - _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/parent", q, nil) |
| + _, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/parent", q, nil) |
| return err |
| } |
diff --git a/internal/instancectl/gen.go b/internal/instancectl/gen.go| index f06f188c3..0bdcd5315 100644 |
| --- a/internal/instancectl/gen.go |
| +++ b/internal/instancectl/gen.go |
| @@ -47,6 +47,7 @@ func GenDropIn(host string, n uint64, tier cgroup.Limit) string { |
| "Slice=" + SliceName(host) + "\n" + |
| "BindPaths=/data/instances/" + iid + " /run/carlos/instances/" + iid + "\n" + |
| "ReadWritePaths=/data/instances/" + iid + " /run/carlos/instances/" + iid + "\n" + |
| + "WorkingDirectory=/data/instances/" + iid + "\n" + |
| "BindReadOnlyPaths=/run/carlos-control/exe/" + key + "/app:/opt/carlos/app /run/carlos-control/exe/" + key + "/live:/opt/carlos/live\n" + |
| "StandardOutput=file:" + GenLogSocket(host, n) + "\n" + |
| "MemoryMax=" + max + "\n" + |
diff --git a/internal/instancectl/server_gen_test.go b/internal/instancectl/server_gen_test.go| index 88317469f..b0fd390cb 100644 |
| --- a/internal/instancectl/server_gen_test.go |
| +++ b/internal/instancectl/server_gen_test.go |
| @@ -40,6 +40,7 @@ func TestGenDropInText(t *testing.T) { |
| Slice=carlos-instances-bill\x2d2.example.com.slice |
| BindPaths=/data/instances/bill-2.example.com /run/carlos/instances/bill-2.example.com |
| ReadWritePaths=/data/instances/bill-2.example.com /run/carlos/instances/bill-2.example.com |
| +WorkingDirectory=/data/instances/bill-2.example.com |
| BindReadOnlyPaths=/run/carlos-control/exe/bill-2.example.com@7/app:/opt/carlos/app /run/carlos-control/exe/bill-2.example.com@7/live:/opt/carlos/live |
| StandardOutput=file:/run/carlos/logs/bill-2.example.com@7.run.sock |
| MemoryMax=536870912 |