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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

### Fixed

- `setup` now installs apple/container's recommended guest kernel when no default is configured.
apple/container cannot boot any VM without one, so on a fresh install — or one whose user data
was deleted, which `cleanup --all -d` does by design — every sandbox create failed at image
unpack with `default kernel not configured for architecture arm64`. The step is skipped when a
kernel is already set, and a failed download only warns (with the retry command) so `setup`
still repairs the rest of the wiring.
- `update --all` now stops the container runtime before running apple/container's updater and
restarts it afterwards. The updater refuses to run while the runtime is up, so this step
previously always failed with "`container` is still running".

## [0.2.5] - 2026-08-02

### Fixed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ source of truth. See [docs/architecture.md](docs/architecture.md).
| anything looks broken | re-run `openshell-driver-applecontainer setup` — it repairs all wiring |
| `openshell status` fails | `brew services info openshell`; gateway log: `/opt/homebrew/var/log/openshell/openshell-gateway.err.log` |
| sandbox stuck / failed | driver log: `~/Library/Logs/openshell-driver-applecontainer.log`; failed sandboxes carry the guest console tail in their status |
| `default kernel not configured for architecture` | apple/container has no guest kernel (a fresh install, or one whose data was deleted). `setup` installs one automatically; if its download failed, retry `container system kernel set --recommended` |
| slow first create | the base image (~2.6 GB) is pulling; `setup` without `--no-pull` pre-pulls it |

## Configuration reference
Expand Down
10 changes: 10 additions & 0 deletions cmd/openshell-driver-applecontainer/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,20 @@ func updatePrerequisites(log *slog.Logger) {
}
const acUpdater = "/usr/local/bin/update-container.sh"
if _, err := os.Stat(acUpdater); err == nil {
// The updater refuses to run while the runtime is up ("`container` is
// still running"), so stop it first — same as the uninstall path does.
if err := streamCmd("container", "system", "stop"); err != nil {
log.Debug("container system stop", "err", err)
}
log.Info("update: updating apple/container (its updater needs sudo)")
if err := streamCmd(acUpdater); err != nil {
log.Warn("apple/container updater failed", "err", err)
}
// Bring the runtime back up; the driver needs it. (setup would also
// start it, but --no-setup must not leave it stopped.)
if err := streamCmd("container", "system", "start"); err != nil {
log.Warn("could not restart the container runtime; run `container system start`", "err", err)
}
}
}

Expand Down
34 changes: 34 additions & 0 deletions internal/hostsetup/cleanup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,40 @@ func TestCleanupAllDeleteDataPassesDeleteFlag(t *testing.T) {
}
}

// A fresh apple/container install (or one whose user data was deleted by
// `cleanup --all -d`) has no default kernel, and then every sandbox create
// fails at image unpack. setup must install one.
func TestEnsureKernelInstallsWhenMissing(t *testing.T) {
rec := &cmdRec{}
s := newCleanupSetup(t, rec, true) // Home is a temp dir: no kernel present

s.ensureKernel()

if !rec.ran("container system kernel set --recommended") {
t.Errorf("expected the recommended kernel to be installed; calls=%v", rec.calls)
}
}

// When a default kernel is already configured, setup must not re-download it.
func TestEnsureKernelSkipsWhenPresent(t *testing.T) {
rec := &cmdRec{}
s := newCleanupSetup(t, rec, true)

kernelPath := s.defaultKernelPath()
if err := os.MkdirAll(filepath.Dir(kernelPath), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(kernelPath, []byte("kernel"), 0o644); err != nil {
t.Fatal(err)
}

s.ensureKernel()

if rec.ran("kernel set") {
t.Errorf("a configured kernel must not be reinstalled; calls=%v", rec.calls)
}
}

// With OpenShell absent, cleanup skips the gateway/brew steps but still
// removes the driver service.
func TestCleanupWithoutOpenShell(t *testing.T) {
Expand Down
37 changes: 37 additions & 0 deletions internal/hostsetup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -113,6 +114,12 @@ func (s *Setup) Run(ctx context.Context, opts Options) error {
}
}

// 1b. Default guest kernel. apple/container cannot boot any VM without
// one, and a fresh install (or one whose user data was deleted, e.g. by
// `cleanup --all -d`) ships without it — every sandbox create would fail
// with "default kernel not configured for architecture".
s.ensureKernel()

// 2. vmnet network + guest-reachable gateway address.
gatewayIP, err := s.ensureNetwork(ctx, opts.Network)
if err != nil {
Expand Down Expand Up @@ -326,6 +333,36 @@ func (s *Setup) removePrerequisites(opts CleanupOptions) {
}
}

// defaultKernelPath is where apple/container records the default guest kernel
// for this architecture (a symlink into its kernels directory).
func (s *Setup) defaultKernelPath() string {
return filepath.Join(s.Home, "Library", "Application Support", "com.apple.container",
"kernels", "default.kernel-"+runtime.GOARCH)
}

// ensureKernel installs apple/container's recommended guest kernel when no
// default is configured. Without it every sandbox create fails at image
// unpack with "default kernel not configured for architecture".
//
// This is deliberately non-fatal: setup is the documented repair command, so a
// transient download failure must not stop it from fixing the rest of the
// wiring. It warns with the exact manual fallback instead.
func (s *Setup) ensureKernel() {
if _, err := os.Lstat(s.defaultKernelPath()); err == nil {
return
}
s.Log.Info("setup: no default guest kernel configured; installing the recommended one (one-time, ~600 MB)")
err := s.ExecStream("container", "system", "kernel", "set", "--recommended")
if err == nil {
s.Log.Info("setup: default guest kernel installed")
return
}
s.Log.Warn("setup: could not install the recommended guest kernel — SANDBOXES WILL NOT BOOT until one is set",
"err", err,
"retry", "container system kernel set --recommended",
"fallback", "download the kata-static arm64 tarball with curl, then: container system kernel set --arch arm64 --tar <file> --binary opt/kata/share/kata-containers/vmlinux-<version>")
}

func (s *Setup) ensureNetwork(ctx context.Context, name string) (string, error) {
find := func() (string, error) {
networks, err := s.RT.Networks(ctx)
Expand Down