From 207a3eb6f799e3640b7070201504dbf91b5e2e87 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 16 Sep 2026 17:24:11 +0200 Subject: [PATCH] test(e2e): reproduce --project-directory ignored on an oci:// project TestOciRemoteProjectDirectory publishes a project whose only service mounts a relative volume, then runs `up` on the oci:// artifact with an explicit --project-directory pointing elsewhere. The relative volume must resolve against that directory. It currently fails against the pinned compose-go: LoadConfigFiles defaults the working dir to the downloaded artifact's own cache directory whenever a remote resource loader is involved, silently overriding the explicit --project-directory the same way it would a mere default -- docker/compose#14224. On Docker Desktop this surfaces as a hard failure (the cache directory isn't a shared mount); on Linux it silently mounts the wrong directory, matching the original report. New BindMountSource check (pkg/e2e/checks.go) pins a service's bind mount source to an exact expected path, for tests that need to verify which working directory a relative volume path resolved against. Needs the compose-go fix (github.com/ndeloof/compose-go@a626c70, branch 14224-working-dir) to pass -- not yet reflected in go.mod here. Signed-off-by: Nicolas De Loof --- pkg/e2e/checks.go | 48 +++++++++++++++++++ pkg/e2e/remote_oci_test.go | 28 +++++++++++ .../compose.yaml | 7 +++ 3 files changed, 83 insertions(+) create mode 100644 pkg/e2e/testdata/TestOciRemoteProjectDirectory/compose.yaml diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index 55ceb2c50..7e061136b 100644 --- a/pkg/e2e/checks.go +++ b/pkg/e2e/checks.go @@ -27,6 +27,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "regexp" "runtime" "slices" @@ -553,6 +554,53 @@ func ContainerEnv(service, name, value string) Check { } } +// BindMountSource expects the service's bind mount at target to resolve to +// exactly wantSource — pinning down which working directory a relative +// volume path was resolved against (e.g. an explicit --project-directory, +// as opposed to wherever the compose file itself was loaded from). +func BindMountSource(service, target, wantSource string) Check { + return Check{ + name: fmt.Sprintf("service %q mount %q resolves to %s", service, target, wantSource), + fn: func(ctx *CheckContext) error { + containers := ctx.curr.service(service) + if len(containers) == 0 { + return errors.New("service has no container") + } + wantAbs, err := filepath.Abs(wantSource) + if err != nil { + return err + } + for _, c := range containers { + res := icmd.RunCmd(ctx.scenario.cli.NewDockerCmd(ctx.scenario.t, "inspect", "--format", "{{json .Mounts}}", c.ID)) + if res.ExitCode != 0 { + return fmt.Errorf("inspect failed: %s", res.Combined()) + } + var mounts []struct { + Destination string + Source string + } + if err := json.Unmarshal([]byte(strings.TrimSpace(res.Stdout())), &mounts); err != nil { + return err + } + found := false + for _, m := range mounts { + if m.Destination != target { + continue + } + found = true + if m.Source != wantAbs { + return fmt.Errorf("container %s mount %s resolves to %s, want %s", c.Name, target, m.Source, wantAbs) + } + } + if !found { + return fmt.Errorf("container %s has no mount at %s", c.Name, target) + } + } + return nil + }, + } +} + // LabelSet expects every container of the service to carry a non-empty label. func LabelSet(service, key string) Check { return Check{ diff --git a/pkg/e2e/remote_oci_test.go b/pkg/e2e/remote_oci_test.go index f06c3a276..fed5117bf 100644 --- a/pkg/e2e/remote_oci_test.go +++ b/pkg/e2e/remote_oci_test.go @@ -91,3 +91,31 @@ func TestOciRemoteTagSelection(t *testing.T) { ServiceState("app", "running"), ContainerEnv("app", "FLAVOR", "v1")) } + +// TestOciRemoteProjectDirectory is the docker/compose#14224 repro: an +// explicit --project-directory must resolve a relative volume path against +// itself, not against the local copy compose downloaded the oci:// artifact +// into (its self-contained-artifact default, otherwise correct for extends +// and bundled env files, but not meant to override an explicit request). +func TestOciRemoteProjectDirectory(t *testing.T) { + s := NewScenario(t, "--project-directory must resolve a relative volume path for an oci:// project, not the artifact's own download directory") + registry := startLocalRegistry(t, s) + ref := registry + "/remote-project-directory:v1" + s.Env("XDG_CACHE_HOME=" + t.TempDir()) + s.Step("publish pushes the project to the registry", + ComposeCmd("publish", "--yes", "--insecure-registry", ref)) + + // distinct from both the anchored testdata copy and the artifact's own + // download cache, so a bind source under it can only come from + // --project-directory being honored + projectDir := t.TempDir() + if err := os.Mkdir(filepath.Join(projectDir, "data"), 0o755); err != nil { + t.Fatal(err) + } + + s.FromRemote("oci://"+ref, "--insecure-registry", registry, "--project-directory", projectDir) + s.Step("up resolves the relative volume against --project-directory", + ComposeCmd("up", "-d", "--wait", "--yes").Within(60*time.Second), + ServiceState("app", "running"), + BindMountSource("app", "/data", filepath.Join(projectDir, "data"))) +} diff --git a/pkg/e2e/testdata/TestOciRemoteProjectDirectory/compose.yaml b/pkg/e2e/testdata/TestOciRemoteProjectDirectory/compose.yaml new file mode 100644 index 000000000..19167b292 --- /dev/null +++ b/pkg/e2e/testdata/TestOciRemoteProjectDirectory/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + init: true + command: sleep infinity + volumes: + - ./data:/data