From 7ef19ecaa683bc82c26d2ad6efc14b6f958eb699 Mon Sep 17 00:00:00 2001 From: Vyncint Ng Date: Sun, 2 Aug 2026 21:56:27 +0700 Subject: [PATCH] fix: install a default guest kernel in setup, stop the runtime for the updater MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps a full teardown-and-reinstall cycle exposed. apple/container cannot boot any VM until a default guest kernel is set, and a fresh install has none — nor does one whose user data was deleted, which `cleanup --all -d` does by design (it passes -d to apple/container's uninstaller, wiping its kernels directory). Every sandbox create then failed at image unpack with "default kernel not configured for architecture arm64", with nothing in setup's output hinting at the cause. setup now installs the recommended kernel when no default is configured, skips the step when one is, and only warns (with the retry command) if the download fails, so setup still repairs the rest of the wiring. `update --all` ran apple/container's updater while the runtime was up, which it refuses ("`container` is still running"), so that step always failed. Stop the runtime first, as the uninstall path already does, and restart it afterwards so --no-setup does not leave it down. Covers both kernel branches with tests and documents the symptom in the README troubleshooting table. Signed-off-by: Vyncint Ng --- CHANGELOG.md | 12 ++++++ README.md | 1 + cmd/openshell-driver-applecontainer/update.go | 10 +++++ internal/hostsetup/cleanup_test.go | 34 +++++++++++++++++ internal/hostsetup/setup.go | 37 +++++++++++++++++++ 5 files changed, 94 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b59fc4..1dbf5af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 326473d..3a4715c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/openshell-driver-applecontainer/update.go b/cmd/openshell-driver-applecontainer/update.go index 85ba57b..2ac73a8 100644 --- a/cmd/openshell-driver-applecontainer/update.go +++ b/cmd/openshell-driver-applecontainer/update.go @@ -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) + } } } diff --git a/internal/hostsetup/cleanup_test.go b/internal/hostsetup/cleanup_test.go index f3d1e58..6097462 100644 --- a/internal/hostsetup/cleanup_test.go +++ b/internal/hostsetup/cleanup_test.go @@ -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) { diff --git a/internal/hostsetup/setup.go b/internal/hostsetup/setup.go index 1fc8464..51c919c 100644 --- a/internal/hostsetup/setup.go +++ b/internal/hostsetup/setup.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" "time" @@ -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 { @@ -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 --binary opt/kata/share/kata-containers/vmlinux-") +} + func (s *Setup) ensureNetwork(ctx context.Context, name string) (string, error) { find := func() (string, error) { networks, err := s.RT.Networks(ctx)