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)