EXPERIMENT: devkit - switch Alpine base to Ubuntu (noble)

Build the devkit debug extension from the same Ubuntu release the guest image
uses (assets.image ... version, e.g. noble) instead of Alpine. The rootfs is
assembled via 'docker build' + 'docker export' so apt resolves dependencies
natively, and the guest helper scripts wrap apt-get (devkit-apt) instead of apk.

Not for merge; experiment on branch experiment/devkit-ubuntu.
This commit is contained in:
Fabiano Fidêncio
2026-07-23 16:50:36 +02:00
parent ee2a8a2bca
commit d3f3714edd
6 changed files with 78 additions and 82 deletions

View File

@@ -8,7 +8,7 @@
# (kata-ctl exec <sandbox-id>) on the NVIDIA CPU runtime-rs class:
#
# * With the kata-<shim>-devkit RuntimeClass, the console drops into the rich
# Alpine-based devkit shell overlaid on the read-only guest rootfs.
# Ubuntu-based devkit shell overlaid on the read-only guest rootfs.
# * Without devkit (the base RuntimeClass), the shell-less NVIDIA base cannot
# spawn a console shell at all.
@@ -34,8 +34,9 @@ devkit_runtimeclass() {
# when a real shell runs it. The literal command text, even if echoed back by
# the PTY, never contains the expanded value - so "SHELL_OK=42" in the output is
# unambiguous proof that a debug console shell actually executed. GUEST_ID and
# apk then tell devkit (Alpine overlay) apart from the base rootfs.
DEVKIT_PROBE='echo "SHELL_OK=$((6*7))"; . /etc/os-release 2>/dev/null; echo "GUEST_ID=${ID}"; command -v apk >/dev/null 2>&1 && apk --version || true'
# the devkit-only devkit-apt wrapper then tell the devkit (Ubuntu) overlay apart
# from the base rootfs.
DEVKIT_PROBE='echo "SHELL_OK=$((6*7))"; . /etc/os-release 2>/dev/null; echo "GUEST_ID=${ID}"; command -v devkit-apt >/dev/null 2>&1 && echo "DEVKIT_TOOL=yes" || true'
check_and_skip() {
if ! devkit_supported; then
@@ -119,10 +120,10 @@ setup() {
echo "${output}" | grep -q 'SHELL_OK=42' \
|| die "devkit debug console did not provide a working shell"
echo "${output}" | grep -q 'GUEST_ID=alpine' \
|| die "devkit debug console did not report an Alpine guest"
echo "${output}" | grep -q 'apk-tools' \
|| die "devkit debug console shell lacks apk; not the devkit overlay"
echo "${output}" | grep -q 'GUEST_ID=ubuntu' \
|| die "devkit debug console did not report an Ubuntu guest"
echo "${output}" | grep -q 'DEVKIT_TOOL=yes' \
|| die "devkit debug console shell lacks devkit-apt; not the devkit overlay"
}
@test "Without devkit, the NVIDIA base debug console has no usable shell" {

View File

@@ -5,10 +5,11 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# Thin apk wrapper: runs Alpine's apk inside the devkit overlay/chroot, so
# `devkit-apk add htop` installs into the writable overlay at runtime.
# Thin apt wrapper: runs Ubuntu's apt-get inside the devkit overlay/chroot, so
# `devkit-apt update && devkit-apt install -y htop` installs into the writable
# overlay at runtime (an offline base ships no package lists, hence the update).
DEVKIT=/run/kata-extensions/devkit
# shellcheck source=tools/osbuilder/rootfs-builder/devkit/devkit-init.sh
. "${DEVKIT}/usr/bin/devkit-init"
devkit_chroot_exec /sbin/apk "$@"
devkit_chroot_exec /usr/bin/apt-get "$@"

View File

@@ -5,17 +5,16 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# Shared devkit guest runtime library, sourced by devkit-enter and devkit-apk.
# Shared devkit guest runtime library, sourced by devkit-enter and devkit-apt.
#
# The devkit extension (read-only at ${DEVKIT}) is a full minimal Alpine rootfs.
# We overlay a writable, exec-enabled tmpfs on it and chroot in, so apk and the
# The devkit extension (read-only at ${DEVKIT}) is a full minimal Ubuntu rootfs.
# We overlay a writable, exec-enabled tmpfs on it and chroot in, so apt and the
# prebaked tools run natively against a normal root filesystem.
#
# BB is Alpine's statically-linked busybox (busybox-static, /bin/busybox.static),
# the ONLY binary we can exec on the shell-less guest base before the musl loader
# exists. We deliberately do NOT reuse /bin/busybox: that is Alpine's dynamic
# busybox providing the chroot's coreutils applets, and clobbering it breaks
# ls/cat/ps and apk's busybox trigger.
# BB is a statically-linked busybox (busybox-static, /bin/busybox.static), the
# ONLY binary we can exec on the shell-less guest base before the glibc dynamic
# loader is reachable there. It is installed at a dedicated path so it never
# clobbers the rootfs's own /bin/busybox or GNU coreutils.
DEVKIT_INIT_VERSION=1
@@ -33,7 +32,7 @@ devkit_is_mounted() {
# Mount an exec-enabled tmpfs at ${WRITABLE}: /run is typically noexec, so the
# overlay upper/work (and the fallback rootfs copy) must live on our own tmpfs
# for the prebaked and apk-installed binaries to exec.
# for the prebaked and apt-installed binaries to exec.
devkit_mount_writable() {
"${BB}" mkdir -p "${WRITABLE}"
devkit_is_mounted "${WRITABLE}" && return 0
@@ -60,7 +59,7 @@ devkit_mount_root() {
"${BB}" touch "${WRITABLE}/.copied"
}
# Bind the kernel virtual filesystems so apk and the debug tools behave; /dev is
# Bind the kernel virtual filesystems so apt and the debug tools behave; /dev is
# rbind'd to bring in pts/shm for interactive shells.
devkit_bind_mounts() {
"${BB}" mkdir -p "${MERGED}/proc" "${MERGED}/sys" "${MERGED}/dev"
@@ -78,7 +77,7 @@ devkit_bind_mounts() {
fi
}
# Give apk/curl working DNS by importing the guest resolver config.
# Give apt/curl working DNS by importing the guest resolver config.
devkit_seed_resolv_conf() {
"${BB}" test -e /etc/resolv.conf || return 0
"${BB}" mkdir -p "${MERGED}/etc"

View File

@@ -307,7 +307,7 @@ debug: false
# Deploy the optional "devkit" debug guest extension and, per enabled shim, an
# extra `kata-<shim>-devkit` RuntimeClass wired to it. Pods on that class boot
# with the extension cold-plugged, so the agent debug console drops into a rich
# shell (Alpine + apk + debug tools) overlaid on the read-only guest.
# shell (Ubuntu + apt + debug tools) overlaid on the read-only guest.
#
# Debugging aid: only effective with `debug: true`, ignored otherwise. Leave it
# off in production and in confidential (CoCo/TEE) deployments.

View File

@@ -741,35 +741,35 @@ EOF
rm -rf "${extension_rootfs}"
}
# Debug tools prebaked so they work offline; `apk add <pkg>` covers anything else
# inside the chroot. busybox-static is the static /bin/busybox.static that
# bootstraps the overlay/chroot from the shell-less guest base (see
# Debug tools prebaked so they work offline; `devkit-apt install <pkg>` covers
# anything else inside the chroot. busybox-static is the static /bin/busybox.static
# that bootstraps the overlay/chroot from the shell-less guest base (see
# devkit-init.sh); pciutils and util-linux let the guest's devices and namespaces
# be inspected from the chroot. Kept lean: heavier tools are pulled on demand.
readonly devkit_debug_packages="busybox-static bash ca-certificates strace ltrace iproute2 procps psmisc lsof tcpdump curl wget file less netcat-openbsd bind-tools pciutils util-linux"
readonly devkit_debug_packages="busybox-static bash ca-certificates strace ltrace iproute2 procps psmisc lsof tcpdump curl wget file less netcat-openbsd dnsutils pciutils util-linux"
# Install the generic devkit debug extension image (erofs+verity): a self-contained
# minimal Alpine rootfs with debug tools prebaked. It ships no kata-agent, kernel
# minimal Ubuntu rootfs with debug tools prebaked. It ships no kata-agent, kernel
# or driver userspace, so - unlike the monolithic images - it is assembled here
# and handed to image_builder.sh (like install_image_coco_extension), never via
# rootfs.sh.
install_image_devkit_extension() {
local component="rootfs-image-devkit-extension"
local branch ver mirror
branch="$(get_from_kata_deps ".externals.alpine.branch")"
ver="$(get_from_kata_deps ".externals.alpine.version")"
mirror="$(get_from_kata_deps ".externals.alpine.mirror")"
[[ -n "${branch}" ]] || die "externals.alpine.branch must be set in versions.yaml"
[[ -n "${ver}" ]] || die "externals.alpine.version must be set in versions.yaml"
[[ -n "${mirror}" ]] || mirror="https://dl-cdn.alpinelinux.org/alpine"
# Reuse the guest image's Ubuntu release (e.g. "noble") so the devkit matches
# the base userspace ABI (glibc) the guest ships.
local os_name os_version
os_name="$(get_from_kata_deps ".assets.image.architecture.${ARCH}.name")"
os_version="$(get_from_kata_deps ".assets.image.architecture.${ARCH}.version")"
[[ "${os_name}" == "ubuntu" ]] || die "devkit extension expects an ubuntu base, got '${os_name}' for ${ARCH}"
[[ -n "${os_version}" ]] || die "assets.image.architecture.${ARCH}.version must be set in versions.yaml"
# Self-contained (Alpine release + guest scripts), so key the cache on the
# Alpine version and the devkit source directory.
# Self-contained (Ubuntu release + guest scripts), so key the cache on the
# Ubuntu version and the devkit source directory.
local devkit_last_commit
devkit_last_commit="$(git -C "${repo_root_dir}" log -1 --abbrev=9 --pretty=format:"%h" \
-- tools/osbuilder/rootfs-builder/devkit 2>/dev/null || echo "unknown")"
latest_artefact="$(get_kata_version)-devkit-extension-${ver}-${devkit_last_commit}"
latest_artefact="$(get_kata_version)-devkit-extension-${os_version}-${devkit_last_commit}"
latest_builder_image=""
install_cached_tarball_component \
@@ -787,60 +787,61 @@ install_image_devkit_extension() {
local extension_rootfs
extension_rootfs="$(mktemp -d "${repo_root_dir}/.devkit-extension-rootfs.XXXX")"
# Alpine's arch naming matches ${ARCH} (x86_64/aarch64).
local tarball url
tarball="alpine-minirootfs-${ver}-${ARCH}.tar.gz"
url="${mirror}/${branch}/releases/${ARCH}/${tarball}"
info "Fetching Alpine minirootfs ${url}"
curl -fsSL -o "${extension_rootfs}/../${tarball}" "${url}" \
|| die "failed to download Alpine minirootfs ${url}"
tar -xzf "${extension_rootfs}/../${tarball}" -C "${extension_rootfs}"
rm -f "${extension_rootfs}/../${tarball}"
# Pin repositories and seed a resolver so apk can fetch indexes and content.
mkdir -p "${extension_rootfs}/etc/apk"
cat > "${extension_rootfs}/etc/apk/repositories" <<-EOF
${mirror}/${branch}/main
${mirror}/${branch}/community
# Build an Ubuntu image with the debug toolset, then export its filesystem as
# the extension rootfs. Building the toolset via `docker build`/apt lets apt
# resolve dependencies normally (no chroot or --root gymnastics), and
# `docker export | tar` as the unprivileged build user yields a tree we own
# outright - no privileged sibling / chown dance needed.
info "Building devkit ubuntu rootfs (${os_name}:${os_version}) with debug toolset"
local build_tag="kata-devkit-ubuntu-${ARCH}:build"
docker build -t "${build_tag}" - <<-EOF || die "docker build for the devkit ubuntu rootfs failed"
FROM docker.io/library/ubuntu:${os_version}
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends ${devkit_debug_packages} \
&& rm -rf /var/lib/apt/lists/*
EOF
cp -L /etc/resolv.conf "${extension_rootfs}/etc/resolv.conf" 2>/dev/null || true
# The build container is unprivileged, so a direct chroot is denied. Run apk
# in a privileged sibling container instead (Docker-in-Docker uses host paths,
# hence the repo-root temp dir): apk installs into --root but still runs
# package scripts chrooted, which needs the sibling's privileges. apk writes
# as root, so chown the tree back afterwards or the trim/cleanup and
# image_builder steps below hit "Permission denied".
info "Installing devkit debug toolset with apk"
# shellcheck disable=SC2086
docker run --rm --privileged \
-v "${extension_rootfs}:${extension_rootfs}" \
"docker.io/library/alpine:${ver}" \
sh -c "/sbin/apk add --no-cache --root '${extension_rootfs}' ${devkit_debug_packages} \
&& chown -R $(id -u):$(id -g) '${extension_rootfs}'" \
|| die "apk failed to install the devkit debug toolset"
local cid
cid="$(docker create "${build_tag}")" || die "docker create for the devkit ubuntu rootfs failed"
# Skip /dev: docker export carries device nodes the unprivileged tar cannot
# recreate (devkit-init mounts /dev at runtime anyway).
docker export "${cid}" | tar -C "${extension_rootfs}" --exclude='dev/*' -xf - \
|| { docker rm -f "${cid}" >/dev/null 2>&1 || true; die "exporting the devkit ubuntu rootfs failed"; }
docker rm -f "${cid}" >/dev/null 2>&1 || true
docker rmi -f "${build_tag}" >/dev/null 2>&1 || true
# busybox-static ships a statically-linked busybox; expose it at the path
# devkit-init bootstraps from on the shell-less guest base.
local bb_src=""
local cand
for cand in usr/bin/busybox bin/busybox usr/sbin/busybox sbin/busybox; do
[[ -f "${extension_rootfs}/${cand}" ]] && { bb_src="${cand}"; break; }
done
[[ -n "${bb_src}" ]] || die "busybox-static not found in the devkit ubuntu rootfs"
cp -a "${extension_rootfs}/${bb_src}" "${extension_rootfs}/usr/bin/busybox.static"
# Install the guest-side helper scripts and expose the debug console entry as
# ${DEVKIT}/bin/devkit-sh. Do NOT reuse ${DEVKIT}/bin/sh: that is Alpine's own
# /bin/sh -> busybox symlink, needed unclobbered inside the chroot.
# devkit-sh (a sibling symlink to devkit-enter; Ubuntu's /bin is a symlink to
# /usr/bin, so /run/kata-extensions/devkit/bin/devkit-sh resolves here).
local devkit_src="${repo_root_dir}/tools/osbuilder/rootfs-builder/devkit"
local script dest
for script in devkit-init.sh devkit-enter.sh devkit-apk.sh; do
for script in devkit-init.sh devkit-enter.sh devkit-apt.sh; do
[[ -f "${devkit_src}/${script}" ]] || die "missing ${devkit_src}/${script}"
dest="${script%.sh}"
install -D -m0755 "${devkit_src}/${script}" "${extension_rootfs}/usr/bin/${dest}"
done
mkdir -p "${extension_rootfs}/bin"
ln -sf ../usr/bin/devkit-enter "${extension_rootfs}/bin/devkit-sh"
ln -sf devkit-enter "${extension_rootfs}/usr/bin/devkit-sh"
# Trim what a debug rootfs does not need (man/doc/info, apk cache) and the
# seeded resolver (devkit-init re-seeds one at runtime). Root inode must be
# 0755 (mktemp makes it 0700), else the mount point is unsearchable non-root.
# Trim what a debug rootfs does not need (man/doc/info, apt lists/cache) and
# the docker-seeded resolver (devkit-init re-seeds one at runtime). Root inode
# must be 0755 (mktemp makes it 0700), else the mount point is unsearchable.
rm -rf \
"${extension_rootfs}/usr/share/man"/* \
"${extension_rootfs}/usr/share/doc"/* \
"${extension_rootfs}/usr/share/info"/* \
"${extension_rootfs}/var/cache/apk"/* \
"${extension_rootfs}/var/lib/apt/lists"/* \
"${extension_rootfs}/var/cache/apt"/* \
"${extension_rootfs}/etc/resolv.conf" \
2>/dev/null || true
chmod 0755 "${extension_rootfs}"

View File

@@ -211,12 +211,6 @@ assets:
externals:
description: "Third-party projects used by the system"
alpine:
desc: "Alpine Linux minirootfs, base for the devkit debug extension"
branch: "v3.21"
version: "3.21.3"
mirror: "https://dl-cdn.alpinelinux.org/alpine"
nvrc:
# yamllint disable-line rule:line-length
desc: "The NVRC project provides a Rust binary that implements a simple init system for microVMs"