Skip to content

Commit 3917a95

Browse files
fix(runtime): mount the project a composed service runs from in container mode
Every process the container environment runs is given the project directory as its working directory, but the environment was built over the workspace path alone. That carries the project only for a service authored in the workspace tree. A service a workspace *composes* is resolved into the module cache, or to wherever an overlay points — both outside the workspace — so the container started with a working directory that does not exist inside it: OCI runtime exec failed: exec failed: unable to start container process: chdir to cwd (".../services/<svc>/code") set in config.json failed: no such file or directory which the caller reports as `cannot run uv sync: process exited with code 127`, naming uv rather than the missing mount. No composed Python service could run under `--runtime-context container`; the native and Nix backends were unaffected, which is why this survived — the module cache has always been outside the workspace, so this is not new with overlays. The same file already gets it right at the image-runtime environment (`runtime.go:1165`, which passes `s.Service.SourceLocation`); the asymmetry inside one file was the tell. Classification: a fix, in the agent that owns the runner environment. The workspace mount stays for a service that lives in the workspace, and the project is mounted at its own path only when that mount does not already cover it, so nothing changes for a service the workspace holds. Testing: `go build`, `go vet`, `go mod tidy -diff`, `go test ./...` (81s, ok — the Docker-backed cases ran, `nix` skipped as it always does here), and `go test -race ./...`. The new test fails on `main` with exactly the 127 above and passes with the fix. Not verified: Kubernetes deployment paths, which this does not touch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5f9c11d commit 3917a95

2 files changed

Lines changed: 101 additions & 0 deletions

File tree

‎composed_project_test.go‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
"time"
10+
11+
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
12+
"github.com/codefly-dev/core/resources"
13+
"github.com/codefly-dev/core/shared"
14+
"github.com/codefly-dev/core/standards"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// TestContainerEnvironmentCarriesAProjectOutsideTheWorkspace pins the mount a
19+
// composed service depends on.
20+
//
21+
// A service the user authored lives in the workspace tree; a service a
22+
// workspace *composes* does not — it is resolved into the module cache, or to
23+
// wherever an overlay points. Both are outside the workspace path, and every
24+
// process this environment runs is given the project directory as its working
25+
// directory. Mounting only the workspace therefore produced a container whose
26+
// working directory did not exist inside it: "chdir to cwd ... no such file or
27+
// directory", surfacing as `uv sync` exiting 127 before the service ever
28+
// started.
29+
func TestContainerEnvironmentCarriesAProjectOutsideTheWorkspace(t *testing.T) {
30+
ownTestContainers(t)
31+
ctx := context.Background()
32+
33+
// Docker on macOS only bind-mounts shared paths, and the per-user temp dir
34+
// is not one — keep both trees inside the repo, as the other container
35+
// tests do.
36+
workspace, err := os.MkdirTemp("testdata", "composed-workspace-")
37+
require.NoError(t, err)
38+
t.Cleanup(func() { _ = os.RemoveAll(workspace) })
39+
workspace = shared.MustSolvePath(workspace)
40+
41+
source, err := os.MkdirTemp("testdata", "composed-project-")
42+
require.NoError(t, err)
43+
t.Cleanup(func() { _ = os.RemoveAll(source) })
44+
source = shared.MustSolvePath(source)
45+
require.NoError(t, os.MkdirAll(filepath.Join(source, "src"), 0o700))
46+
require.NoError(t, os.WriteFile(filepath.Join(source, "pyproject.toml"), []byte(probeProject), 0o600))
47+
require.NoError(t, os.WriteFile(filepath.Join(source, "src", "main.py"), []byte("app = None\n"), 0o600))
48+
49+
runtime := NewRuntime(NewService())
50+
runtime.Location = workspace
51+
runtime.Logger = runtime.Wool
52+
runtime.Base.Runtime.RuntimeContext = resources.NewRuntimeContextContainer()
53+
runtime.Identity = &resources.ServiceIdentity{
54+
Name: fmt.Sprintf("composed-%d", time.Now().UnixMilli()),
55+
Module: "mod",
56+
Workspace: "test",
57+
WorkspacePath: workspace,
58+
}
59+
runtime.Environment = shared.Must(resources.LocalEnvironment().Proto())
60+
runtime.Service.SourceLocation = source
61+
62+
endpoint := &basev0.Endpoint{Module: "mod", Service: runtime.Identity.Name, Name: "rest", Api: standards.REST}
63+
runtime.FastAPI.RestEndpoint = endpoint
64+
runtime.NetworkMappings = []*basev0.NetworkMapping{{
65+
Endpoint: endpoint,
66+
Instances: []*basev0.NetworkInstance{{
67+
Access: resources.NewNativeNetworkAccess(),
68+
Hostname: "localhost",
69+
Port: 9998,
70+
Address: "http://localhost:9998",
71+
}},
72+
}}
73+
74+
require.False(t, within(workspace, source),
75+
"the fixture must place the project outside the workspace, as a composed module is")
76+
77+
env, _, err := runtime.CreateRunnerEnvironment(ctx)
78+
if err != nil {
79+
t.Skipf("docker is not available: %v", err)
80+
}
81+
t.Cleanup(func() { _ = env.Shutdown(context.Background()) })
82+
require.NoError(t, env.Init(ctx))
83+
84+
proc, err := env.NewProcess("sh", "-c", "test -f pyproject.toml")
85+
require.NoError(t, err)
86+
proc.WithDir(source)
87+
require.NoError(t, proc.Run(ctx),
88+
"the composed project must exist at its own path inside the container")
89+
}

‎runtime.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ func (s *Runtime) CreateRunnerEnvironment(ctx context.Context) (runners.RunnerEn
255255
if err != nil {
256256
return nil, "", s.Wool.Wrapf(err, "cannot create docker runner")
257257
}
258+
// Every process this environment runs is given the project directory as
259+
// its working directory, so the project has to be inside a mount. The
260+
// workspace mount above carries it only for a service that lives in the
261+
// workspace tree. A composed service does not: it is resolved into the
262+
// module cache, or to wherever an overlay points, both outside the
263+
// workspace — and the container then starts with a working directory
264+
// that does not exist inside it ("chdir to cwd ... no such file or
265+
// directory", reported as `uv sync` exiting 127). Mount the project at
266+
// its own path when the workspace mount does not already cover it.
267+
if !within(s.Identity.WorkspacePath, s.Service.SourceLocation) {
268+
dockerEnv.WithMount(s.Service.SourceLocation, s.Service.SourceLocation)
269+
}
258270
dockerEnv.WithPause()
259271
// Run as the invoking host user. uv sync writes uv.lock into the
260272
// bind-mounted source and populates the venv; as root those files

0 commit comments

Comments
 (0)