Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions applecontainer-bridge/Sources/ACBridge/lifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ private func runContainer(spec: RunSpecJSON) async throws {
var cfg = ContainerConfiguration(id: spec.id, image: img.description, process: process)
cfg.platform = platform
cfg.labels = spec.labels ?? [:]
cfg.mounts = try (spec.mounts ?? []).map(toFilesystem)
var resolvedMounts: [Filesystem] = []
for m in spec.mounts ?? [] {
resolvedMounts.append(try await toFilesystem(m))
}
cfg.mounts = resolvedMounts
cfg.capAdd = spec.capAdd ?? []
cfg.useInit = spec.initProcess ?? false
// Enable Rosetta when running an amd64 container on an arm64
Expand Down Expand Up @@ -232,7 +236,7 @@ private func imageConfigUser(_ cfg: ImageConfig?) -> ProcessConfiguration.User {
return .id(uid: 0, gid: 0)
}

private func toFilesystem(_ m: MountJSON) throws -> Filesystem {
private func toFilesystem(_ m: MountJSON) async throws -> Filesystem {
var options: MountOptions = []
if m.readOnly ?? false {
options.append("ro")
Expand All @@ -246,13 +250,23 @@ private func toFilesystem(_ m: MountJSON) throws -> Filesystem {
case "tmpfs":
return .tmpfs(destination: m.target, options: options)
case "volume":
// PR-C scope decision: treat named volumes as virtiofs binds
// against the supplied source. Real named-volume support (with
// the apiserver pre-creating the volume) is a later PR.
guard let src = m.source, !src.isEmpty else {
throw BridgeError.invalidArgument("volume mount on this backend currently requires source (named-volume support is deferred)")
// Named volume: source carries the volume name. Resolve it
// through ClientVolume.inspect to fetch the backing image
// path + filesystem format the apiserver expects, then build
// a proper Filesystem.volume. Treating the spec as a virtiofs
// bind (PR-C's interim shape) makes the apiserver resolve the
// name against CWD and fail with errno 2 at bootstrap.
guard let name = m.source, !name.isEmpty else {
throw BridgeError.invalidArgument("named volume mount requires source (volume name)")
}
return .virtiofs(source: src, destination: m.target, options: options)
let vol = try await ClientVolume.inspect(name)
return .volume(
name: vol.name,
format: vol.format,
source: vol.source,
destination: m.target,
options: options
)
default:
throw BridgeError.invalidArgument("unknown mount type \"\(m.type)\"")
}
Expand Down
73 changes: 73 additions & 0 deletions runtime/applecontainer/lifecycle_darwin_arm64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package applecontainer
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -176,6 +178,77 @@ func TestRunContainer_BindMount(t *testing.T) {
}
}

// TestRunContainer_NamedVolume creates a container with a named volume
// mount and verifies the inspect path reports a MountVolume entry
// pointing at the volume's backing image (not a virtiofs bind against
// the launcher CWD). Regression guard for the bug where named volumes
// fell through to virtiofs and bootstrapped with errno 2.
func TestRunContainer_NamedVolume(t *testing.T) {
rt := runtimeOrSkip(t)
ctx := context.Background()
const (
id = "ac-namedvol-test"
volume = "ac-namedvol-test-vol"
)
_ = rt.RemoveContainer(ctx, id, runtime.RemoveOptions{Force: true})
cliRun(t, "volume", "rm", volume)
t.Cleanup(func() {
_ = rt.RemoveContainer(ctx, id, runtime.RemoveOptions{Force: true})
cliRun(t, "volume", "rm", volume)
})

cliRunStrict(t, "volume", "create", volume)
cliRunStrict(t,
"run", "--rm", "--name", "ac-alpine-warmup-vol",
"docker.io/library/alpine:latest", "/bin/true",
)

_, err := rt.RunContainer(ctx, runtime.RunSpec{
Image: "docker.io/library/alpine:latest",
Name: id,
Cmd: []string{"sleep", "60"},
Mounts: []runtime.MountSpec{
{Type: runtime.MountVolume, Source: volume, Target: "/mnt/data"},
},
})
if err != nil {
t.Fatalf("RunContainer with named volume: %v", err)
}

details, err := rt.InspectContainer(ctx, id)
if err != nil {
t.Fatalf("InspectContainer: %v", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd: %v", err)
}
cwd = filepath.Clean(cwd)
var found bool
for _, m := range details.Mounts {
if m.Target == "/mnt/data" && m.Type == string(runtime.MountVolume) {
found = true
// The apiserver substitutes the volume's on-disk image
// path as the canonical source. We don't pin the exact
// path (changes across apple/container releases) but it
// must not be empty and must not be the launcher CWD —
// the original bug had a non-empty source rooted at the
// launcher's working directory (apple's virtiofs path
// resolution), so empty-source alone wouldn't catch it.
if m.Source == "" {
t.Errorf("named volume mount has empty source; expected backing path")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if strings.HasPrefix(filepath.Clean(m.Source), cwd+string(filepath.Separator)) {
t.Errorf("named volume mount source rooted at launcher CWD: source=%q cwd=%q", m.Source, cwd)
}
}
}
if !found {
t.Errorf("named volume mount not round-tripped; details.Mounts=%+v want target=/mnt/data type=volume",
details.Mounts)
}
}

// waitForState polls InspectContainer until the desired state is
// observed or the timeout fires. Apple's runtime status transitions
// asynchronously through the apiserver event loop.
Expand Down
Loading