diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index 55ceb2c504..60e82e2072 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,59 @@ func ContainerEnv(service, name, value string) Check { } } +// BindMountSource expects the service's bind mount at target to resolve to +// exactly wantSource (which must be absolute — this check pins down which +// working directory a relative volume path was resolved against, e.g. an +// explicit --project-directory, so a relative wantSource here would silently +// compare against the wrong base: the test process's own cwd, not any +// project directory). +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 { + if !filepath.IsAbs(wantSource) { + return fmt.Errorf("BindMountSource: wantSource must be absolute, got %q", wantSource) + } + containers := ctx.curr.service(service) + if len(containers) == 0 { + return errors.New("service has no container") + } + 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 { + Type string + 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.Type != "bind" { + continue + } + if m.Destination != target { + continue + } + found = true + if m.Source != wantSource { + return fmt.Errorf("container %s mount %s resolves to %s, want %s", c.Name, target, m.Source, wantSource) + } + break + } + 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 f06c3a2763..fed5117bf2 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 0000000000..19167b292a --- /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