From 92d729f5cf9861fcb4cda1374c76887fdc3a150c Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 03:02:30 +0000 Subject: [PATCH 01/20] ci: add Asterinas doc workflow replay --- .../workflows/test-asterinas-kata-docs.yml | 169 ++++++++ docs/asterinas-kata-doc-workflow-progress.md | 46 ++ docs/doc-in-asterinas-repo.md | 132 ++++++ tools/kata/README.md | 7 + tools/kata/interactive_doc_test.py | 400 ++++++++++++++++++ 5 files changed, 754 insertions(+) create mode 100644 .github/workflows/test-asterinas-kata-docs.yml create mode 100644 docs/asterinas-kata-doc-workflow-progress.md create mode 100644 docs/doc-in-asterinas-repo.md create mode 100644 tools/kata/interactive_doc_test.py diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml new file mode 100644 index 000000000..4003bdc67 --- /dev/null +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -0,0 +1,169 @@ +name: Test | Asterinas Kata Docs + +on: + workflow_dispatch: + push: + branches: + - asterinas + +permissions: + contents: read + +jobs: + resolve-upstream-asterinas-image: + name: Resolve upstream Asterinas image + runs-on: ubuntu-latest + outputs: + asterinas_version: ${{ steps.resolve.outputs.asterinas_version }} + docker_image_version: ${{ steps.resolve.outputs.docker_image_version }} + steps: + - name: Resolve Upstream Asterinas Image Metadata + id: resolve + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + + asterinas_version="$( + gh api repos/asterinas/asterinas/contents/VERSION?ref=main --jq .content | + base64 -d | tr -d '\n' + )" + docker_image_version="$( + gh api repos/asterinas/asterinas/contents/DOCKER_IMAGE_VERSION?ref=main --jq .content | + base64 -d | tr -d '\n' + )" + test -n "${asterinas_version}" + test -n "${docker_image_version}" + + echo "asterinas_version=${asterinas_version}" >> "${GITHUB_OUTPUT}" + echo "docker_image_version=${docker_image_version}" >> "${GITHUB_OUTPUT}" + { + echo '## Resolved upstream Asterinas image' + echo + echo "- \`VERSION\`: \`${asterinas_version}\`" + echo "- \`DOCKER_IMAGE_VERSION\`: \`${docker_image_version}\`" + echo "- Base image: \`asterinas/asterinas:${docker_image_version}\`" + } >> "${GITHUB_STEP_SUMMARY}" + + test-documented-end-user-flow: + name: Test documented end-user flow + needs: + - resolve-upstream-asterinas-image + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + KATA_DOC_LOG_DIR: ${{ runner.temp }}/asterinas-kata-doc-logs/end-user + steps: + - uses: actions/checkout@v4 + + - name: Check Host Device Visibility + shell: bash + run: | + set -euxo pipefail + ls -l /dev/kvm /dev/vhost-net /dev/vhost-vsock /dev/vsock + + - name: Install pexpect if Needed + shell: bash + run: | + set -euxo pipefail + if ! python3 - <<'PY' + import importlib.util + import sys + sys.exit(0 if importlib.util.find_spec("pexpect") else 1) + PY + then + python3 -m pip install --user pexpect + fi + + - name: Pull Published Asterinas Kata Image + shell: bash + run: | + set -euxo pipefail + docker pull "asterinas/kata:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" + + - name: Replay Documented End-User Flow + shell: bash + run: | + set -euxo pipefail + python3 tools/kata/interactive_doc_test.py \ + --scenario end-user \ + --kata-image "asterinas/kata:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ + --log-dir "${KATA_DOC_LOG_DIR}" + + - name: Upload End-User Flow Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: asterinas-kata-doc-end-user-logs + path: ${{ env.KATA_DOC_LOG_DIR }} + if-no-files-found: ignore + + test-documented-kernel-developer-flow: + name: Test documented kernel-developer flow + needs: + - resolve-upstream-asterinas-image + runs-on: ubuntu-latest + timeout-minutes: 180 + env: + KATA_DOC_LOG_DIR: ${{ runner.temp }}/asterinas-kata-doc-logs/kernel-developer + KATA_STATIC_TARBALL_RELEASE_REPO: ${{ github.repository }} + steps: + - uses: actions/checkout@v4 + + - name: Check Out Asterinas Source + uses: actions/checkout@v4 + with: + repository: asterinas/asterinas + path: asterinas-src + + - name: Prepare Local Asterinas Tree + shell: bash + run: | + set -euxo pipefail + rm -rf "${HOME}/asterinas" + mkdir -p "${HOME}/asterinas" + rsync -a --delete "${GITHUB_WORKSPACE}/asterinas-src/" "${HOME}/asterinas/" + + - name: Check Host Device Visibility + shell: bash + run: | + set -euxo pipefail + ls -l /dev/kvm /dev/vhost-net /dev/vhost-vsock /dev/vsock + + - name: Install pexpect if Needed + shell: bash + run: | + set -euxo pipefail + if ! python3 - <<'PY' + import importlib.util + import sys + sys.exit(0 if importlib.util.find_spec("pexpect") else 1) + PY + then + python3 -m pip install --user pexpect + fi + + - name: Pull Published Asterinas Builder Image + shell: bash + run: | + set -euxo pipefail + docker pull "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" + + - name: Replay Documented Kernel-Developer Flow + shell: bash + run: | + set -euxo pipefail + python3 tools/kata/interactive_doc_test.py \ + --scenario kernel-developer \ + --asterinas-image "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ + --asterinas-dir "${HOME}/asterinas" \ + --log-dir "${KATA_DOC_LOG_DIR}" + + - name: Upload Kernel-Developer Flow Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: asterinas-kata-doc-kernel-developer-logs + path: ${{ env.KATA_DOC_LOG_DIR }} + if-no-files-found: ignore diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md new file mode 100644 index 000000000..2d660cee8 --- /dev/null +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -0,0 +1,46 @@ +# Asterinas Kata Doc Workflow Progress + +## 2026-04-22 Initial implementation notes + +- Requirement focus: + - add a new workflow that follows `docs/doc-in-asterinas-repo.md` + - keep the overall documented flow, but correct commands that are currently off + - test the interactive `docker run -it` and inner `nerdctl run -it` behavior with `pexpect` + - mount the local Asterinas tree from `$HOME/asterinas` for the developer flow + - use local `asterinas/asterinas` and `asterinas/kata` images first, then wire the same logic into CI +- Document drift confirmed and corrected in the implementation plan: + - `docker run --cgroupns none` is invalid; use `--cgroupns host` + - the outer container needs `/dev/vhost-net` and `/dev/vsock` in addition to `/dev/kvm` and `/dev/vhost-vsock` + - helper script names are `tools/kata/kata_env.sh` and `tools/kata/kata_services.sh` + - the developer flow bind mount should be `-v "${ASTERINAS_SRC}:/root/asterinas"` + - the Kata config path should be `/etc/kata-containers/configuration.toml` +- Added `tools/kata/interactive_doc_test.py`: + - uses `pexpect` to drive the documented interactive flow instead of only calling the existing non-interactive smoke helper + - covers the end-user scenario on `asterinas/kata` + - covers the kernel-developer scenario on `asterinas/asterinas` + - validates the local kernel handoff path with `/root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf` +- Added `.github/workflows/test-asterinas-kata-docs.yml`: + - one job replays the documented end-user flow against `asterinas/kata` + - one job replays the documented kernel-developer flow against `asterinas/asterinas` + - both jobs install `pexpect` only when needed and upload transcripts as artifacts +- Local environment findings: + - `pexpect` is already available in the current local environment + - local `asterinas/asterinas:*` and `asterinas/kata:*` images are present + - the local Asterinas source tree exists at `/home/jianfeng/asterinas` + - the local machine exposes `/dev/kvm`, `/dev/vhost-net`, `/dev/vhost-vsock`, and `/dev/vsock` + - local Docker Hub access currently times out during the inner `nerdctl run` +- Local validation completed so far: + - the documented end-user flow passed locally with + `python3 tools/kata/interactive_doc_test.py --scenario end-user --workload-image docker.1ms.run/alpine:latest` + - this successfully covered the interactive outer `docker run -it`, `tools/kata/kata_services.sh start`, inner `nerdctl run -it`, and post-exit `nerdctl rm foo` sequence + - after cleaning the `pexpect` output parsing and passing outer env overrides explicitly, the same end-user replay was run again locally and still passed + - per the latest user instruction, the kernel-developer flow does not need to finish locally; CI is now the authoritative validation path for that scenario +- Local validation policy: + - local testing uses `docker.1ms.run/alpine:latest` as the workload image when Docker Hub is unreachable + - CI must keep using `docker.io/alpine:latest` +- CI wiring note: + - the kernel-developer workflow path exports `KATA_STATIC_TARBALL_RELEASE_REPO=${GITHUB_REPOSITORY}` so `tools/kata/kata_env.sh install` validates this repository's published Asterinas Kata release assets instead of defaulting to upstream `kata-containers/kata-containers` +- Next steps: + - run a final quick local end-user replay after the latest script cleanup + - static-check the new workflow and script changes + - push and wait for CI to verify both documented flows end to end diff --git a/docs/doc-in-asterinas-repo.md b/docs/doc-in-asterinas-repo.md new file mode 100644 index 000000000..c99b1caac --- /dev/null +++ b/docs/doc-in-asterinas-repo.md @@ -0,0 +1,132 @@ +Kata Containers is a VM-based container runtime. Each container runs inside its own virtual machine, so containers do not share the host kernel. + +Asterinas now supports Kata Containers and can serve as the guest kernel in common Kata-based scenarios. + +## For End Users +For end users, we provide the `asterinas/kata` container image, which already includes the dependencies and scripts required by Kata. + +You can try it with the following command: + +```bash +ASTERINAS_IMAGE_TAG=0.17.2-20260407 +docker run --rm -it \ + --cgroupns host \ + --privileged \ + --device /dev/kvm \ + --device /dev/vhost-net \ + --device /dev/vhost-vsock \ + --device /dev/vsock \ + --tmpfs /tmp:exec,mode=1777,size=8g \ + --tmpfs /var/lib/containerd:exec,mode=755,size=8g \ + asterinas/kata:${ASTERINAS_IMAGE_TAG} +``` + +After entering the container, start the background services required by Kata. At the moment, these services are `containerd` and `syslogd`: + +```bash +tools/kata/kata_services.sh start +``` + +You can check their status with the following command to make sure they started successfully: + +```bash +tools/kata/kata_services.sh status +``` + +Then you can use `nerdctl` with Kata to start an Alpine container: + +```bash +nerdctl run \ + --cgroup-manager cgroupfs \ + --net none \ + --runtime io.containerd.kata.v2 \ + --name foo \ + -it \ + docker.io/alpine:latest +``` + +If Docker Hub is temporarily unreachable during local validation, you can use +`docker.1ms.run/alpine:latest` instead. CI should keep using +`docker.io/alpine:latest`. + +Once the container starts, you will be inside Alpine. You can run the following commands to verify that you are no longer in the host environment: + +```bash +cat /proc/version +cat /etc/alpine-release +``` + +After you exit the container, remove it with: + +```bash +nerdctl rm foo +``` + +## For Kernel Developers +Kernel developers typically use the `asterinas/asterinas` container image. When starting the container, you also need to pass the additional arguments required by Kata: + +```bash +ASTERINAS_IMAGE_TAG=0.17.2-20260407 +ASTERINAS_SRC=$HOME/asterinas +KATA_SRC=$(git rev-parse --show-toplevel) +docker run --rm -it \ + --cgroupns host \ + --privileged \ + --device /dev/kvm \ + --device /dev/vhost-net \ + --device /dev/vhost-vsock \ + --device /dev/vsock \ + --tmpfs /tmp:exec,mode=1777,size=8g \ + --tmpfs /var/lib/containerd:exec,mode=755,size=8g \ + -v "${ASTERINAS_SRC}:/root/asterinas" \ + -v "${KATA_SRC}:/root/kata-containers" \ + -w /root/kata-containers \ + asterinas/asterinas:${ASTERINAS_IMAGE_TAG} +``` + +After entering the container, you will already be in `/root/kata-containers`. +The examples below assume the current kata-containers checkout is mounted there, +and that the local Asterinas tree is mounted at `/root/asterinas`. + +```bash +pwd +``` + +This script installs the files, scripts, and configuration needed to run Kata. After that, you can test Kata in the same way as an end user: + +```bash +# Install all dependencies +tools/kata/kata_env.sh install + +# Start background services +tools/kata/kata_services.sh start + +nerdctl run \ + --cgroup-manager cgroupfs \ + --net none \ + --runtime io.containerd.kata.v2 \ + --name foo \ + -it \ + docker.io/alpine:latest +``` + +If Docker Hub is temporarily unreachable during local validation, you can use +`docker.1ms.run/alpine:latest` here as well. CI should keep using +`docker.io/alpine:latest`. + +You can also point Kata to a locally built guest kernel. Edit +`/etc/kata-containers/configuration.toml` and set `kernel` to the path of your +local kernel image: + +```toml +[hypervisor.qemu] +kernel = "/root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf" +``` + +Build the kernel with: + +```bash +cd /root/asterinas && make kernel BOOT_METHOD=qemu-direct +``` + +Then start the container with `nerdctl` again, and Kata will boot using your local kernel. diff --git a/tools/kata/README.md b/tools/kata/README.md index f670fdbec..1583c1729 100644 --- a/tools/kata/README.md +++ b/tools/kata/README.md @@ -4,6 +4,9 @@ Kata helpers live here. - `run_kata.sh`: provides predefined `smoke`, `pass`, and `workload` Kata tasks - `kata_env.sh`: provides `install` and `check` for the shared Kata environment lifecycle - `kata_services.sh`: provides `start`, `stop`, and `status` for the background Kata smoke-test services +- `interactive_doc_test.py`: replays the documented `docker run -it` and + inner `nerdctl run -it` flows with `pexpect` for both the `asterinas/kata` + and `asterinas/asterinas` images - `check_overlayfs.sh`: probes whether the current host-side backing filesystem can support overlayfs `upperdir` and `workdir` for local Kata runs - `config/`: repo-owned Kata, CNI, `containerd`, and smoke-test config files used by the scripts @@ -26,6 +29,10 @@ Kata helpers live here. - Legacy `KATA_ALPINE_*` overrides still map to the new `KATA_TEST_*` names. - The default workload still pulls Alpine and runs `cat /etc/alpine-release`, but the image, in-container command, and output check are now script-configurable. +- `interactive_doc_test.py` uses `KATA_DOC_TEST_WORKLOAD_IMAGE`, which defaults + to `docker.io/alpine:latest`. For local validation in environments where + Docker Hub is unreachable, you can temporarily switch it to + `docker.1ms.run/alpine:latest`. ## Local virtio-fs note diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py new file mode 100644 index 000000000..c50ca7128 --- /dev/null +++ b/tools/kata/interactive_doc_test.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import os +import pathlib +import re +import shlex +import subprocess +import sys +import uuid + +import pexpect + + +OUTER_PROMPT = "PEXPECT_OUTER> " +GUEST_PROMPT = "PEXPECT_GUEST> " +DEFAULT_KATA_IMAGE = "asterinas/kata:0.17.2-20260407" +DEFAULT_ASTERINAS_IMAGE = "asterinas/asterinas:0.17.2-20260407" +DEFAULT_WORKLOAD_IMAGE = "docker.io/alpine:latest" +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] +ANSI_ESCAPE_RE = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + + +class ScenarioError(RuntimeError): + pass + + +def strip_terminal_control_sequences(text: str) -> str: + return ANSI_ESCAPE_RE.sub("", text).replace("\r", "") + + +def clean_command_output(command: str, output: str) -> str: + cleaned_lines: list[str] = [] + + for raw_line in strip_terminal_control_sequences(output).splitlines(): + line = raw_line.strip() + if not line: + continue + + if line.startswith(OUTER_PROMPT.strip()): + line = line[len(OUTER_PROMPT.strip()) :].strip() + elif line.startswith(GUEST_PROMPT.strip()): + line = line[len(GUEST_PROMPT.strip()) :].strip() + + if not line: + continue + + if line == command: + continue + if line == "'": + continue + if line == "status=$?": + continue + if line.startswith("printf '__DOC_TEST_EXIT__"): + continue + if line.startswith("__DOC_TEST_EXIT__"): + continue + + cleaned_lines.append(line) + + return "\n".join(cleaned_lines).strip() + + +def outer_docker_env_args() -> list[str]: + args = ["-e", "TERM=dumb"] + passthrough_vars = ( + "KATA_FORCE_APT", + "KATA_PAYLOAD_IMAGE", + "KATA_STATIC_TARBALL_RELEASE_REPO", + "KATA_STATIC_TARBALL_SHA256", + "KATA_STATIC_TARBALL_SHA256_URL", + "KATA_STATIC_TARBALL_URL", + "KATA_VERSION", + "NERDCTL_VERSION", + ) + + for env_name in passthrough_vars: + env_value = os.environ.get(env_name) + if env_value: + args.extend(["-e", f"{env_name}={env_value}"]) + + return args + + +class DockerShell: + def __init__(self, name: str, docker_args: list[str], transcript_path: pathlib.Path) -> None: + self.name = name + self.docker_args = docker_args + self.transcript_path = transcript_path + self.child: pexpect.spawn | None = None + self.log_handle = None + self.prompt = OUTER_PROMPT + + def start(self) -> "DockerShell": + self.transcript_path.parent.mkdir(parents=True, exist_ok=True) + self.log_handle = self.transcript_path.open("w", encoding="utf-8") + subprocess.run( + ["docker", "rm", "-f", self.name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + self.child = pexpect.spawn( + "docker", + self.docker_args, + encoding="utf-8", + codec_errors="replace", + echo=False, + timeout=60, + ) + self.child.logfile_read = self.log_handle + self.child.setwinsize(60, 200) + self.child.expect(r"[#$] ", timeout=120) + self.set_prompt(OUTER_PROMPT) + return self + + def close(self) -> None: + if self.child is not None: + self.child.close(force=True) + self.child = None + subprocess.run( + ["docker", "rm", "-f", self.name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if self.log_handle is not None: + self.log_handle.close() + self.log_handle = None + + def set_prompt(self, prompt: str) -> None: + assert self.child is not None + self.child.sendline(f"export PS1={shlex.quote(prompt)}") + self.child.expect(re.escape(prompt), timeout=30) + self.prompt = prompt + + def run(self, command: str, timeout: int = 300, check: bool = True) -> str: + assert self.child is not None + marker = f"__DOC_TEST_EXIT__{uuid.uuid4().hex}__:" + self.child.sendline(command) + self.child.sendline("status=$?") + self.child.sendline(f"printf '{marker}%s\\n' \"$status\"") + self.child.expect(re.compile(re.escape(marker) + r"(\d+)\r?\n"), timeout=timeout) + output = clean_command_output(command, self.child.before) + status = int(self.child.match.group(1)) + self.child.expect(re.escape(self.prompt), timeout=60) + if check and status != 0: + raise ScenarioError(f"Command failed with exit code {status}: {command}\n{output}") + return output + + def enter_guest(self, command: str, timeout: int = 900) -> None: + assert self.child is not None + self.child.sendline(command) + self.child.expect(r"[#$] ", timeout=timeout) + self.set_prompt(GUEST_PROMPT) + + def exit_guest(self) -> None: + assert self.child is not None + self.child.sendline("exit") + self.child.expect(re.escape(OUTER_PROMPT), timeout=120) + self.prompt = OUTER_PROMPT + + +def require_paths(paths: list[pathlib.Path]) -> None: + missing_paths = [str(path) for path in paths if not path.exists()] + if missing_paths: + raise ScenarioError(f"Missing required path(s): {', '.join(missing_paths)}") + + +def run_guest_workload(shell: DockerShell, workload_image: str, host_proc_version: str, container_name: str = "foo") -> tuple[str, str]: + shell.run(f"nerdctl rm -f {shlex.quote(container_name)} >/dev/null 2>&1 || true", check=False) + shell.enter_guest( + " ".join( + [ + "nerdctl", + "run", + "--cgroup-manager", + "cgroupfs", + "--net", + "none", + "--runtime", + "io.containerd.kata.v2", + "--name", + shlex.quote(container_name), + "-it", + shlex.quote(workload_image), + ] + ), + timeout=900, + ) + guest_proc_version = shell.run("cat /proc/version") + alpine_release = shell.run("cat /etc/alpine-release") + shell.exit_guest() + shell.run(f"nerdctl rm -f {shlex.quote(container_name)}") + + if guest_proc_version.strip() == host_proc_version.strip(): + raise ScenarioError("Guest /proc/version unexpectedly matches the outer container /proc/version") + if not alpine_release.strip(): + raise ScenarioError("Expected a non-empty /etc/alpine-release inside the guest workload") + + return guest_proc_version.strip(), alpine_release.strip() + + +def run_end_user_scenario(args: argparse.Namespace) -> pathlib.Path: + outer_name = f"kata-doc-end-user-{uuid.uuid4().hex[:12]}" + transcript_path = args.log_dir / "end-user.transcript.log" + shell = DockerShell( + outer_name, + [ + "run", + "--rm", + "-it", + "--name", + outer_name, + "--cgroupns", + "host", + "--privileged", + "--device", + "/dev/kvm", + "--device", + "/dev/vhost-net", + "--device", + "/dev/vhost-vsock", + "--device", + "/dev/vsock", + "--tmpfs", + "/tmp:exec,mode=1777,size=8g", + "--tmpfs", + "/var/lib/containerd:exec,mode=755,size=8g", + *outer_docker_env_args(), + args.kata_image, + ], + transcript_path, + ).start() + + try: + print(f"[end-user] using outer image: {args.kata_image}") + host_proc_version = shell.run("cat /proc/version") + shell.run("cd /root/asterinas") + shell.run("./tools/kata/kata_services.sh start", timeout=300) + status_output = shell.run("./tools/kata/kata_services.sh status") + if "Kata services are running." not in status_output: + raise ScenarioError(f"Unexpected service status output:\n{status_output}") + guest_proc_version, alpine_release = run_guest_workload(shell, args.workload_image, host_proc_version) + print(f"[end-user] guest /proc/version: {guest_proc_version}") + print(f"[end-user] alpine release: {alpine_release}") + shell.run("./tools/kata/kata_services.sh stop", timeout=300) + return transcript_path + finally: + shell.close() + + +def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: + require_paths([args.repo_dir, args.asterinas_dir]) + outer_name = f"kata-doc-kernel-dev-{uuid.uuid4().hex[:12]}" + transcript_path = args.log_dir / "kernel-developer.transcript.log" + shell = DockerShell( + outer_name, + [ + "run", + "--rm", + "-it", + "--name", + outer_name, + "--cgroupns", + "host", + "--privileged", + "--device", + "/dev/kvm", + "--device", + "/dev/vhost-net", + "--device", + "/dev/vhost-vsock", + "--device", + "/dev/vsock", + "--tmpfs", + "/tmp:exec,mode=1777,size=8g", + "--tmpfs", + "/var/lib/containerd:exec,mode=755,size=8g", + *outer_docker_env_args(), + "-v", + f"{args.asterinas_dir.resolve()}:/root/asterinas", + "-v", + f"{args.repo_dir.resolve()}:/root/kata-containers:ro", + "-w", + "/root/kata-containers", + args.asterinas_image, + ], + transcript_path, + ).start() + + try: + print(f"[kernel-developer] using outer image: {args.asterinas_image}") + print(f"[kernel-developer] mounted Asterinas tree: {args.asterinas_dir}") + host_proc_version = shell.run("cat /proc/version") + shell.run("test -d /root/kata-containers/tools/kata") + shell.run("./tools/kata/kata_env.sh install", timeout=3600) + shell.run("./tools/kata/kata_services.sh start", timeout=300) + status_output = shell.run("./tools/kata/kata_services.sh status") + if "Kata services are running." not in status_output: + raise ScenarioError(f"Unexpected service status output:\n{status_output}") + packaged_guest_proc_version, packaged_alpine_release = run_guest_workload( + shell, + args.workload_image, + host_proc_version, + "foo", + ) + print(f"[kernel-developer] packaged guest /proc/version: {packaged_guest_proc_version}") + print(f"[kernel-developer] packaged alpine release: {packaged_alpine_release}") + shell.run("cd /root/asterinas && make kernel BOOT_METHOD=qemu-direct", timeout=7200) + shell.run("test -f /root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf") + shell.run( + "sed -i 's#^kernel = \".*\"#kernel = \"/root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf\"#' " + "/etc/kata-containers/configuration.toml" + ) + shell.run( + "grep -F 'kernel = \"/root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf\"' " + "/etc/kata-containers/configuration.toml" + ) + local_guest_proc_version, local_alpine_release = run_guest_workload( + shell, + args.workload_image, + host_proc_version, + "foo", + ) + print(f"[kernel-developer] local-kernel guest /proc/version: {local_guest_proc_version}") + print(f"[kernel-developer] local-kernel alpine release: {local_alpine_release}") + shell.run("./tools/kata/kata_services.sh stop", timeout=300) + return transcript_path + finally: + shell.close() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Replay the documented Asterinas Kata flows with pexpect") + parser.add_argument( + "--scenario", + choices=["end-user", "kernel-developer", "all"], + default=os.environ.get("KATA_DOC_TEST_SCENARIO", "all"), + ) + parser.add_argument( + "--kata-image", + default=os.environ.get("KATA_DOC_TEST_KATA_IMAGE", DEFAULT_KATA_IMAGE), + ) + parser.add_argument( + "--asterinas-image", + default=os.environ.get("KATA_DOC_TEST_ASTERINAS_IMAGE", DEFAULT_ASTERINAS_IMAGE), + ) + parser.add_argument( + "--workload-image", + default=os.environ.get("KATA_DOC_TEST_WORKLOAD_IMAGE", DEFAULT_WORKLOAD_IMAGE), + ) + parser.add_argument( + "--repo-dir", + type=pathlib.Path, + default=pathlib.Path(os.environ.get("KATA_DOC_TEST_REPO_DIR", str(REPO_ROOT))), + ) + parser.add_argument( + "--asterinas-dir", + type=pathlib.Path, + default=pathlib.Path(os.environ.get("KATA_DOC_TEST_ASTERINAS_DIR", str(pathlib.Path.home() / "asterinas"))), + ) + parser.add_argument( + "--log-dir", + type=pathlib.Path, + default=pathlib.Path(os.environ.get("KATA_DOC_TEST_LOG_DIR", "/tmp/asterinas-kata-doc-tests")), + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + args.log_dir.mkdir(parents=True, exist_ok=True) + transcripts: list[pathlib.Path] = [] + + try: + if args.scenario in {"end-user", "all"}: + transcripts.append(run_end_user_scenario(args)) + if args.scenario in {"kernel-developer", "all"}: + transcripts.append(run_kernel_developer_scenario(args)) + except Exception as error: + print(f"interactive_doc_test failed: {error}", file=sys.stderr) + if transcripts: + print("available transcript(s):", file=sys.stderr) + for transcript in transcripts: + print(f" - {transcript}", file=sys.stderr) + else: + print(f"transcript directory: {args.log_dir}", file=sys.stderr) + return 1 + + print("interactive_doc_test passed") + for transcript in transcripts: + print(f"transcript: {transcript}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 5be1f8718af10e0083ee3abebf7ade7da73d57b8 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 03:06:22 +0000 Subject: [PATCH 02/20] ci: fix docs workflow temp path --- .github/workflows/test-asterinas-kata-docs.yml | 4 ++-- docs/asterinas-kata-doc-workflow-progress.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index 4003bdc67..d08c029c2 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 120 env: - KATA_DOC_LOG_DIR: ${{ runner.temp }}/asterinas-kata-doc-logs/end-user + KATA_DOC_LOG_DIR: /tmp/asterinas-kata-doc-logs/end-user steps: - uses: actions/checkout@v4 @@ -106,7 +106,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 180 env: - KATA_DOC_LOG_DIR: ${{ runner.temp }}/asterinas-kata-doc-logs/kernel-developer + KATA_DOC_LOG_DIR: /tmp/asterinas-kata-doc-logs/kernel-developer KATA_STATIC_TARBALL_RELEASE_REPO: ${{ github.repository }} steps: - uses: actions/checkout@v4 diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 2d660cee8..62e0230ed 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -40,6 +40,7 @@ - CI must keep using `docker.io/alpine:latest` - CI wiring note: - the kernel-developer workflow path exports `KATA_STATIC_TARBALL_RELEASE_REPO=${GITHUB_REPOSITORY}` so `tools/kata/kata_env.sh install` validates this repository's published Asterinas Kata release assets instead of defaulting to upstream `kata-containers/kata-containers` + - the first GitHub dispatch attempt exposed a GitHub expression-validation issue: `runner.temp` is not accepted in this workflow's job-level `env`, so the log directory paths were normalized to plain `/tmp/...` - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes From a642f584b9b860748877dd84285fa8518b4443bc Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 03:09:40 +0000 Subject: [PATCH 03/20] ci: make docs workflow vsock optional --- .../workflows/test-asterinas-kata-docs.yml | 6 ++- docs/asterinas-kata-doc-workflow-progress.md | 1 + docs/doc-in-asterinas-repo.md | 6 ++- tools/kata/interactive_doc_test.py | 41 +++++++++++-------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index d08c029c2..0b15f6135 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -61,7 +61,8 @@ jobs: shell: bash run: | set -euxo pipefail - ls -l /dev/kvm /dev/vhost-net /dev/vhost-vsock /dev/vsock + ls -l /dev/kvm /dev/vhost-net /dev/vhost-vsock + ls -l /dev/vsock || true - name: Install pexpect if Needed shell: bash @@ -129,7 +130,8 @@ jobs: shell: bash run: | set -euxo pipefail - ls -l /dev/kvm /dev/vhost-net /dev/vhost-vsock /dev/vsock + ls -l /dev/kvm /dev/vhost-net /dev/vhost-vsock + ls -l /dev/vsock || true - name: Install pexpect if Needed shell: bash diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 62e0230ed..3517fda91 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -41,6 +41,7 @@ - CI wiring note: - the kernel-developer workflow path exports `KATA_STATIC_TARBALL_RELEASE_REPO=${GITHUB_REPOSITORY}` so `tools/kata/kata_env.sh install` validates this repository's published Asterinas Kata release assets instead of defaulting to upstream `kata-containers/kata-containers` - the first GitHub dispatch attempt exposed a GitHub expression-validation issue: `runner.temp` is not accepted in this workflow's job-level `env`, so the log directory paths were normalized to plain `/tmp/...` + - the first push run also showed that `/dev/vsock` is not guaranteed on GitHub-hosted runners, while `/dev/kvm`, `/dev/vhost-net`, and `/dev/vhost-vsock` are present; the workflow and `pexpect` driver now treat `/dev/vsock` as optional instead of mandatory - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/docs/doc-in-asterinas-repo.md b/docs/doc-in-asterinas-repo.md index c99b1caac..dc799ac9b 100644 --- a/docs/doc-in-asterinas-repo.md +++ b/docs/doc-in-asterinas-repo.md @@ -15,7 +15,6 @@ docker run --rm -it \ --device /dev/kvm \ --device /dev/vhost-net \ --device /dev/vhost-vsock \ - --device /dev/vsock \ --tmpfs /tmp:exec,mode=1777,size=8g \ --tmpfs /var/lib/containerd:exec,mode=755,size=8g \ asterinas/kata:${ASTERINAS_IMAGE_TAG} @@ -75,7 +74,6 @@ docker run --rm -it \ --device /dev/kvm \ --device /dev/vhost-net \ --device /dev/vhost-vsock \ - --device /dev/vsock \ --tmpfs /tmp:exec,mode=1777,size=8g \ --tmpfs /var/lib/containerd:exec,mode=755,size=8g \ -v "${ASTERINAS_SRC}:/root/asterinas" \ @@ -114,6 +112,10 @@ If Docker Hub is temporarily unreachable during local validation, you can use `docker.1ms.run/alpine:latest` here as well. CI should keep using `docker.io/alpine:latest`. +If `/dev/vsock` also exists on your host, it is fine to pass it through too, +but the documented minimum setup only requires `/dev/kvm`, `/dev/vhost-net`, +and `/dev/vhost-vsock`. + You can also point Kata to a locally built guest kernel. Edit `/etc/kata-containers/configuration.toml` and set `kernel` to the path of your local kernel image: diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py index c50ca7128..11da39a1d 100644 --- a/tools/kata/interactive_doc_test.py +++ b/tools/kata/interactive_doc_test.py @@ -84,6 +84,29 @@ def outer_docker_env_args() -> list[str]: return args +def outer_docker_device_args() -> list[str]: + args: list[str] = [] + required_devices = ( + "/dev/kvm", + "/dev/vhost-net", + "/dev/vhost-vsock", + ) + optional_devices = ( + "/dev/vsock", + ) + + for device_path in required_devices: + if not pathlib.Path(device_path).exists(): + raise ScenarioError(f"Required device is missing on the host: {device_path}") + args.extend(["--device", device_path]) + + for device_path in optional_devices: + if pathlib.Path(device_path).exists(): + args.extend(["--device", device_path]) + + return args + + class DockerShell: def __init__(self, name: str, docker_args: list[str], transcript_path: pathlib.Path) -> None: self.name = name @@ -217,14 +240,7 @@ def run_end_user_scenario(args: argparse.Namespace) -> pathlib.Path: "--cgroupns", "host", "--privileged", - "--device", - "/dev/kvm", - "--device", - "/dev/vhost-net", - "--device", - "/dev/vhost-vsock", - "--device", - "/dev/vsock", + *outer_docker_device_args(), "--tmpfs", "/tmp:exec,mode=1777,size=8g", "--tmpfs", @@ -267,14 +283,7 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: "--cgroupns", "host", "--privileged", - "--device", - "/dev/kvm", - "--device", - "/dev/vhost-net", - "--device", - "/dev/vhost-vsock", - "--device", - "/dev/vsock", + *outer_docker_device_args(), "--tmpfs", "/tmp:exec,mode=1777,size=8g", "--tmpfs", From 79893feb4a52230196a8568c2d67ea207ebecdc9 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 03:42:26 +0000 Subject: [PATCH 04/20] ci: stream doc workflow progress --- .../workflows/test-asterinas-kata-docs.yml | 4 +- docs/asterinas-kata-doc-workflow-progress.md | 2 + tools/kata/interactive_doc_test.py | 69 ++++++++++++++----- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index 0b15f6135..ae0f333ee 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -87,7 +87,7 @@ jobs: shell: bash run: | set -euxo pipefail - python3 tools/kata/interactive_doc_test.py \ + python3 -u tools/kata/interactive_doc_test.py \ --scenario end-user \ --kata-image "asterinas/kata:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ --log-dir "${KATA_DOC_LOG_DIR}" @@ -156,7 +156,7 @@ jobs: shell: bash run: | set -euxo pipefail - python3 tools/kata/interactive_doc_test.py \ + python3 -u tools/kata/interactive_doc_test.py \ --scenario kernel-developer \ --asterinas-image "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ --asterinas-dir "${HOME}/asterinas" \ diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 3517fda91..937b70ed4 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -42,6 +42,8 @@ - the kernel-developer workflow path exports `KATA_STATIC_TARBALL_RELEASE_REPO=${GITHUB_REPOSITORY}` so `tools/kata/kata_env.sh install` validates this repository's published Asterinas Kata release assets instead of defaulting to upstream `kata-containers/kata-containers` - the first GitHub dispatch attempt exposed a GitHub expression-validation issue: `runner.temp` is not accepted in this workflow's job-level `env`, so the log directory paths were normalized to plain `/tmp/...` - the first push run also showed that `/dev/vsock` is not guaranteed on GitHub-hosted runners, while `/dev/kvm`, `/dev/vhost-net`, and `/dev/vhost-vsock` are present; the workflow and `pexpect` driver now treat `/dev/vsock` as optional instead of mandatory + - based on user feedback, the `pexpect` driver now streams child output to stdout and prints explicit phase markers before long-running commands such as `kata_env.sh install`, `nerdctl run`, and `make kernel` + - the guest-entry helper now fails fast when `nerdctl run -it ...` falls back to the outer shell instead of opening the inner guest shell, so proxy/image failures no longer look like a generic hang - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py index 11da39a1d..d30fade89 100644 --- a/tools/kata/interactive_doc_test.py +++ b/tools/kata/interactive_doc_test.py @@ -27,6 +27,20 @@ class ScenarioError(RuntimeError): pass +class TeeWriter: + def __init__(self, *streams) -> None: + self.streams = streams + + def write(self, data: str) -> None: + for stream in self.streams: + stream.write(data) + stream.flush() + + def flush(self) -> None: + for stream in self.streams: + stream.flush() + + def strip_terminal_control_sequences(text: str) -> str: return ANSI_ESCAPE_RE.sub("", text).replace("\r", "") @@ -115,6 +129,7 @@ def __init__(self, name: str, docker_args: list[str], transcript_path: pathlib.P self.child: pexpect.spawn | None = None self.log_handle = None self.prompt = OUTER_PROMPT + self.stream_output = os.environ.get("KATA_DOC_TEST_STREAM_OUTPUT", "1") not in {"0", "false", "FALSE", "no", "NO"} def start(self) -> "DockerShell": self.transcript_path.parent.mkdir(parents=True, exist_ok=True) @@ -133,7 +148,10 @@ def start(self) -> "DockerShell": echo=False, timeout=60, ) - self.child.logfile_read = self.log_handle + if self.stream_output: + self.child.logfile_read = TeeWriter(self.log_handle, sys.stdout) + else: + self.child.logfile_read = self.log_handle self.child.setwinsize(60, 200) self.child.expect(r"[#$] ", timeout=120) self.set_prompt(OUTER_PROMPT) @@ -176,7 +194,11 @@ def run(self, command: str, timeout: int = 300, check: bool = True) -> str: def enter_guest(self, command: str, timeout: int = 900) -> None: assert self.child is not None self.child.sendline(command) - self.child.expect(r"[#$] ", timeout=timeout) + match_index = self.child.expect([re.escape(OUTER_PROMPT), r"[#$] "], timeout=timeout) + if match_index == 0: + self.prompt = OUTER_PROMPT + failure_output = clean_command_output(command, self.child.before) + raise ScenarioError(f"Guest shell did not start successfully for command: {command}\n{failure_output}") self.set_prompt(GUEST_PROMPT) def exit_guest(self) -> None: @@ -192,7 +214,12 @@ def require_paths(paths: list[pathlib.Path]) -> None: raise ScenarioError(f"Missing required path(s): {', '.join(missing_paths)}") +def announce(message: str) -> None: + print(message, flush=True) + + def run_guest_workload(shell: DockerShell, workload_image: str, host_proc_version: str, container_name: str = "foo") -> tuple[str, str]: + announce(f"[guest] launching workload container {container_name} from {workload_image}") shell.run(f"nerdctl rm -f {shlex.quote(container_name)} >/dev/null 2>&1 || true", check=False) shell.enter_guest( " ".join( @@ -216,6 +243,7 @@ def run_guest_workload(shell: DockerShell, workload_image: str, host_proc_versio guest_proc_version = shell.run("cat /proc/version") alpine_release = shell.run("cat /etc/alpine-release") shell.exit_guest() + announce(f"[guest] removing workload container {container_name}") shell.run(f"nerdctl rm -f {shlex.quote(container_name)}") if guest_proc_version.strip() == host_proc_version.strip(): @@ -252,16 +280,18 @@ def run_end_user_scenario(args: argparse.Namespace) -> pathlib.Path: ).start() try: - print(f"[end-user] using outer image: {args.kata_image}") + announce(f"[end-user] using outer image: {args.kata_image}") host_proc_version = shell.run("cat /proc/version") shell.run("cd /root/asterinas") + announce("[end-user] starting Kata background services") shell.run("./tools/kata/kata_services.sh start", timeout=300) status_output = shell.run("./tools/kata/kata_services.sh status") if "Kata services are running." not in status_output: raise ScenarioError(f"Unexpected service status output:\n{status_output}") guest_proc_version, alpine_release = run_guest_workload(shell, args.workload_image, host_proc_version) - print(f"[end-user] guest /proc/version: {guest_proc_version}") - print(f"[end-user] alpine release: {alpine_release}") + announce(f"[end-user] guest /proc/version: {guest_proc_version}") + announce(f"[end-user] alpine release: {alpine_release}") + announce("[end-user] stopping Kata background services") shell.run("./tools/kata/kata_services.sh stop", timeout=300) return transcript_path finally: @@ -301,11 +331,13 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: ).start() try: - print(f"[kernel-developer] using outer image: {args.asterinas_image}") - print(f"[kernel-developer] mounted Asterinas tree: {args.asterinas_dir}") + announce(f"[kernel-developer] using outer image: {args.asterinas_image}") + announce(f"[kernel-developer] mounted Asterinas tree: {args.asterinas_dir}") host_proc_version = shell.run("cat /proc/version") shell.run("test -d /root/kata-containers/tools/kata") + announce("[kernel-developer] running tools/kata/kata_env.sh install") shell.run("./tools/kata/kata_env.sh install", timeout=3600) + announce("[kernel-developer] starting Kata background services") shell.run("./tools/kata/kata_services.sh start", timeout=300) status_output = shell.run("./tools/kata/kata_services.sh status") if "Kata services are running." not in status_output: @@ -316,10 +348,12 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: host_proc_version, "foo", ) - print(f"[kernel-developer] packaged guest /proc/version: {packaged_guest_proc_version}") - print(f"[kernel-developer] packaged alpine release: {packaged_alpine_release}") + announce(f"[kernel-developer] packaged guest /proc/version: {packaged_guest_proc_version}") + announce(f"[kernel-developer] packaged alpine release: {packaged_alpine_release}") + announce("[kernel-developer] building local kernel with make kernel BOOT_METHOD=qemu-direct") shell.run("cd /root/asterinas && make kernel BOOT_METHOD=qemu-direct", timeout=7200) shell.run("test -f /root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf") + announce("[kernel-developer] switching Kata to the locally built kernel") shell.run( "sed -i 's#^kernel = \".*\"#kernel = \"/root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf\"#' " "/etc/kata-containers/configuration.toml" @@ -334,8 +368,9 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: host_proc_version, "foo", ) - print(f"[kernel-developer] local-kernel guest /proc/version: {local_guest_proc_version}") - print(f"[kernel-developer] local-kernel alpine release: {local_alpine_release}") + announce(f"[kernel-developer] local-kernel guest /proc/version: {local_guest_proc_version}") + announce(f"[kernel-developer] local-kernel alpine release: {local_alpine_release}") + announce("[kernel-developer] stopping Kata background services") shell.run("./tools/kata/kata_services.sh stop", timeout=300) return transcript_path finally: @@ -390,18 +425,18 @@ def main() -> int: if args.scenario in {"kernel-developer", "all"}: transcripts.append(run_kernel_developer_scenario(args)) except Exception as error: - print(f"interactive_doc_test failed: {error}", file=sys.stderr) + announce(f"interactive_doc_test failed: {error}") if transcripts: - print("available transcript(s):", file=sys.stderr) + announce("available transcript(s):") for transcript in transcripts: - print(f" - {transcript}", file=sys.stderr) + announce(f" - {transcript}") else: - print(f"transcript directory: {args.log_dir}", file=sys.stderr) + announce(f"transcript directory: {args.log_dir}") return 1 - print("interactive_doc_test passed") + announce("interactive_doc_test passed") for transcript in transcripts: - print(f"transcript: {transcript}") + announce(f"transcript: {transcript}") return 0 From e9b04de715be879ebbc1e5a3e580984bb83fa5e3 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 04:21:46 +0000 Subject: [PATCH 05/20] ci: speed up kata static install --- docs/asterinas-kata-doc-workflow-progress.md | 1 + tools/kata/kata_env.sh | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 937b70ed4..452950280 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -44,6 +44,7 @@ - the first push run also showed that `/dev/vsock` is not guaranteed on GitHub-hosted runners, while `/dev/kvm`, `/dev/vhost-net`, and `/dev/vhost-vsock` are present; the workflow and `pexpect` driver now treat `/dev/vsock` as optional instead of mandatory - based on user feedback, the `pexpect` driver now streams child output to stdout and prints explicit phase markers before long-running commands such as `kata_env.sh install`, `nerdctl run`, and `make kernel` - the guest-entry helper now fails fast when `nerdctl run -it ...` falls back to the outer shell instead of opening the inner guest shell, so proxy/image failures no longer look like a generic hang + - CI logs showed the real long pole inside `tools/kata/kata_env.sh install`: the static tarball path unpacked into a temporary directory and then copied a second 4.7 GiB tree into `/opt`; this has now been collapsed into a direct `tar --zstd -xf ... -C / opt/kata` install path, with explicit progress messages around the install phases - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/kata_env.sh b/tools/kata/kata_env.sh index 69c6bd632..d24d41a2c 100755 --- a/tools/kata/kata_env.sh +++ b/tools/kata/kata_env.sh @@ -431,23 +431,20 @@ install_kata_from_payload_image() { install_kata_from_static_tarball() { source_marker_path="${KATA_INSTALL_SOURCE_MARKER:-/opt/kata/.kata-install-source}" static_tarball_url="$(resolve_static_tarball_url)" - extract_dir="${KATA_STATIC_EXTRACT_DIR:-/tmp/kata-static-extract}" static_tarball_sha256="$(resolve_static_tarball_sha256)" static_tarball_path="$(prepare_cached_static_tarball)" - rm -rf "${extract_dir}" - install -d -m 0755 "${extract_dir}" - tar --zstd -xf "${static_tarball_path}" -C "${extract_dir}" - - test -d "${extract_dir}/opt/kata" + echo "Installing Kata static tarball from ${static_tarball_url}" rm -rf /opt/kata - cp -a "${extract_dir}/opt/kata" /opt/ + tar --zstd -xf "${static_tarball_path}" -C / opt/kata + test -d /opt/kata { printf 'static-tarball-url %s\n' "${static_tarball_url}" printf 'static-tarball-sha256 %s\n' "${static_tarball_sha256}" } > "${source_marker_path}" test -x /opt/kata/bin/kata-runtime test -x /opt/kata/bin/containerd-shim-kata-v2 + echo "Installed Kata static tarball into /opt/kata" } patch_qemu_config_for_asterinas() { @@ -545,9 +542,11 @@ wait_for_containerd_ready() { } run_install_task() { + echo "Installing required distro packages" install_required_packages if need_nerdctl_install; then + echo "Installing nerdctl ${NERDCTL_VERSION}" download_release_asset \ /tmp/nerdctl.tgz \ "https://github.com/containerd/nerdctl/releases/download/${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION#v}-linux-amd64.tar.gz" @@ -555,6 +554,7 @@ run_install_task() { fi if should_install_crictl && need_crictl_install; then + echo "Installing crictl ${CRICTL_VERSION}" download_release_asset \ /tmp/crictl.tgz \ "https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRICTL_VERSION}/crictl-${CRICTL_VERSION}-linux-amd64.tar.gz" @@ -563,10 +563,13 @@ run_install_task() { if need_kata_install; then if [ -n "${KATA_ASTERINAS_KERNEL_PATH:-}" ] && [ -f "${KATA_ASTERINAS_KERNEL_PATH}" ]; then + echo "Installing Kata with local Asterinas kernel overlay" install_kata_from_asterinas_kernel_overlay elif [ -n "${KATA_STATIC_TARBALL_URL:-}" ] || [ -n "${KATA_STATIC_TARBALL_RELEASE_REPO:-}" ]; then + echo "Installing Kata from static tarball" install_kata_from_static_tarball else + echo "Installing Kata from payload image" install_kata_from_payload_image fi fi @@ -574,6 +577,7 @@ run_install_task() { install -d -m 0755 /usr/local/bin ln -sf /opt/kata/bin/kata-runtime /usr/local/bin/kata-runtime ln -sf /opt/kata/bin/containerd-shim-kata-v2 /usr/local/bin/containerd-shim-kata-v2 + echo "Kata install task completed" } run_check_task() { From f55faec9ff8c83058571b821069cc2ffbd4a9779 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 04:36:33 +0000 Subject: [PATCH 06/20] ci: fix kata static tar extraction --- docs/asterinas-kata-doc-workflow-progress.md | 1 + tools/kata/kata_env.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 452950280..1baad3a40 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -45,6 +45,7 @@ - based on user feedback, the `pexpect` driver now streams child output to stdout and prints explicit phase markers before long-running commands such as `kata_env.sh install`, `nerdctl run`, and `make kernel` - the guest-entry helper now fails fast when `nerdctl run -it ...` falls back to the outer shell instead of opening the inner guest shell, so proxy/image failures no longer look like a generic hang - CI logs showed the real long pole inside `tools/kata/kata_env.sh install`: the static tarball path unpacked into a temporary directory and then copied a second 4.7 GiB tree into `/opt`; this has now been collapsed into a direct `tar --zstd -xf ... -C / opt/kata` install path, with explicit progress messages around the install phases + - the next CI log sample exposed a follow-on bug in that optimization: the tarball did not contain a directly addressable `opt/kata` member for selective extraction, so the install now unpacks the verified tarball directly at `/` instead - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/kata_env.sh b/tools/kata/kata_env.sh index d24d41a2c..8c868b4e8 100755 --- a/tools/kata/kata_env.sh +++ b/tools/kata/kata_env.sh @@ -436,7 +436,7 @@ install_kata_from_static_tarball() { echo "Installing Kata static tarball from ${static_tarball_url}" rm -rf /opt/kata - tar --zstd -xf "${static_tarball_path}" -C / opt/kata + tar --zstd -xf "${static_tarball_path}" -C / test -d /opt/kata { printf 'static-tarball-url %s\n' "${static_tarball_url}" From e688ff4fdbb32f6ea6601538c093053e592ab1f3 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 04:44:11 +0000 Subject: [PATCH 07/20] ci: pre-resolve kata static release assets --- .../publish-asterinas-kata-image.yml | 25 ++++++++++++++++++ .../workflows/test-asterinas-kata-docs.yml | 26 +++++++++++++++++++ docs/asterinas-kata-doc-workflow-progress.md | 1 + tools/packaging/asterinas-kata/Dockerfile | 2 ++ 4 files changed, 54 insertions(+) diff --git a/.github/workflows/publish-asterinas-kata-image.yml b/.github/workflows/publish-asterinas-kata-image.yml index 41848ee64..a029060d9 100644 --- a/.github/workflows/publish-asterinas-kata-image.yml +++ b/.github/workflows/publish-asterinas-kata-image.yml @@ -39,6 +39,7 @@ jobs: docker_image_version: ${{ steps.resolve.outputs.docker_image_version }} kata_static_tarball_release_repo: ${{ steps.resolve.outputs.kata_static_tarball_release_repo }} kata_static_tarball_url: ${{ steps.resolve.outputs.kata_static_tarball_url }} + kata_static_tarball_sha256: ${{ steps.resolve.outputs.kata_static_tarball_sha256 }} should_push: ${{ steps.resolve.outputs.should_push }} steps: - name: Resolve Publish Inputs @@ -67,6 +68,27 @@ jobs: image_repository="${WORKFLOW_IMAGE_REPOSITORY:-${DEFAULT_IMAGE_REPOSITORY}}" kata_static_tarball_release_repo="${WORKFLOW_RELEASE_REPO:-${GITHUB_REPOSITORY}}" kata_static_tarball_url="${WORKFLOW_STATIC_TARBALL_URL:-}" + kata_static_tarball_sha256= + if [ -z "${kata_static_tarball_url}" ]; then + kata_static_tarball_url="$( + gh api "repos/${kata_static_tarball_release_repo}/releases/latest" --jq ' + .assets[] + | select(.name | test("^kata-static-.*-asterinas-amd64\\.tar\\.zst$")) + | .browser_download_url + ' | head -n 1 + )" + fi + test -n "${kata_static_tarball_url}" + kata_static_tarball_name="${kata_static_tarball_url##*/}" + kata_static_tarball_sha256="$( + gh api "repos/${kata_static_tarball_release_repo}/releases/latest" --jq ' + .assets[] + | select(.name | test("^kata-static-.*-asterinas-amd64\\.SHA256SUMS$")) + | .browser_download_url + ' | head -n 1 | + xargs curl -fsSL | + awk -v asset_name="${kata_static_tarball_name}" '$2 == asset_name { print $1; exit }' + )" should_push=false if [ -n "${DOCKERHUB_USERNAME}" ] && [ -n "${DOCKERHUB_TOKEN}" ]; then @@ -84,6 +106,7 @@ jobs: echo "docker_image_version=${docker_image_version}" >> "${GITHUB_OUTPUT}" echo "kata_static_tarball_release_repo=${kata_static_tarball_release_repo}" >> "${GITHUB_OUTPUT}" echo "kata_static_tarball_url=${kata_static_tarball_url}" >> "${GITHUB_OUTPUT}" + echo "kata_static_tarball_sha256=${kata_static_tarball_sha256}" >> "${GITHUB_OUTPUT}" echo "should_push=${should_push}" >> "${GITHUB_OUTPUT}" { @@ -99,6 +122,7 @@ jobs: else echo "- Explicit Kata tarball URL: not set" fi + echo "- Kata tarball SHA256: \`${kata_static_tarball_sha256}\`" echo "- Push enabled: \`${should_push}\`" } >> "${GITHUB_STEP_SUMMARY}" @@ -145,6 +169,7 @@ jobs: ASTERINAS_BASE_IMAGE=${{ env.ASTERINAS_BASE_IMAGE }} KATA_STATIC_TARBALL_RELEASE_REPO=${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_release_repo }} KATA_STATIC_TARBALL_URL=${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_url }} + KATA_STATIC_TARBALL_SHA256=${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_sha256 }} ASTERINAS_RELEASE_VERSION=${{ needs.resolve-upstream-asterinas-image.outputs.asterinas_version }} ASTERINAS_DOCKER_IMAGE_VERSION=${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }} diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index ae0f333ee..a10b33cea 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -16,6 +16,8 @@ jobs: outputs: asterinas_version: ${{ steps.resolve.outputs.asterinas_version }} docker_image_version: ${{ steps.resolve.outputs.docker_image_version }} + kata_static_tarball_url: ${{ steps.resolve.outputs.kata_static_tarball_url }} + kata_static_tarball_sha256: ${{ steps.resolve.outputs.kata_static_tarball_sha256 }} steps: - name: Resolve Upstream Asterinas Image Metadata id: resolve @@ -33,17 +35,39 @@ jobs: gh api repos/asterinas/asterinas/contents/DOCKER_IMAGE_VERSION?ref=main --jq .content | base64 -d | tr -d '\n' )" + kata_static_tarball_url="$( + gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq ' + .assets[] + | select(.name | test("^kata-static-.*-asterinas-amd64\\.tar\\.zst$")) + | .browser_download_url + ' | head -n 1 + )" + test -n "${kata_static_tarball_url}" + kata_static_tarball_name="${kata_static_tarball_url##*/}" + kata_static_tarball_sha256="$( + gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq ' + .assets[] + | select(.name | test("^kata-static-.*-asterinas-amd64\\.SHA256SUMS$")) + | .browser_download_url + ' | head -n 1 | + xargs curl -fsSL | + awk -v asset_name="${kata_static_tarball_name}" '$2 == asset_name { print $1; exit }' + )" test -n "${asterinas_version}" test -n "${docker_image_version}" + test -n "${kata_static_tarball_sha256}" echo "asterinas_version=${asterinas_version}" >> "${GITHUB_OUTPUT}" echo "docker_image_version=${docker_image_version}" >> "${GITHUB_OUTPUT}" + echo "kata_static_tarball_url=${kata_static_tarball_url}" >> "${GITHUB_OUTPUT}" + echo "kata_static_tarball_sha256=${kata_static_tarball_sha256}" >> "${GITHUB_OUTPUT}" { echo '## Resolved upstream Asterinas image' echo echo "- \`VERSION\`: \`${asterinas_version}\`" echo "- \`DOCKER_IMAGE_VERSION\`: \`${docker_image_version}\`" echo "- Base image: \`asterinas/asterinas:${docker_image_version}\`" + echo "- Kata static tarball: \`${kata_static_tarball_url}\`" } >> "${GITHUB_STEP_SUMMARY}" test-documented-end-user-flow: @@ -109,6 +133,8 @@ jobs: env: KATA_DOC_LOG_DIR: /tmp/asterinas-kata-doc-logs/kernel-developer KATA_STATIC_TARBALL_RELEASE_REPO: ${{ github.repository }} + KATA_STATIC_TARBALL_URL: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_url }} + KATA_STATIC_TARBALL_SHA256: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_sha256 }} steps: - uses: actions/checkout@v4 diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 1baad3a40..543f6642b 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -46,6 +46,7 @@ - the guest-entry helper now fails fast when `nerdctl run -it ...` falls back to the outer shell instead of opening the inner guest shell, so proxy/image failures no longer look like a generic hang - CI logs showed the real long pole inside `tools/kata/kata_env.sh install`: the static tarball path unpacked into a temporary directory and then copied a second 4.7 GiB tree into `/opt`; this has now been collapsed into a direct `tar --zstd -xf ... -C / opt/kata` install path, with explicit progress messages around the install phases - the next CI log sample exposed a follow-on bug in that optimization: the tarball did not contain a directly addressable `opt/kata` member for selective extraction, so the install now unpacks the verified tarball directly at `/` instead + - the latest CI round also exposed an unrelated but real stability issue in `Publish | Asterinas Kata Image`: the Docker build resolved the latest release URL from inside the container and hit unauthenticated GitHub API rate limits, so the workflows now resolve the tarball URL and SHA256 up front and pass them into the container explicitly - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/packaging/asterinas-kata/Dockerfile b/tools/packaging/asterinas-kata/Dockerfile index f6773e0e3..647ded2d1 100644 --- a/tools/packaging/asterinas-kata/Dockerfile +++ b/tools/packaging/asterinas-kata/Dockerfile @@ -3,6 +3,7 @@ FROM ${ASTERINAS_BASE_IMAGE} ARG KATA_STATIC_TARBALL_RELEASE_REPO=kata-containers/kata-containers ARG KATA_STATIC_TARBALL_URL= +ARG KATA_STATIC_TARBALL_SHA256= ARG ASTERINAS_RELEASE_VERSION= ARG ASTERINAS_DOCKER_IMAGE_VERSION= @@ -12,6 +13,7 @@ COPY tools/kata /root/asterinas/tools/kata RUN KATA_STATIC_TARBALL_RELEASE_REPO="${KATA_STATIC_TARBALL_RELEASE_REPO}" \ KATA_STATIC_TARBALL_URL="${KATA_STATIC_TARBALL_URL}" \ + KATA_STATIC_TARBALL_SHA256="${KATA_STATIC_TARBALL_SHA256}" \ bash /root/asterinas/tools/kata/kata_env.sh install RUN printf '%s\n' "${ASTERINAS_RELEASE_VERSION}" > /root/asterinas/.asterinas-version && \ From f939047cbd8b7b5a5d2a8e963669a2109bcd0ab2 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 04:49:04 +0000 Subject: [PATCH 08/20] ci: match release checksums by suffix --- .github/workflows/publish-asterinas-kata-image.yml | 2 +- .github/workflows/test-asterinas-kata-docs.yml | 2 +- docs/asterinas-kata-doc-workflow-progress.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-asterinas-kata-image.yml b/.github/workflows/publish-asterinas-kata-image.yml index a029060d9..75f198eb7 100644 --- a/.github/workflows/publish-asterinas-kata-image.yml +++ b/.github/workflows/publish-asterinas-kata-image.yml @@ -87,7 +87,7 @@ jobs: | .browser_download_url ' | head -n 1 | xargs curl -fsSL | - awk -v asset_name="${kata_static_tarball_name}" '$2 == asset_name { print $1; exit }' + awk -v asset_name="${kata_static_tarball_name}" '$2 == asset_name || $2 ~ ("/" asset_name "$") { print $1; exit }' )" should_push=false diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index a10b33cea..f69ab320c 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -51,7 +51,7 @@ jobs: | .browser_download_url ' | head -n 1 | xargs curl -fsSL | - awk -v asset_name="${kata_static_tarball_name}" '$2 == asset_name { print $1; exit }' + awk -v asset_name="${kata_static_tarball_name}" '$2 == asset_name || $2 ~ ("/" asset_name "$") { print $1; exit }' )" test -n "${asterinas_version}" test -n "${docker_image_version}" diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 543f6642b..fc11faea3 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -47,6 +47,7 @@ - CI logs showed the real long pole inside `tools/kata/kata_env.sh install`: the static tarball path unpacked into a temporary directory and then copied a second 4.7 GiB tree into `/opt`; this has now been collapsed into a direct `tar --zstd -xf ... -C / opt/kata` install path, with explicit progress messages around the install phases - the next CI log sample exposed a follow-on bug in that optimization: the tarball did not contain a directly addressable `opt/kata` member for selective extraction, so the install now unpacks the verified tarball directly at `/` instead - the latest CI round also exposed an unrelated but real stability issue in `Publish | Asterinas Kata Image`: the Docker build resolved the latest release URL from inside the container and hit unauthenticated GitHub API rate limits, so the workflows now resolve the tarball URL and SHA256 up front and pass them into the container explicitly + - after that pre-resolution change, the next push showed a format mismatch in the release `SHA256SUMS`: the checksum file records the tarball with a full path suffix instead of only the basename, so the workflow-side SHA lookup now accepts either an exact basename match or a trailing `/${asset_name}` - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes From 7ddd8d6ed7aa2340ed4808fa3f985bbcfeb44b6c Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 05:07:51 +0000 Subject: [PATCH 09/20] ci: dump kata console logs on failure --- docs/asterinas-kata-doc-workflow-progress.md | 2 + tools/kata/interactive_doc_test.py | 90 ++++++++++++++------ 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index fc11faea3..5660193c7 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -48,6 +48,8 @@ - the next CI log sample exposed a follow-on bug in that optimization: the tarball did not contain a directly addressable `opt/kata` member for selective extraction, so the install now unpacks the verified tarball directly at `/` instead - the latest CI round also exposed an unrelated but real stability issue in `Publish | Asterinas Kata Image`: the Docker build resolved the latest release URL from inside the container and hit unauthenticated GitHub API rate limits, so the workflows now resolve the tarball URL and SHA256 up front and pass them into the container explicitly - after that pre-resolution change, the next push showed a format mismatch in the release `SHA256SUMS`: the checksum file records the tarball with a full path suffix instead of only the basename, so the workflow-side SHA lookup now accepts either an exact basename match or a trailing `/${asset_name}` + - configuration comparison result: before the local-kernel override, both the end-user flow and the kernel-developer flow start Kata from the same default config target, `/opt/kata/share/defaults/kata-containers/configuration.toml -> configuration-asterinas.toml` + - on interactive failures, `tools/kata/interactive_doc_test.py` now also dumps `/tmp/kata-console.log`, `/tmp/console.log`, `/tmp/kata-qemu-serial.log`, `/tmp/qemu-serial.log`, `/tmp/containerd.log`, and `/tmp/kata-syslog.log` so QEMU-side failures are visible immediately - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py index d30fade89..1240d2640 100644 --- a/tools/kata/interactive_doc_test.py +++ b/tools/kata/interactive_doc_test.py @@ -218,33 +218,73 @@ def announce(message: str) -> None: print(message, flush=True) +def dump_kata_debug_logs(shell: DockerShell, context: str) -> None: + announce(f"[debug] dumping Kata/QEMU logs after {context}") + for log_path in ( + "/tmp/kata-console.log", + "/tmp/console.log", + "/tmp/kata-qemu-serial.log", + "/tmp/qemu-serial.log", + "/tmp/containerd.log", + "/tmp/kata-syslog.log", + ): + output = shell.run( + f'test -f {shlex.quote(log_path)} && {{ echo "--- {log_path} ---"; tail -n 200 {shlex.quote(log_path)}; }} || true', + check=False, + timeout=60, + ) + if output: + announce(output) + + +def log_active_kata_configs(shell: DockerShell, label: str) -> None: + announce(f"[{label}] active Kata default config:") + announce( + shell.run( + "readlink -f /opt/kata/share/defaults/kata-containers/configuration.toml", + timeout=60, + ) + ) + announce(f"[{label}] active runtime config:") + announce( + shell.run( + "readlink -f /etc/kata-containers/configuration.toml 2>/dev/null || echo /etc/kata-containers/configuration.toml", + timeout=60, + ) + ) + + def run_guest_workload(shell: DockerShell, workload_image: str, host_proc_version: str, container_name: str = "foo") -> tuple[str, str]: announce(f"[guest] launching workload container {container_name} from {workload_image}") shell.run(f"nerdctl rm -f {shlex.quote(container_name)} >/dev/null 2>&1 || true", check=False) - shell.enter_guest( - " ".join( - [ - "nerdctl", - "run", - "--cgroup-manager", - "cgroupfs", - "--net", - "none", - "--runtime", - "io.containerd.kata.v2", - "--name", - shlex.quote(container_name), - "-it", - shlex.quote(workload_image), - ] - ), - timeout=900, - ) - guest_proc_version = shell.run("cat /proc/version") - alpine_release = shell.run("cat /etc/alpine-release") - shell.exit_guest() - announce(f"[guest] removing workload container {container_name}") - shell.run(f"nerdctl rm -f {shlex.quote(container_name)}") + try: + shell.enter_guest( + " ".join( + [ + "nerdctl", + "run", + "--cgroup-manager", + "cgroupfs", + "--net", + "none", + "--runtime", + "io.containerd.kata.v2", + "--name", + shlex.quote(container_name), + "-it", + shlex.quote(workload_image), + ] + ), + timeout=900, + ) + guest_proc_version = shell.run("cat /proc/version") + alpine_release = shell.run("cat /etc/alpine-release") + shell.exit_guest() + announce(f"[guest] removing workload container {container_name}") + shell.run(f"nerdctl rm -f {shlex.quote(container_name)}") + except Exception: + dump_kata_debug_logs(shell, f"failed nerdctl run for {container_name}") + raise if guest_proc_version.strip() == host_proc_version.strip(): raise ScenarioError("Guest /proc/version unexpectedly matches the outer container /proc/version") @@ -285,6 +325,7 @@ def run_end_user_scenario(args: argparse.Namespace) -> pathlib.Path: shell.run("cd /root/asterinas") announce("[end-user] starting Kata background services") shell.run("./tools/kata/kata_services.sh start", timeout=300) + log_active_kata_configs(shell, "end-user") status_output = shell.run("./tools/kata/kata_services.sh status") if "Kata services are running." not in status_output: raise ScenarioError(f"Unexpected service status output:\n{status_output}") @@ -339,6 +380,7 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: shell.run("./tools/kata/kata_env.sh install", timeout=3600) announce("[kernel-developer] starting Kata background services") shell.run("./tools/kata/kata_services.sh start", timeout=300) + log_active_kata_configs(shell, "kernel-developer") status_output = shell.run("./tools/kata/kata_services.sh status") if "Kata services are running." not in status_output: raise ScenarioError(f"Unexpected service status output:\n{status_output}") From e4d5a7c8d4da76019a56ac1a2965f646c500a1c4 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 05:20:54 +0000 Subject: [PATCH 10/20] ci: prebuild asterinas kernel for docs flow --- .github/workflows/test-asterinas-kata-docs.yml | 10 ++++++++++ docs/asterinas-kata-doc-workflow-progress.md | 1 + 2 files changed, 11 insertions(+) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index f69ab320c..3e9225e9f 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -178,6 +178,16 @@ jobs: set -euxo pipefail docker pull "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" + - name: Prebuild Local Asterinas Kernel + shell: bash + run: | + set -euxo pipefail + docker run --rm \ + -v "${HOME}/asterinas:/root/asterinas" \ + -w /root/asterinas \ + "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ + bash -lc 'make kernel BOOT_METHOD=qemu-direct' + - name: Replay Documented Kernel-Developer Flow shell: bash run: | diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 5660193c7..d841690e0 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -50,6 +50,7 @@ - after that pre-resolution change, the next push showed a format mismatch in the release `SHA256SUMS`: the checksum file records the tarball with a full path suffix instead of only the basename, so the workflow-side SHA lookup now accepts either an exact basename match or a trailing `/${asset_name}` - configuration comparison result: before the local-kernel override, both the end-user flow and the kernel-developer flow start Kata from the same default config target, `/opt/kata/share/defaults/kata-containers/configuration.toml -> configuration-asterinas.toml` - on interactive failures, `tools/kata/interactive_doc_test.py` now also dumps `/tmp/kata-console.log`, `/tmp/console.log`, `/tmp/kata-qemu-serial.log`, `/tmp/qemu-serial.log`, `/tmp/containerd.log`, and `/tmp/kata-syslog.log` so QEMU-side failures are visible immediately + - to keep the interactive developer replay within the expected post-install budget, the CI job now prebuilds the mounted `$HOME/asterinas` kernel once before invoking the `pexpect` driver; the driver still runs the documented `make kernel BOOT_METHOD=qemu-direct` command inside the container, but that command should now hit an already-built tree and return quickly - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes From 15cd778d5fc93fecd12111d2ad315e12300c0c67 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 05:34:59 +0000 Subject: [PATCH 11/20] ci: fix interactive command marker handling --- docs/asterinas-kata-doc-workflow-progress.md | 1 + tools/kata/interactive_doc_test.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index d841690e0..3b7cd17ee 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -51,6 +51,7 @@ - configuration comparison result: before the local-kernel override, both the end-user flow and the kernel-developer flow start Kata from the same default config target, `/opt/kata/share/defaults/kata-containers/configuration.toml -> configuration-asterinas.toml` - on interactive failures, `tools/kata/interactive_doc_test.py` now also dumps `/tmp/kata-console.log`, `/tmp/console.log`, `/tmp/kata-qemu-serial.log`, `/tmp/qemu-serial.log`, `/tmp/containerd.log`, and `/tmp/kata-syslog.log` so QEMU-side failures are visible immediately - to keep the interactive developer replay within the expected post-install budget, the CI job now prebuilds the mounted `$HOME/asterinas` kernel once before invoking the `pexpect` driver; the driver still runs the documented `make kernel BOOT_METHOD=qemu-direct` command inside the container, but that command should now hit an already-built tree and return quickly + - a deeper replay hang was then traced to the Python driver itself rather than Kata: `DockerShell.run()` sent the target command, `status=$?`, and the exit-marker `printf` as three separate queued lines, which is fragile for long-running interactive commands. The driver now wraps each command and exit-marker emission into one shell line so it can no longer stall waiting for a marker that was never executed - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py index 1240d2640..763abc068 100644 --- a/tools/kata/interactive_doc_test.py +++ b/tools/kata/interactive_doc_test.py @@ -180,11 +180,10 @@ def set_prompt(self, prompt: str) -> None: def run(self, command: str, timeout: int = 300, check: bool = True) -> str: assert self.child is not None marker = f"__DOC_TEST_EXIT__{uuid.uuid4().hex}__:" - self.child.sendline(command) - self.child.sendline("status=$?") - self.child.sendline(f"printf '{marker}%s\\n' \"$status\"") + wrapped_command = f"{command}; __doc_test_status=$?; printf '{marker}%s\\n' \"$__doc_test_status\"" + self.child.sendline(wrapped_command) self.child.expect(re.compile(re.escape(marker) + r"(\d+)\r?\n"), timeout=timeout) - output = clean_command_output(command, self.child.before) + output = clean_command_output(wrapped_command, self.child.before) status = int(self.child.match.group(1)) self.child.expect(re.escape(self.prompt), timeout=60) if check and status != 0: From 5bb9d71492ca287f246a10bc16578a6e05718c91 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 05:43:45 +0000 Subject: [PATCH 12/20] ci: share asterinas build caches in docs flow --- .../workflows/test-asterinas-kata-docs.yml | 7 +++++++ docs/asterinas-kata-doc-workflow-progress.md | 1 + tools/kata/interactive_doc_test.py | 21 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index 3e9225e9f..f0122b75c 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -135,6 +135,9 @@ jobs: KATA_STATIC_TARBALL_RELEASE_REPO: ${{ github.repository }} KATA_STATIC_TARBALL_URL: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_url }} KATA_STATIC_TARBALL_SHA256: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_sha256 }} + KATA_DOC_TEST_NIX_DIR: ${{ runner.temp }}/asterinas-kernel-cache/nix + KATA_DOC_TEST_CARGO_HOME: ${{ runner.temp }}/asterinas-kernel-cache/cargo + KATA_DOC_TEST_RUSTUP_HOME: ${{ runner.temp }}/asterinas-kernel-cache/rustup steps: - uses: actions/checkout@v4 @@ -182,8 +185,12 @@ jobs: shell: bash run: | set -euxo pipefail + mkdir -p "${KATA_DOC_TEST_NIX_DIR}" "${KATA_DOC_TEST_CARGO_HOME}" "${KATA_DOC_TEST_RUSTUP_HOME}" docker run --rm \ -v "${HOME}/asterinas:/root/asterinas" \ + -v "${KATA_DOC_TEST_NIX_DIR}:/nix" \ + -v "${KATA_DOC_TEST_CARGO_HOME}:/root/.cargo" \ + -v "${KATA_DOC_TEST_RUSTUP_HOME}:/root/.rustup" \ -w /root/asterinas \ "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ bash -lc 'make kernel BOOT_METHOD=qemu-direct' diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 3b7cd17ee..b8f20b4a8 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -52,6 +52,7 @@ - on interactive failures, `tools/kata/interactive_doc_test.py` now also dumps `/tmp/kata-console.log`, `/tmp/console.log`, `/tmp/kata-qemu-serial.log`, `/tmp/qemu-serial.log`, `/tmp/containerd.log`, and `/tmp/kata-syslog.log` so QEMU-side failures are visible immediately - to keep the interactive developer replay within the expected post-install budget, the CI job now prebuilds the mounted `$HOME/asterinas` kernel once before invoking the `pexpect` driver; the driver still runs the documented `make kernel BOOT_METHOD=qemu-direct` command inside the container, but that command should now hit an already-built tree and return quickly - a deeper replay hang was then traced to the Python driver itself rather than Kata: `DockerShell.run()` sent the target command, `status=$?`, and the exit-marker `printf` as three separate queued lines, which is fragile for long-running interactive commands. The driver now wraps each command and exit-marker emission into one shell line so it can no longer stall waiting for a marker that was never executed + - the newest transcript showed the remaining long pole after `kata_env.sh install`: the interactive `make kernel BOOT_METHOD=qemu-direct` reinitialized `/nix`, cargo, and rustup state inside the developer container. The workflow now prebuilds with shared cache mounts and passes the same cache mounts into the interactive container so the documented in-container `make kernel` can reuse the same build state - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py index 763abc068..df0f5f6b1 100644 --- a/tools/kata/interactive_doc_test.py +++ b/tools/kata/interactive_doc_test.py @@ -121,6 +121,26 @@ def outer_docker_device_args() -> list[str]: return args +def kernel_developer_cache_mount_args() -> list[str]: + args: list[str] = [] + cache_mounts = ( + ("KATA_DOC_TEST_NIX_DIR", "/nix"), + ("KATA_DOC_TEST_CARGO_HOME", "/root/.cargo"), + ("KATA_DOC_TEST_RUSTUP_HOME", "/root/.rustup"), + ) + + for env_name, container_path in cache_mounts: + host_path = os.environ.get(env_name) + if not host_path: + continue + + host_dir = pathlib.Path(host_path) + host_dir.mkdir(parents=True, exist_ok=True) + args.extend(["-v", f"{host_dir.resolve()}:{container_path}"]) + + return args + + class DockerShell: def __init__(self, name: str, docker_args: list[str], transcript_path: pathlib.Path) -> None: self.name = name @@ -359,6 +379,7 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: "--tmpfs", "/var/lib/containerd:exec,mode=755,size=8g", *outer_docker_env_args(), + *kernel_developer_cache_mount_args(), "-v", f"{args.asterinas_dir.resolve()}:/root/asterinas", "-v", From fc8b10c75220acb048320cf300b4efcceb82d585 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 05:45:26 +0000 Subject: [PATCH 13/20] ci: use tmp paths for docs build caches --- .github/workflows/test-asterinas-kata-docs.yml | 6 +++--- docs/asterinas-kata-doc-workflow-progress.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index f0122b75c..e5d7f67e8 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -135,9 +135,9 @@ jobs: KATA_STATIC_TARBALL_RELEASE_REPO: ${{ github.repository }} KATA_STATIC_TARBALL_URL: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_url }} KATA_STATIC_TARBALL_SHA256: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_sha256 }} - KATA_DOC_TEST_NIX_DIR: ${{ runner.temp }}/asterinas-kernel-cache/nix - KATA_DOC_TEST_CARGO_HOME: ${{ runner.temp }}/asterinas-kernel-cache/cargo - KATA_DOC_TEST_RUSTUP_HOME: ${{ runner.temp }}/asterinas-kernel-cache/rustup + KATA_DOC_TEST_NIX_DIR: /tmp/asterinas-kernel-cache/nix + KATA_DOC_TEST_CARGO_HOME: /tmp/asterinas-kernel-cache/cargo + KATA_DOC_TEST_RUSTUP_HOME: /tmp/asterinas-kernel-cache/rustup steps: - uses: actions/checkout@v4 diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index b8f20b4a8..88478dd56 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -53,6 +53,7 @@ - to keep the interactive developer replay within the expected post-install budget, the CI job now prebuilds the mounted `$HOME/asterinas` kernel once before invoking the `pexpect` driver; the driver still runs the documented `make kernel BOOT_METHOD=qemu-direct` command inside the container, but that command should now hit an already-built tree and return quickly - a deeper replay hang was then traced to the Python driver itself rather than Kata: `DockerShell.run()` sent the target command, `status=$?`, and the exit-marker `printf` as three separate queued lines, which is fragile for long-running interactive commands. The driver now wraps each command and exit-marker emission into one shell line so it can no longer stall waiting for a marker that was never executed - the newest transcript showed the remaining long pole after `kata_env.sh install`: the interactive `make kernel BOOT_METHOD=qemu-direct` reinitialized `/nix`, cargo, and rustup state inside the developer container. The workflow now prebuilds with shared cache mounts and passes the same cache mounts into the interactive container so the documented in-container `make kernel` can reuse the same build state + - the first cache-sharing attempt repeated the earlier workflow-expression mistake by putting `runner.temp` into job-level `env`; the cache directories are now normalized to plain `/tmp/asterinas-kernel-cache/...` - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes From 57e1d1380eb1a34125344f54577e5e1364c8baa3 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 05:58:06 +0000 Subject: [PATCH 14/20] ci: prewarm builder image for docs flow --- .../workflows/test-asterinas-kata-docs.yml | 23 +++++++++++-------- docs/asterinas-kata-doc-workflow-progress.md | 1 + 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index e5d7f67e8..ae58c6c5b 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -135,9 +135,6 @@ jobs: KATA_STATIC_TARBALL_RELEASE_REPO: ${{ github.repository }} KATA_STATIC_TARBALL_URL: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_url }} KATA_STATIC_TARBALL_SHA256: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_sha256 }} - KATA_DOC_TEST_NIX_DIR: /tmp/asterinas-kernel-cache/nix - KATA_DOC_TEST_CARGO_HOME: /tmp/asterinas-kernel-cache/cargo - KATA_DOC_TEST_RUSTUP_HOME: /tmp/asterinas-kernel-cache/rustup steps: - uses: actions/checkout@v4 @@ -181,19 +178,25 @@ jobs: set -euxo pipefail docker pull "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" + - name: Define Prewarmed Builder Image Name + shell: bash + run: | + set -euxo pipefail + echo "KATA_DOC_TEST_PREBUILT_IMAGE=local/asterinas-doc-prebuilt:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_ENV}" + - name: Prebuild Local Asterinas Kernel shell: bash run: | set -euxo pipefail - mkdir -p "${KATA_DOC_TEST_NIX_DIR}" "${KATA_DOC_TEST_CARGO_HOME}" "${KATA_DOC_TEST_RUSTUP_HOME}" - docker run --rm \ + docker rm -f asterinas-doc-prewarm >/dev/null 2>&1 || true + docker run -d --name asterinas-doc-prewarm \ -v "${HOME}/asterinas:/root/asterinas" \ - -v "${KATA_DOC_TEST_NIX_DIR}:/nix" \ - -v "${KATA_DOC_TEST_CARGO_HOME}:/root/.cargo" \ - -v "${KATA_DOC_TEST_RUSTUP_HOME}:/root/.rustup" \ -w /root/asterinas \ "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ - bash -lc 'make kernel BOOT_METHOD=qemu-direct' + bash -lc 'sleep infinity' + docker exec asterinas-doc-prewarm bash -lc 'make kernel BOOT_METHOD=qemu-direct' + docker commit asterinas-doc-prewarm "${KATA_DOC_TEST_PREBUILT_IMAGE}" + docker rm -f asterinas-doc-prewarm - name: Replay Documented Kernel-Developer Flow shell: bash @@ -201,7 +204,7 @@ jobs: set -euxo pipefail python3 -u tools/kata/interactive_doc_test.py \ --scenario kernel-developer \ - --asterinas-image "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ + --asterinas-image "${KATA_DOC_TEST_PREBUILT_IMAGE}" \ --asterinas-dir "${HOME}/asterinas" \ --log-dir "${KATA_DOC_LOG_DIR}" diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 88478dd56..ca8faac95 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -54,6 +54,7 @@ - a deeper replay hang was then traced to the Python driver itself rather than Kata: `DockerShell.run()` sent the target command, `status=$?`, and the exit-marker `printf` as three separate queued lines, which is fragile for long-running interactive commands. The driver now wraps each command and exit-marker emission into one shell line so it can no longer stall waiting for a marker that was never executed - the newest transcript showed the remaining long pole after `kata_env.sh install`: the interactive `make kernel BOOT_METHOD=qemu-direct` reinitialized `/nix`, cargo, and rustup state inside the developer container. The workflow now prebuilds with shared cache mounts and passes the same cache mounts into the interactive container so the documented in-container `make kernel` can reuse the same build state - the first cache-sharing attempt repeated the earlier workflow-expression mistake by putting `runner.temp` into job-level `env`; the cache directories are now normalized to plain `/tmp/asterinas-kernel-cache/...` + - a follow-up CI attempt showed that directly bind-mounting empty cache directories onto `/nix` and `/root/.cargo` hid the builder image's existing tooling (`nix-build`, `/root/.cargo/env`, etc.). The workflow now uses a safer prewarm approach instead: it prebuilds the kernel in a temporary container, commits that container to a local one-off image, and replays the documented interactive developer flow on top of that prewarmed image - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes From f5394f391c1f000e11ff572cd5cf4abb7dfc71c5 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 06:11:49 +0000 Subject: [PATCH 15/20] ci: force bash in doc replay containers --- docs/asterinas-kata-doc-workflow-progress.md | 1 + tools/kata/interactive_doc_test.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index ca8faac95..13c5ae8e8 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -55,6 +55,7 @@ - the newest transcript showed the remaining long pole after `kata_env.sh install`: the interactive `make kernel BOOT_METHOD=qemu-direct` reinitialized `/nix`, cargo, and rustup state inside the developer container. The workflow now prebuilds with shared cache mounts and passes the same cache mounts into the interactive container so the documented in-container `make kernel` can reuse the same build state - the first cache-sharing attempt repeated the earlier workflow-expression mistake by putting `runner.temp` into job-level `env`; the cache directories are now normalized to plain `/tmp/asterinas-kernel-cache/...` - a follow-up CI attempt showed that directly bind-mounting empty cache directories onto `/nix` and `/root/.cargo` hid the builder image's existing tooling (`nix-build`, `/root/.cargo/env`, etc.). The workflow now uses a safer prewarm approach instead: it prebuilds the kernel in a temporary container, commits that container to a local one-off image, and replays the documented interactive developer flow on top of that prewarmed image + - the first prewarmed-image replay then exposed one more subtle issue: `docker commit` preserved the prewarm container's running command, so the derived image no longer dropped into an interactive shell by default. The `pexpect` driver now explicitly appends `bash` when starting both the end-user and kernel-developer outer containers - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py index df0f5f6b1..32a2338cb 100644 --- a/tools/kata/interactive_doc_test.py +++ b/tools/kata/interactive_doc_test.py @@ -334,6 +334,7 @@ def run_end_user_scenario(args: argparse.Namespace) -> pathlib.Path: "/var/lib/containerd:exec,mode=755,size=8g", *outer_docker_env_args(), args.kata_image, + "bash", ], transcript_path, ).start() @@ -387,6 +388,7 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: "-w", "/root/kata-containers", args.asterinas_image, + "bash", ], transcript_path, ).start() From f128432b33a902c92f3d72e4fb751c8874fe8874 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 06:35:52 +0000 Subject: [PATCH 16/20] ci: align asterinas source and release assets --- .../workflows/test-asterinas-kata-docs.yml | 1 + .github/workflows/test-asterinas-kata.yml | 28 +++++++++++++++++++ docs/asterinas-kata-doc-workflow-progress.md | 2 ++ 3 files changed, 31 insertions(+) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index ae58c6c5b..bbd832754 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -142,6 +142,7 @@ jobs: uses: actions/checkout@v4 with: repository: asterinas/asterinas + ref: v${{ needs.resolve-upstream-asterinas-image.outputs.asterinas_version }} path: asterinas-src - name: Prepare Local Asterinas Tree diff --git a/.github/workflows/test-asterinas-kata.yml b/.github/workflows/test-asterinas-kata.yml index b2dc0f618..33d443676 100644 --- a/.github/workflows/test-asterinas-kata.yml +++ b/.github/workflows/test-asterinas-kata.yml @@ -20,6 +20,8 @@ jobs: outputs: asterinas_version: ${{ steps.resolve.outputs.asterinas_version }} docker_image_version: ${{ steps.resolve.outputs.docker_image_version }} + kata_static_tarball_url: ${{ steps.resolve.outputs.kata_static_tarball_url }} + kata_static_tarball_sha256: ${{ steps.resolve.outputs.kata_static_tarball_sha256 }} steps: - name: Resolve Upstream Asterinas Image Metadata id: resolve @@ -37,17 +39,39 @@ jobs: gh api repos/asterinas/asterinas/contents/DOCKER_IMAGE_VERSION?ref=main --jq .content | base64 -d | tr -d '\n' )" + kata_static_tarball_url="$( + gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq ' + .assets[] + | select(.name | test("^kata-static-.*-asterinas-amd64\\.tar\\.zst$")) + | .browser_download_url + ' | head -n 1 + )" + test -n "${kata_static_tarball_url}" + kata_static_tarball_name="${kata_static_tarball_url##*/}" + kata_static_tarball_sha256="$( + gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq ' + .assets[] + | select(.name | test("^kata-static-.*-asterinas-amd64\\.SHA256SUMS$")) + | .browser_download_url + ' | head -n 1 | + xargs curl -fsSL | + awk -v asset_name="${kata_static_tarball_name}" '$2 == asset_name || $2 ~ ("/" asset_name "$") { print $1; exit }' + )" test -n "${asterinas_version}" test -n "${docker_image_version}" + test -n "${kata_static_tarball_sha256}" echo "asterinas_version=${asterinas_version}" >> "${GITHUB_OUTPUT}" echo "docker_image_version=${docker_image_version}" >> "${GITHUB_OUTPUT}" + echo "kata_static_tarball_url=${kata_static_tarball_url}" >> "${GITHUB_OUTPUT}" + echo "kata_static_tarball_sha256=${kata_static_tarball_sha256}" >> "${GITHUB_OUTPUT}" { echo '## Resolved upstream Asterinas image' echo echo "- \`VERSION\`: \`${asterinas_version}\`" echo "- \`DOCKER_IMAGE_VERSION\`: \`${docker_image_version}\`" echo "- Base image: \`asterinas/asterinas:${docker_image_version}\`" + echo "- Kata static tarball: \`${kata_static_tarball_url}\`" } >> "${GITHUB_STEP_SUMMARY}" build-published-kata-image: @@ -88,6 +112,8 @@ jobs: build-args: | ASTERINAS_BASE_IMAGE=${{ env.ASTERINAS_BASE_IMAGE }} KATA_STATIC_TARBALL_RELEASE_REPO=${{ github.repository }} + KATA_STATIC_TARBALL_URL=${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_url }} + KATA_STATIC_TARBALL_SHA256=${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_sha256 }} ASTERINAS_RELEASE_VERSION=${{ needs.resolve-upstream-asterinas-image.outputs.asterinas_version }} ASTERINAS_DOCKER_IMAGE_VERSION=${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }} @@ -119,6 +145,8 @@ jobs: KATA_GUEST_KERNEL: ${{ matrix.guest_kernel }} KATA_TEST_IMAGE_LOG_LABEL: ${{ matrix.image_variant == 'source' && 'base image / guest kernel' || 'test image / guest kernel' }} KATA_TEST_IMAGE_LOG_VALUE: ${{ matrix.image_variant == 'source' && format('asterinas/asterinas:{0} / {1}', needs.resolve-upstream-asterinas-image.outputs.docker_image_version, matrix.guest_kernel) || format('asterinas/kata:{0} / {1}', needs.resolve-upstream-asterinas-image.outputs.docker_image_version, matrix.guest_kernel) }} + KATA_STATIC_TARBALL_URL: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_url }} + KATA_STATIC_TARBALL_SHA256: ${{ needs.resolve-upstream-asterinas-image.outputs.kata_static_tarball_sha256 }} defaults: run: shell: bash diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 13c5ae8e8..d8cd11d04 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -56,6 +56,8 @@ - the first cache-sharing attempt repeated the earlier workflow-expression mistake by putting `runner.temp` into job-level `env`; the cache directories are now normalized to plain `/tmp/asterinas-kernel-cache/...` - a follow-up CI attempt showed that directly bind-mounting empty cache directories onto `/nix` and `/root/.cargo` hid the builder image's existing tooling (`nix-build`, `/root/.cargo/env`, etc.). The workflow now uses a safer prewarm approach instead: it prebuilds the kernel in a temporary container, commits that container to a local one-off image, and replays the documented interactive developer flow on top of that prewarmed image - the first prewarmed-image replay then exposed one more subtle issue: `docker commit` preserved the prewarm container's running command, so the derived image no longer dropped into an interactive shell by default. The `pexpect` driver now explicitly appends `bash` when starting both the end-user and kernel-developer outer containers + - the latest developer replay reached the local-kernel launch stage and then timed out waiting for the guest agent after switching to `/root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf`. That CI path had been building the local kernel from upstream `asterinas/asterinas` `main`, which can drift from the published `asterinas/asterinas:` image and packaged Kata release. The next adjustment is to make the CI checkout use the matching upstream release tag `v${ASTERINAS_VERSION}` so the local source tree aligns with the image/runtime bundle it is being validated against + - the same release-asset pre-resolution fix also needs to be applied to `.github/workflows/test-asterinas-kata.yml`, because its published-image matrix still calls `tools/kata/kata_env.sh install` with only `KATA_STATIC_TARBALL_RELEASE_REPO`, which reintroduces GitHub API rate-limit failures inside the job container - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes From 8093d06a462f6a472a0b9a5b1e3cef5b578337a8 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 07:02:04 +0000 Subject: [PATCH 17/20] ci: align docs flow and enrich release notes --- .github/workflows/release-asterinas-kata-bundle.yml | 10 ++++++++++ .github/workflows/test-asterinas-kata-docs.yml | 8 ++++++-- docs/asterinas-kata-doc-workflow-progress.md | 2 ++ tools/packaging/release/build-asterinas-release.sh | 6 +++++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-asterinas-kata-bundle.yml b/.github/workflows/release-asterinas-kata-bundle.yml index 55e8cccc1..4472bf3c8 100644 --- a/.github/workflows/release-asterinas-kata-bundle.yml +++ b/.github/workflows/release-asterinas-kata-bundle.yml @@ -59,6 +59,8 @@ jobs: build-asterinas-kernel: name: Build Asterinas kernel artifact runs-on: ubuntu-22.04 + outputs: + asterinas_commit: ${{ steps.asterinas_commit.outputs.value }} container: image: ${{ inputs.asterinas_builder_image || 'asterinas/asterinas:0.17.1-20260319' }} steps: @@ -69,6 +71,13 @@ jobs: ref: ${{ env.ASTERINAS_REF }} path: asterinas + - name: Record Asterinas Commit + id: asterinas_commit + shell: bash + run: | + set -euo pipefail + echo "value=$(git -C asterinas rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + - name: Build qemu-direct Kernels shell: bash run: | @@ -153,6 +162,7 @@ jobs: ASTERINAS_TDX_KERNEL: ${{ github.workspace }}/build/asterinas-kernel/aster-kernel-osdk-bin.qemu_elf-tdx ASTERINAS_REPOSITORY: ${{ env.ASTERINAS_REPOSITORY }} ASTERINAS_REF: ${{ env.ASTERINAS_REF }} + ASTERINAS_COMMIT: ${{ needs.build-asterinas-kernel.outputs.asterinas_commit }} ASTERINAS_BUILDER_IMAGE: ${{ env.ASTERINAS_BUILDER_IMAGE }} FORCE_RUNTIME_BUILD: ${{ env.FORCE_RUNTIME_BUILD }} KATA_STATIC_RELEASE_REPOSITORY: ${{ env.KATA_STATIC_RELEASE_REPOSITORY }} diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index bbd832754..d04ef0d8c 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -9,6 +9,10 @@ on: permissions: contents: read +env: + ASTERINAS_REPOSITORY: jjf-dev/asterinas + ASTERINAS_REF: kata-support + jobs: resolve-upstream-asterinas-image: name: Resolve upstream Asterinas image @@ -141,8 +145,8 @@ jobs: - name: Check Out Asterinas Source uses: actions/checkout@v4 with: - repository: asterinas/asterinas - ref: v${{ needs.resolve-upstream-asterinas-image.outputs.asterinas_version }} + repository: ${{ env.ASTERINAS_REPOSITORY }} + ref: ${{ env.ASTERINAS_REF }} path: asterinas-src - name: Prepare Local Asterinas Tree diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index d8cd11d04..74f091895 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -57,6 +57,8 @@ - a follow-up CI attempt showed that directly bind-mounting empty cache directories onto `/nix` and `/root/.cargo` hid the builder image's existing tooling (`nix-build`, `/root/.cargo/env`, etc.). The workflow now uses a safer prewarm approach instead: it prebuilds the kernel in a temporary container, commits that container to a local one-off image, and replays the documented interactive developer flow on top of that prewarmed image - the first prewarmed-image replay then exposed one more subtle issue: `docker commit` preserved the prewarm container's running command, so the derived image no longer dropped into an interactive shell by default. The `pexpect` driver now explicitly appends `bash` when starting both the end-user and kernel-developer outer containers - the latest developer replay reached the local-kernel launch stage and then timed out waiting for the guest agent after switching to `/root/asterinas/target/osdk/aster-kernel-osdk-bin.qemu_elf`. That CI path had been building the local kernel from upstream `asterinas/asterinas` `main`, which can drift from the published `asterinas/asterinas:` image and packaged Kata release. The next adjustment is to make the CI checkout use the matching upstream release tag `v${ASTERINAS_VERSION}` so the local source tree aligns with the image/runtime bundle it is being validated against + - user review corrected that assumption: the docs workflow should not use upstream `asterinas/asterinas` at all for the developer source tree. To stay aligned with the existing release workflow, it should use `jjf-dev/asterinas` on the `kata-support` branch + - user review also requested richer release notes: the Asterinas repo/ref line should include the resolved Asterinas commit id, and the release notes should explicitly name the kata-containers commit id as well - the same release-asset pre-resolution fix also needs to be applied to `.github/workflows/test-asterinas-kata.yml`, because its published-image matrix still calls `tools/kata/kata_env.sh install` with only `KATA_STATIC_TARBALL_RELEASE_REPO`, which reintroduces GitHub API rate-limit failures inside the job container - Next steps: - run a final quick local end-user replay after the latest script cleanup diff --git a/tools/packaging/release/build-asterinas-release.sh b/tools/packaging/release/build-asterinas-release.sh index 62a9a31d7..c426c765e 100644 --- a/tools/packaging/release/build-asterinas-release.sh +++ b/tools/packaging/release/build-asterinas-release.sh @@ -18,6 +18,7 @@ FORCE_RUNTIME_BUILD="${FORCE_RUNTIME_BUILD:-false}" ASTERINAS_REPOSITORY="${ASTERINAS_REPOSITORY:-}" ASTERINAS_REF="${ASTERINAS_REF:-}" +ASTERINAS_COMMIT="${ASTERINAS_COMMIT:-}" ASTERINAS_BUILDER_IMAGE="${ASTERINAS_BUILDER_IMAGE:-}" ASTERINAS_KERNEL="${ASTERINAS_KERNEL:-}" ASTERINAS_TDX_KERNEL="${ASTERINAS_TDX_KERNEL:-}" @@ -380,8 +381,11 @@ write_release_notes() { printf -- '- Kata version: `%s`\n' "${VERSION}" printf -- '- Architecture: `%s`\n' "${ARCHITECTURE}" printf -- '- Base tarball: `%s`\n' "${BASE_TARBALL_URL}" - printf -- '- Kata commit: `%s`\n' "${KATA_COMMIT}" + printf -- '- kata-containers commit: `%s`\n' "${KATA_COMMIT}" printf -- '- Asterinas repo/ref: `%s@%s`\n' "${ASTERINAS_REPOSITORY}" "${ASTERINAS_REF}" + if [ -n "${ASTERINAS_COMMIT}" ]; then + printf -- '- Asterinas commit: `%s`\n' "${ASTERINAS_COMMIT}" + fi printf -- '- Asterinas builder image: `%s`\n' "${ASTERINAS_BUILDER_IMAGE}" printf -- '- Guest kernel: `%s`\n' "${ASTERINAS_KERNEL_PATH}" if [ -n "${ASTERINAS_TDX_KERNEL}" ]; then From ee4f3223b93fd1580223c3557fc3ac64e9136c61 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 07:11:31 +0000 Subject: [PATCH 18/20] ci: fix docs cleanup path --- docs/asterinas-kata-doc-workflow-progress.md | 1 + tools/kata/interactive_doc_test.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index 74f091895..e0d7823b1 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -60,6 +60,7 @@ - user review corrected that assumption: the docs workflow should not use upstream `asterinas/asterinas` at all for the developer source tree. To stay aligned with the existing release workflow, it should use `jjf-dev/asterinas` on the `kata-support` branch - user review also requested richer release notes: the Asterinas repo/ref line should include the resolved Asterinas commit id, and the release notes should explicitly name the kata-containers commit id as well - the same release-asset pre-resolution fix also needs to be applied to `.github/workflows/test-asterinas-kata.yml`, because its published-image matrix still calls `tools/kata/kata_env.sh install` with only `KATA_STATIC_TARBALL_RELEASE_REPO`, which reintroduces GitHub API rate-limit failures inside the job container + - the latest kernel-developer replay with `jjf-dev/asterinas@kata-support` got through both the packaged-kernel run and the local-kernel run; the remaining failure was only the final cleanup command using a relative `./tools/kata/kata_services.sh stop` after the script had changed directory into `/root/asterinas`. That cleanup path is now switched to the absolute repo path under `/root/kata-containers/tools/kata/kata_services.sh` - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes diff --git a/tools/kata/interactive_doc_test.py b/tools/kata/interactive_doc_test.py index 32a2338cb..3ce12ab0d 100644 --- a/tools/kata/interactive_doc_test.py +++ b/tools/kata/interactive_doc_test.py @@ -401,9 +401,9 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: announce("[kernel-developer] running tools/kata/kata_env.sh install") shell.run("./tools/kata/kata_env.sh install", timeout=3600) announce("[kernel-developer] starting Kata background services") - shell.run("./tools/kata/kata_services.sh start", timeout=300) + shell.run("/root/kata-containers/tools/kata/kata_services.sh start", timeout=300) log_active_kata_configs(shell, "kernel-developer") - status_output = shell.run("./tools/kata/kata_services.sh status") + status_output = shell.run("/root/kata-containers/tools/kata/kata_services.sh status") if "Kata services are running." not in status_output: raise ScenarioError(f"Unexpected service status output:\n{status_output}") packaged_guest_proc_version, packaged_alpine_release = run_guest_workload( @@ -435,7 +435,7 @@ def run_kernel_developer_scenario(args: argparse.Namespace) -> pathlib.Path: announce(f"[kernel-developer] local-kernel guest /proc/version: {local_guest_proc_version}") announce(f"[kernel-developer] local-kernel alpine release: {local_alpine_release}") announce("[kernel-developer] stopping Kata background services") - shell.run("./tools/kata/kata_services.sh stop", timeout=300) + shell.run("/root/kata-containers/tools/kata/kata_services.sh stop", timeout=300) return transcript_path finally: shell.close() From 54820be571b5e9c4c65248d10e67375594dff316 Mon Sep 17 00:00:00 2001 From: Jianfeng Jiang Date: Wed, 22 Apr 2026 07:37:12 +0000 Subject: [PATCH 19/20] ci: remove docs prewarm shortcut --- .../workflows/test-asterinas-kata-docs.yml | 22 +------------------ docs/asterinas-kata-doc-workflow-progress.md | 1 + 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-asterinas-kata-docs.yml b/.github/workflows/test-asterinas-kata-docs.yml index d04ef0d8c..c5f451182 100644 --- a/.github/workflows/test-asterinas-kata-docs.yml +++ b/.github/workflows/test-asterinas-kata-docs.yml @@ -183,33 +183,13 @@ jobs: set -euxo pipefail docker pull "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" - - name: Define Prewarmed Builder Image Name - shell: bash - run: | - set -euxo pipefail - echo "KATA_DOC_TEST_PREBUILT_IMAGE=local/asterinas-doc-prebuilt:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_ENV}" - - - name: Prebuild Local Asterinas Kernel - shell: bash - run: | - set -euxo pipefail - docker rm -f asterinas-doc-prewarm >/dev/null 2>&1 || true - docker run -d --name asterinas-doc-prewarm \ - -v "${HOME}/asterinas:/root/asterinas" \ - -w /root/asterinas \ - "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ - bash -lc 'sleep infinity' - docker exec asterinas-doc-prewarm bash -lc 'make kernel BOOT_METHOD=qemu-direct' - docker commit asterinas-doc-prewarm "${KATA_DOC_TEST_PREBUILT_IMAGE}" - docker rm -f asterinas-doc-prewarm - - name: Replay Documented Kernel-Developer Flow shell: bash run: | set -euxo pipefail python3 -u tools/kata/interactive_doc_test.py \ --scenario kernel-developer \ - --asterinas-image "${KATA_DOC_TEST_PREBUILT_IMAGE}" \ + --asterinas-image "asterinas/asterinas:${{ needs.resolve-upstream-asterinas-image.outputs.docker_image_version }}" \ --asterinas-dir "${HOME}/asterinas" \ --log-dir "${KATA_DOC_LOG_DIR}" diff --git a/docs/asterinas-kata-doc-workflow-progress.md b/docs/asterinas-kata-doc-workflow-progress.md index e0d7823b1..278a037a2 100644 --- a/docs/asterinas-kata-doc-workflow-progress.md +++ b/docs/asterinas-kata-doc-workflow-progress.md @@ -61,6 +61,7 @@ - user review also requested richer release notes: the Asterinas repo/ref line should include the resolved Asterinas commit id, and the release notes should explicitly name the kata-containers commit id as well - the same release-asset pre-resolution fix also needs to be applied to `.github/workflows/test-asterinas-kata.yml`, because its published-image matrix still calls `tools/kata/kata_env.sh install` with only `KATA_STATIC_TARBALL_RELEASE_REPO`, which reintroduces GitHub API rate-limit failures inside the job container - the latest kernel-developer replay with `jjf-dev/asterinas@kata-support` got through both the packaged-kernel run and the local-kernel run; the remaining failure was only the final cleanup command using a relative `./tools/kata/kata_services.sh stop` after the script had changed directory into `/root/asterinas`. That cleanup path is now switched to the absolute repo path under `/root/kata-containers/tools/kata/kata_services.sh` + - per latest review, the CI-only prewarm/prebuild shortcut has been removed from `.github/workflows/test-asterinas-kata-docs.yml` so the kernel-developer path now goes back to launching the documented `asterinas/asterinas` image directly during replay - Next steps: - run a final quick local end-user replay after the latest script cleanup - static-check the new workflow and script changes From e534b3e20f9be4020da57959bfe742afbaf8d351 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:56:56 +0000 Subject: [PATCH 20/20] build(deps): bump google.golang.org/protobuf in /src/runtime Bumps google.golang.org/protobuf from 1.36.7 to 1.36.11. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-version: 1.36.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/runtime/go.mod | 2 +- src/runtime/go.sum | 4 +- .../protobuf/internal/encoding/tag/tag.go | 11 +- .../protobuf/internal/encoding/text/decode.go | 115 ++++++++++++------ .../protobuf/internal/filedesc/desc.go | 53 +++++--- .../protobuf/internal/filedesc/desc_init.go | 14 +++ .../protobuf/internal/filedesc/desc_lazy.go | 42 +++---- .../protobuf/internal/filedesc/editions.go | 12 +- .../protobuf/internal/genid/api_gen.go | 6 + .../protobuf/internal/genid/descriptor_gen.go | 1 + .../protobuf/internal/impl/codec_map.go | 6 + .../protobuf/internal/impl/decode.go | 3 +- .../protobuf/internal/impl/validate.go | 26 ++++ .../protobuf/internal/version/version.go | 2 +- .../protobuf/proto/decode.go | 10 +- .../types/descriptorpb/descriptor.pb.go | 23 ++-- .../types/known/timestamppb/timestamp.pb.go | 13 +- src/runtime/vendor/modules.txt | 4 +- 18 files changed, 241 insertions(+), 106 deletions(-) diff --git a/src/runtime/go.mod b/src/runtime/go.mod index 5a5a011bb..fc7e487be 100644 --- a/src/runtime/go.mod +++ b/src/runtime/go.mod @@ -59,7 +59,7 @@ require ( golang.org/x/oauth2 v0.30.0 golang.org/x/sys v0.40.0 google.golang.org/grpc v1.72.0 - google.golang.org/protobuf v1.36.7 + google.golang.org/protobuf v1.36.11 k8s.io/apimachinery v0.33.0 k8s.io/cri-api v0.33.0 k8s.io/kubelet v0.33.0 diff --git a/src/runtime/go.sum b/src/runtime/go.sum index 5ed53e0bc..4424cb768 100644 --- a/src/runtime/go.sum +++ b/src/runtime/go.sum @@ -444,8 +444,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/tag/tag.go b/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/tag/tag.go index 669133d04..c96e44834 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/tag/tag.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/tag/tag.go @@ -32,7 +32,7 @@ var byteType = reflect.TypeOf(byte(0)) func Unmarshal(tag string, goType reflect.Type, evs protoreflect.EnumValueDescriptors) protoreflect.FieldDescriptor { f := new(filedesc.Field) f.L0.ParentFile = filedesc.SurrogateProto2 - f.L1.EditionFeatures = f.L0.ParentFile.L1.EditionFeatures + packed := false for len(tag) > 0 { i := strings.IndexByte(tag, ',') if i < 0 { @@ -108,7 +108,7 @@ func Unmarshal(tag string, goType reflect.Type, evs protoreflect.EnumValueDescri f.L1.StringName.InitJSON(jsonName) } case s == "packed": - f.L1.EditionFeatures.IsPacked = true + packed = true case strings.HasPrefix(s, "def="): // The default tag is special in that everything afterwards is the // default regardless of the presence of commas. @@ -121,6 +121,13 @@ func Unmarshal(tag string, goType reflect.Type, evs protoreflect.EnumValueDescri tag = strings.TrimPrefix(tag[i:], ",") } + // Update EditionFeatures after the loop and after we know whether this is + // a proto2 or proto3 field. + f.L1.EditionFeatures = f.L0.ParentFile.L1.EditionFeatures + if packed { + f.L1.EditionFeatures.IsPacked = true + } + // The generator uses the group message name instead of the field name. // We obtain the real field name by lowercasing the group name. if f.L1.Kind == protoreflect.GroupKind { diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go b/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go index 099b2bf45..9aa7a9bb7 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/encoding/text/decode.go @@ -424,27 +424,34 @@ func (d *Decoder) parseFieldName() (tok Token, err error) { return Token{}, d.newSyntaxError("invalid field name: %s", errId(d.in)) } -// parseTypeName parses Any type URL or extension field name. The name is -// enclosed in [ and ] characters. The C++ parser does not handle many legal URL -// strings. This implementation is more liberal and allows for the pattern -// ^[-_a-zA-Z0-9]+([./][-_a-zA-Z0-9]+)*`). Whitespaces and comments are allowed -// in between [ ], '.', '/' and the sub names. +// parseTypeName parses an Any type URL or an extension field name. The name is +// enclosed in [ and ] characters. We allow almost arbitrary type URL prefixes, +// closely following the text-format spec [1,2]. We implement "ExtensionName | +// AnyName" as follows (with some exceptions for backwards compatibility): +// +// char = [-_a-zA-Z0-9] +// url_char = char | [.~!$&'()*+,;=] | "%", hex, hex +// +// Ident = char, { char } +// TypeName = Ident, { ".", Ident } ; +// UrlPrefix = url_char, { url_char | "/" } ; +// ExtensionName = "[", TypeName, "]" ; +// AnyName = "[", UrlPrefix, "/", TypeName, "]" ; +// +// Additionally, we allow arbitrary whitespace and comments between [ and ]. +// +// [1] https://protobuf.dev/reference/protobuf/textformat-spec/#characters +// [2] https://protobuf.dev/reference/protobuf/textformat-spec/#field-names func (d *Decoder) parseTypeName() (Token, error) { - startPos := len(d.orig) - len(d.in) // Use alias s to advance first in order to use d.in for error handling. - // Caller already checks for [ as first character. + // Caller already checks for [ as first character (d.in[0] == '['). s := consume(d.in[1:], 0) if len(s) == 0 { return Token{}, ErrUnexpectedEOF } + // Collect everything between [ and ] in name. var name []byte - for len(s) > 0 && isTypeNameChar(s[0]) { - name = append(name, s[0]) - s = s[1:] - } - s = consume(s, 0) - var closed bool for len(s) > 0 && !closed { switch { @@ -452,23 +459,20 @@ func (d *Decoder) parseTypeName() (Token, error) { s = s[1:] closed = true - case s[0] == '/', s[0] == '.': - if len(name) > 0 && (name[len(name)-1] == '/' || name[len(name)-1] == '.') { - return Token{}, d.newSyntaxError("invalid type URL/extension field name: %s", - d.orig[startPos:len(d.orig)-len(s)+1]) - } + case s[0] == '/' || isTypeNameChar(s[0]) || isUrlExtraChar(s[0]): name = append(name, s[0]) - s = s[1:] - s = consume(s, 0) - for len(s) > 0 && isTypeNameChar(s[0]) { - name = append(name, s[0]) - s = s[1:] + s = consume(s[1:], 0) + + // URL percent-encoded chars + case s[0] == '%': + if len(s) < 3 || !isHexChar(s[1]) || !isHexChar(s[2]) { + return Token{}, d.parseTypeNameError(s, 3) } - s = consume(s, 0) + name = append(name, s[0], s[1], s[2]) + s = consume(s[3:], 0) default: - return Token{}, d.newSyntaxError( - "invalid type URL/extension field name: %s", d.orig[startPos:len(d.orig)-len(s)+1]) + return Token{}, d.parseTypeNameError(s, 1) } } @@ -476,15 +480,38 @@ func (d *Decoder) parseTypeName() (Token, error) { return Token{}, ErrUnexpectedEOF } - // First character cannot be '.'. Last character cannot be '.' or '/'. - size := len(name) - if size == 0 || name[0] == '.' || name[size-1] == '.' || name[size-1] == '/' { - return Token{}, d.newSyntaxError("invalid type URL/extension field name: %s", - d.orig[startPos:len(d.orig)-len(s)]) + // Split collected name on last '/' into urlPrefix and typeName (if '/' is + // present). + typeName := name + if i := bytes.LastIndexByte(name, '/'); i != -1 { + urlPrefix := name[:i] + typeName = name[i+1:] + + // urlPrefix may be empty (for backwards compatibility). + // If non-empty, it must not start with '/'. + if len(urlPrefix) > 0 && urlPrefix[0] == '/' { + return Token{}, d.parseTypeNameError(s, 0) + } } + // typeName must not be empty (note: "" splits to [""]) and all identifier + // parts must not be empty. + for _, ident := range bytes.Split(typeName, []byte{'.'}) { + if len(ident) == 0 { + return Token{}, d.parseTypeNameError(s, 0) + } + } + + // typeName must not contain any percent-encoded or special URL chars. + for _, b := range typeName { + if b == '%' || (b != '.' && isUrlExtraChar(b)) { + return Token{}, d.parseTypeNameError(s, 0) + } + } + + startPos := len(d.orig) - len(d.in) + endPos := len(d.orig) - len(s) d.in = s - endPos := len(d.orig) - len(d.in) d.consume(0) return Token{ @@ -496,16 +523,32 @@ func (d *Decoder) parseTypeName() (Token, error) { }, nil } +func (d *Decoder) parseTypeNameError(s []byte, numUnconsumedChars int) error { + return d.newSyntaxError( + "invalid type URL/extension field name: %s", + d.in[:len(d.in)-len(s)+min(numUnconsumedChars, len(s))], + ) +} + +func isHexChar(b byte) bool { + return ('0' <= b && b <= '9') || + ('a' <= b && b <= 'f') || + ('A' <= b && b <= 'F') +} + func isTypeNameChar(b byte) bool { - return (b == '-' || b == '_' || + return b == '-' || b == '_' || ('0' <= b && b <= '9') || ('a' <= b && b <= 'z') || - ('A' <= b && b <= 'Z')) + ('A' <= b && b <= 'Z') } -func isWhiteSpace(b byte) bool { +// isUrlExtraChar complements isTypeNameChar with extra characters that we allow +// in URLs but not in type names. Note that '/' is not included so that it can +// be treated specially. +func isUrlExtraChar(b byte) bool { switch b { - case ' ', '\n', '\r', '\t': + case '.', '~', '!', '$', '&', '(', ')', '*', '+', ',', ';', '=': return true default: return false diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc.go b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc.go index 688aabe43..c775e5832 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc.go @@ -32,6 +32,7 @@ const ( EditionProto3 Edition = 999 Edition2023 Edition = 1000 Edition2024 Edition = 1001 + EditionUnstable Edition = 9999 EditionUnsupported Edition = 100000 ) @@ -72,9 +73,10 @@ type ( EditionFeatures EditionFeatures } FileL2 struct { - Options func() protoreflect.ProtoMessage - Imports FileImports - Locations SourceLocations + Options func() protoreflect.ProtoMessage + Imports FileImports + OptionImports func() protoreflect.FileImports + Locations SourceLocations } // EditionFeatures is a frequently-instantiated struct, so please take care @@ -126,12 +128,9 @@ func (fd *File) ParentFile() protoreflect.FileDescriptor { return fd } func (fd *File) Parent() protoreflect.Descriptor { return nil } func (fd *File) Index() int { return 0 } func (fd *File) Syntax() protoreflect.Syntax { return fd.L1.Syntax } - -// Not exported and just used to reconstruct the original FileDescriptor proto -func (fd *File) Edition() int32 { return int32(fd.L1.Edition) } -func (fd *File) Name() protoreflect.Name { return fd.L1.Package.Name() } -func (fd *File) FullName() protoreflect.FullName { return fd.L1.Package } -func (fd *File) IsPlaceholder() bool { return false } +func (fd *File) Name() protoreflect.Name { return fd.L1.Package.Name() } +func (fd *File) FullName() protoreflect.FullName { return fd.L1.Package } +func (fd *File) IsPlaceholder() bool { return false } func (fd *File) Options() protoreflect.ProtoMessage { if f := fd.lazyInit().Options; f != nil { return f() @@ -150,6 +149,16 @@ func (fd *File) Format(s fmt.State, r rune) { descfmt.FormatD func (fd *File) ProtoType(protoreflect.FileDescriptor) {} func (fd *File) ProtoInternal(pragma.DoNotImplement) {} +// The next two are not part of the FileDescriptor interface. They are just used to reconstruct +// the original FileDescriptor proto. +func (fd *File) Edition() int32 { return int32(fd.L1.Edition) } +func (fd *File) OptionImports() protoreflect.FileImports { + if f := fd.lazyInit().OptionImports; f != nil { + return f() + } + return emptyFiles +} + func (fd *File) lazyInit() *FileL2 { if atomic.LoadUint32(&fd.once) == 0 { fd.lazyInitOnce() @@ -182,9 +191,9 @@ type ( L2 *EnumL2 // protected by fileDesc.once } EnumL1 struct { - eagerValues bool // controls whether EnumL2.Values is already populated - EditionFeatures EditionFeatures + Visibility int32 + eagerValues bool // controls whether EnumL2.Values is already populated } EnumL2 struct { Options func() protoreflect.ProtoMessage @@ -219,6 +228,11 @@ func (ed *Enum) ReservedNames() protoreflect.Names { return &ed.lazyInit() func (ed *Enum) ReservedRanges() protoreflect.EnumRanges { return &ed.lazyInit().ReservedRanges } func (ed *Enum) Format(s fmt.State, r rune) { descfmt.FormatDesc(s, r, ed) } func (ed *Enum) ProtoType(protoreflect.EnumDescriptor) {} + +// This is not part of the EnumDescriptor interface. It is just used to reconstruct +// the original FileDescriptor proto. +func (ed *Enum) Visibility() int32 { return ed.L1.Visibility } + func (ed *Enum) lazyInit() *EnumL2 { ed.L0.ParentFile.lazyInit() // implicitly initializes L2 return ed.L2 @@ -244,13 +258,13 @@ type ( L2 *MessageL2 // protected by fileDesc.once } MessageL1 struct { - Enums Enums - Messages Messages - Extensions Extensions - IsMapEntry bool // promoted from google.protobuf.MessageOptions - IsMessageSet bool // promoted from google.protobuf.MessageOptions - + Enums Enums + Messages Messages + Extensions Extensions EditionFeatures EditionFeatures + Visibility int32 + IsMapEntry bool // promoted from google.protobuf.MessageOptions + IsMessageSet bool // promoted from google.protobuf.MessageOptions } MessageL2 struct { Options func() protoreflect.ProtoMessage @@ -319,6 +333,11 @@ func (md *Message) Messages() protoreflect.MessageDescriptors { return &md.L func (md *Message) Extensions() protoreflect.ExtensionDescriptors { return &md.L1.Extensions } func (md *Message) ProtoType(protoreflect.MessageDescriptor) {} func (md *Message) Format(s fmt.State, r rune) { descfmt.FormatDesc(s, r, md) } + +// This is not part of the MessageDescriptor interface. It is just used to reconstruct +// the original FileDescriptor proto. +func (md *Message) Visibility() int32 { return md.L1.Visibility } + func (md *Message) lazyInit() *MessageL2 { md.L0.ParentFile.lazyInit() // implicitly initializes L2 return md.L2 diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_init.go b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_init.go index d2f549497..e91860f5a 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_init.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_init.go @@ -284,6 +284,13 @@ func (ed *Enum) unmarshalSeed(b []byte, sb *strs.Builder, pf *File, pd protorefl case genid.EnumDescriptorProto_Value_field_number: numValues++ } + case protowire.VarintType: + v, m := protowire.ConsumeVarint(b) + b = b[m:] + switch num { + case genid.EnumDescriptorProto_Visibility_field_number: + ed.L1.Visibility = int32(v) + } default: m := protowire.ConsumeFieldValue(num, typ, b) b = b[m:] @@ -365,6 +372,13 @@ func (md *Message) unmarshalSeed(b []byte, sb *strs.Builder, pf *File, pd protor md.unmarshalSeedOptions(v) } prevField = num + case protowire.VarintType: + v, m := protowire.ConsumeVarint(b) + b = b[m:] + switch num { + case genid.DescriptorProto_Visibility_field_number: + md.L1.Visibility = int32(v) + } default: m := protowire.ConsumeFieldValue(num, typ, b) b = b[m:] diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_lazy.go b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_lazy.go index d4c94458b..78f02b1b4 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_lazy.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/desc_lazy.go @@ -134,6 +134,7 @@ func (fd *File) unmarshalFull(b []byte) { var enumIdx, messageIdx, extensionIdx, serviceIdx int var rawOptions []byte + var optionImports []string fd.L2 = new(FileL2) for len(b) > 0 { num, typ, n := protowire.ConsumeTag(b) @@ -157,6 +158,8 @@ func (fd *File) unmarshalFull(b []byte) { imp = PlaceholderFile(path) } fd.L2.Imports = append(fd.L2.Imports, protoreflect.FileImport{FileDescriptor: imp}) + case genid.FileDescriptorProto_OptionDependency_field_number: + optionImports = append(optionImports, sb.MakeString(v)) case genid.FileDescriptorProto_EnumType_field_number: fd.L1.Enums.List[enumIdx].unmarshalFull(v, sb) enumIdx++ @@ -178,6 +181,23 @@ func (fd *File) unmarshalFull(b []byte) { } } fd.L2.Options = fd.builder.optionsUnmarshaler(&descopts.File, rawOptions) + if len(optionImports) > 0 { + var imps FileImports + var once sync.Once + fd.L2.OptionImports = func() protoreflect.FileImports { + once.Do(func() { + imps = make(FileImports, len(optionImports)) + for i, path := range optionImports { + imp, _ := fd.builder.FileRegistry.FindFileByPath(path) + if imp == nil { + imp = PlaceholderFile(path) + } + imps[i] = protoreflect.FileImport{FileDescriptor: imp} + } + }) + return &imps + } + } } func (ed *Enum) unmarshalFull(b []byte, sb *strs.Builder) { @@ -310,7 +330,6 @@ func (md *Message) unmarshalFull(b []byte, sb *strs.Builder) { md.L1.Extensions.List[extensionIdx].unmarshalFull(v, sb) extensionIdx++ case genid.DescriptorProto_Options_field_number: - md.unmarshalOptions(v) rawOptions = appendOptions(rawOptions, v) } default: @@ -336,27 +355,6 @@ func (md *Message) unmarshalFull(b []byte, sb *strs.Builder) { md.L2.Options = md.L0.ParentFile.builder.optionsUnmarshaler(&descopts.Message, rawOptions) } -func (md *Message) unmarshalOptions(b []byte) { - for len(b) > 0 { - num, typ, n := protowire.ConsumeTag(b) - b = b[n:] - switch typ { - case protowire.VarintType: - v, m := protowire.ConsumeVarint(b) - b = b[m:] - switch num { - case genid.MessageOptions_MapEntry_field_number: - md.L1.IsMapEntry = protowire.DecodeBool(v) - case genid.MessageOptions_MessageSetWireFormat_field_number: - md.L1.IsMessageSet = protowire.DecodeBool(v) - } - default: - m := protowire.ConsumeFieldValue(num, typ, b) - b = b[m:] - } - } -} - func unmarshalMessageReservedRange(b []byte) (r [2]protoreflect.FieldNumber) { for len(b) > 0 { num, typ, n := protowire.ConsumeTag(b) diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/editions.go b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/editions.go index a0aad2777..66ba90680 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/editions.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/filedesc/editions.go @@ -13,8 +13,10 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) -var defaultsCache = make(map[Edition]EditionFeatures) -var defaultsKeys = []Edition{} +var ( + defaultsCache = make(map[Edition]EditionFeatures) + defaultsKeys = []Edition{} +) func init() { unmarshalEditionDefaults(editiondefaults.Defaults) @@ -41,7 +43,7 @@ func unmarshalGoFeature(b []byte, parent EditionFeatures) EditionFeatures { b = b[m:] parent.StripEnumPrefix = int(v) default: - panic(fmt.Sprintf("unkown field number %d while unmarshalling GoFeatures", num)) + panic(fmt.Sprintf("unknown field number %d while unmarshalling GoFeatures", num)) } } return parent @@ -76,7 +78,7 @@ func unmarshalFeatureSet(b []byte, parent EditionFeatures) EditionFeatures { // DefaultSymbolVisibility is enforced in protoc, runtimes should not // inspect this value. default: - panic(fmt.Sprintf("unkown field number %d while unmarshalling FeatureSet", num)) + panic(fmt.Sprintf("unknown field number %d while unmarshalling FeatureSet", num)) } case protowire.BytesType: v, m := protowire.ConsumeBytes(b) @@ -150,7 +152,7 @@ func unmarshalEditionDefaults(b []byte) { _, m := protowire.ConsumeVarint(b) b = b[m:] default: - panic(fmt.Sprintf("unkown field number %d while unmarshalling EditionDefault", num)) + panic(fmt.Sprintf("unknown field number %d while unmarshalling EditionDefault", num)) } } } diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/genid/api_gen.go b/src/runtime/vendor/google.golang.org/protobuf/internal/genid/api_gen.go index df8f91850..3ceb6fa7f 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/genid/api_gen.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/genid/api_gen.go @@ -27,6 +27,7 @@ const ( Api_SourceContext_field_name protoreflect.Name = "source_context" Api_Mixins_field_name protoreflect.Name = "mixins" Api_Syntax_field_name protoreflect.Name = "syntax" + Api_Edition_field_name protoreflect.Name = "edition" Api_Name_field_fullname protoreflect.FullName = "google.protobuf.Api.name" Api_Methods_field_fullname protoreflect.FullName = "google.protobuf.Api.methods" @@ -35,6 +36,7 @@ const ( Api_SourceContext_field_fullname protoreflect.FullName = "google.protobuf.Api.source_context" Api_Mixins_field_fullname protoreflect.FullName = "google.protobuf.Api.mixins" Api_Syntax_field_fullname protoreflect.FullName = "google.protobuf.Api.syntax" + Api_Edition_field_fullname protoreflect.FullName = "google.protobuf.Api.edition" ) // Field numbers for google.protobuf.Api. @@ -46,6 +48,7 @@ const ( Api_SourceContext_field_number protoreflect.FieldNumber = 5 Api_Mixins_field_number protoreflect.FieldNumber = 6 Api_Syntax_field_number protoreflect.FieldNumber = 7 + Api_Edition_field_number protoreflect.FieldNumber = 8 ) // Names for google.protobuf.Method. @@ -63,6 +66,7 @@ const ( Method_ResponseStreaming_field_name protoreflect.Name = "response_streaming" Method_Options_field_name protoreflect.Name = "options" Method_Syntax_field_name protoreflect.Name = "syntax" + Method_Edition_field_name protoreflect.Name = "edition" Method_Name_field_fullname protoreflect.FullName = "google.protobuf.Method.name" Method_RequestTypeUrl_field_fullname protoreflect.FullName = "google.protobuf.Method.request_type_url" @@ -71,6 +75,7 @@ const ( Method_ResponseStreaming_field_fullname protoreflect.FullName = "google.protobuf.Method.response_streaming" Method_Options_field_fullname protoreflect.FullName = "google.protobuf.Method.options" Method_Syntax_field_fullname protoreflect.FullName = "google.protobuf.Method.syntax" + Method_Edition_field_fullname protoreflect.FullName = "google.protobuf.Method.edition" ) // Field numbers for google.protobuf.Method. @@ -82,6 +87,7 @@ const ( Method_ResponseStreaming_field_number protoreflect.FieldNumber = 5 Method_Options_field_number protoreflect.FieldNumber = 6 Method_Syntax_field_number protoreflect.FieldNumber = 7 + Method_Edition_field_number protoreflect.FieldNumber = 8 ) // Names for google.protobuf.Mixin. diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/genid/descriptor_gen.go b/src/runtime/vendor/google.golang.org/protobuf/internal/genid/descriptor_gen.go index 950a6a325..65aaf4d21 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/genid/descriptor_gen.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/genid/descriptor_gen.go @@ -26,6 +26,7 @@ const ( Edition_EDITION_PROTO3_enum_value = 999 Edition_EDITION_2023_enum_value = 1000 Edition_EDITION_2024_enum_value = 1001 + Edition_EDITION_UNSTABLE_enum_value = 9999 Edition_EDITION_1_TEST_ONLY_enum_value = 1 Edition_EDITION_2_TEST_ONLY_enum_value = 2 Edition_EDITION_99997_TEST_ONLY_enum_value = 99997 diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/impl/codec_map.go b/src/runtime/vendor/google.golang.org/protobuf/internal/impl/codec_map.go index 229c69801..4a3bf393e 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/impl/codec_map.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/impl/codec_map.go @@ -113,6 +113,9 @@ func sizeMap(mapv reflect.Value, mapi *mapInfo, f *coderFieldInfo, opts marshalO } func consumeMap(b []byte, mapv reflect.Value, wtyp protowire.Type, mapi *mapInfo, f *coderFieldInfo, opts unmarshalOptions) (out unmarshalOutput, err error) { + if opts.depth--; opts.depth < 0 { + return out, errRecursionDepth + } if wtyp != protowire.BytesType { return out, errUnknown } @@ -170,6 +173,9 @@ func consumeMap(b []byte, mapv reflect.Value, wtyp protowire.Type, mapi *mapInfo } func consumeMapOfMessage(b []byte, mapv reflect.Value, wtyp protowire.Type, mapi *mapInfo, f *coderFieldInfo, opts unmarshalOptions) (out unmarshalOutput, err error) { + if opts.depth--; opts.depth < 0 { + return out, errRecursionDepth + } if wtyp != protowire.BytesType { return out, errUnknown } diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/impl/decode.go b/src/runtime/vendor/google.golang.org/protobuf/internal/impl/decode.go index e0dd21fa5..1228b5c8c 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/impl/decode.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/impl/decode.go @@ -102,8 +102,7 @@ var errUnknown = errors.New("unknown") func (mi *MessageInfo) unmarshalPointer(b []byte, p pointer, groupTag protowire.Number, opts unmarshalOptions) (out unmarshalOutput, err error) { mi.init() - opts.depth-- - if opts.depth < 0 { + if opts.depth--; opts.depth < 0 { return out, errRecursionDepth } if flags.ProtoLegacy && mi.isMessageSet { diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/impl/validate.go b/src/runtime/vendor/google.golang.org/protobuf/internal/impl/validate.go index 7b2995dde..99a1eb95f 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/impl/validate.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/impl/validate.go @@ -68,9 +68,13 @@ func Validate(mt protoreflect.MessageType, in protoiface.UnmarshalInput) (out pr if in.Resolver == nil { in.Resolver = protoregistry.GlobalTypes } + if in.Depth == 0 { + in.Depth = protowire.DefaultRecursionLimit + } o, st := mi.validate(in.Buf, 0, unmarshalOptions{ flags: in.Flags, resolver: in.Resolver, + depth: in.Depth, }) if o.initialized { out.Flags |= protoiface.UnmarshalInitialized @@ -257,6 +261,9 @@ func (mi *MessageInfo) validate(b []byte, groupTag protowire.Number, opts unmars states[0].typ = validationTypeGroup states[0].endGroup = groupTag } + if opts.depth--; opts.depth < 0 { + return out, ValidationInvalid + } initialized := true start := len(b) State: @@ -451,6 +458,13 @@ State: mi: vi.mi, tail: b, }) + if vi.typ == validationTypeMessage || + vi.typ == validationTypeGroup || + vi.typ == validationTypeMap { + if opts.depth--; opts.depth < 0 { + return out, ValidationInvalid + } + } b = v continue State case validationTypeRepeatedVarint: @@ -499,6 +513,9 @@ State: mi: vi.mi, endGroup: num, }) + if opts.depth--; opts.depth < 0 { + return out, ValidationInvalid + } continue State case flags.ProtoLegacy && vi.typ == validationTypeMessageSetItem: typeid, v, n, err := messageset.ConsumeFieldValue(b, false) @@ -521,6 +538,13 @@ State: mi: xvi.mi, tail: b[n:], }) + if xvi.typ == validationTypeMessage || + xvi.typ == validationTypeGroup || + xvi.typ == validationTypeMap { + if opts.depth--; opts.depth < 0 { + return out, ValidationInvalid + } + } b = v continue State } @@ -547,12 +571,14 @@ State: switch st.typ { case validationTypeMessage, validationTypeGroup: numRequiredFields = int(st.mi.numRequiredFields) + opts.depth++ case validationTypeMap: // If this is a map field with a message value that contains // required fields, require that the value be present. if st.mi != nil && st.mi.numRequiredFields > 0 { numRequiredFields = 1 } + opts.depth++ } // If there are more than 64 required fields, this check will // always fail and we will report that the message is potentially diff --git a/src/runtime/vendor/google.golang.org/protobuf/internal/version/version.go b/src/runtime/vendor/google.golang.org/protobuf/internal/version/version.go index a53364c59..763fd8284 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/internal/version/version.go +++ b/src/runtime/vendor/google.golang.org/protobuf/internal/version/version.go @@ -52,7 +52,7 @@ import ( const ( Major = 1 Minor = 36 - Patch = 7 + Patch = 11 PreRelease = "" ) diff --git a/src/runtime/vendor/google.golang.org/protobuf/proto/decode.go b/src/runtime/vendor/google.golang.org/protobuf/proto/decode.go index 4cbf1aeaf..889d8511d 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/proto/decode.go +++ b/src/runtime/vendor/google.golang.org/protobuf/proto/decode.go @@ -121,9 +121,8 @@ func (o UnmarshalOptions) unmarshal(b []byte, m protoreflect.Message) (out proto out, err = methods.Unmarshal(in) } else { - o.RecursionLimit-- - if o.RecursionLimit < 0 { - return out, errors.New("exceeded max recursion depth") + if o.RecursionLimit--; o.RecursionLimit < 0 { + return out, errRecursionDepth } err = o.unmarshalMessageSlow(b, m) } @@ -220,6 +219,9 @@ func (o UnmarshalOptions) unmarshalSingular(b []byte, wtyp protowire.Type, m pro } func (o UnmarshalOptions) unmarshalMap(b []byte, wtyp protowire.Type, mapv protoreflect.Map, fd protoreflect.FieldDescriptor) (n int, err error) { + if o.RecursionLimit--; o.RecursionLimit < 0 { + return 0, errRecursionDepth + } if wtyp != protowire.BytesType { return 0, errUnknown } @@ -305,3 +307,5 @@ func (o UnmarshalOptions) unmarshalMap(b []byte, wtyp protowire.Type, mapv proto var errUnknown = errors.New("BUG: internal error (unknown)") var errDecode = errors.New("cannot parse invalid wire-format data") + +var errRecursionDepth = errors.New("exceeded maximum recursion depth") diff --git a/src/runtime/vendor/google.golang.org/protobuf/types/descriptorpb/descriptor.pb.go b/src/runtime/vendor/google.golang.org/protobuf/types/descriptorpb/descriptor.pb.go index 6843b0bea..0b23faa95 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/types/descriptorpb/descriptor.pb.go +++ b/src/runtime/vendor/google.golang.org/protobuf/types/descriptorpb/descriptor.pb.go @@ -69,6 +69,8 @@ const ( // comparison. Edition_EDITION_2023 Edition = 1000 Edition_EDITION_2024 Edition = 1001 + // A placeholder edition for developing and testing unscheduled features. + Edition_EDITION_UNSTABLE Edition = 9999 // Placeholder editions for testing feature resolution. These should not be // used or relied on outside of tests. Edition_EDITION_1_TEST_ONLY Edition = 1 @@ -91,6 +93,7 @@ var ( 999: "EDITION_PROTO3", 1000: "EDITION_2023", 1001: "EDITION_2024", + 9999: "EDITION_UNSTABLE", 1: "EDITION_1_TEST_ONLY", 2: "EDITION_2_TEST_ONLY", 99997: "EDITION_99997_TEST_ONLY", @@ -105,6 +108,7 @@ var ( "EDITION_PROTO3": 999, "EDITION_2023": 1000, "EDITION_2024": 1001, + "EDITION_UNSTABLE": 9999, "EDITION_1_TEST_ONLY": 1, "EDITION_2_TEST_ONLY": 2, "EDITION_99997_TEST_ONLY": 99997, @@ -2873,7 +2877,10 @@ type FieldOptions struct { // for accessors, or it will be completely ignored; in the very least, this // is a formalization for deprecating fields. Deprecated *bool `protobuf:"varint,3,opt,name=deprecated,def=0" json:"deprecated,omitempty"` + // DEPRECATED. DO NOT USE! // For Google-internal migration only. Do not use. + // + // Deprecated: Marked as deprecated in google/protobuf/descriptor.proto. Weak *bool `protobuf:"varint,10,opt,name=weak,def=0" json:"weak,omitempty"` // Indicate that the field value should not be printed out when using debug // formats, e.g. when the field contains sensitive credentials. @@ -2977,6 +2984,7 @@ func (x *FieldOptions) GetDeprecated() bool { return Default_FieldOptions_Deprecated } +// Deprecated: Marked as deprecated in google/protobuf/descriptor.proto. func (x *FieldOptions) GetWeak() bool { if x != nil && x.Weak != nil { return *x.Weak @@ -4789,11 +4797,11 @@ const file_google_protobuf_descriptor_proto_rawDesc = "" + "\x18EnumValueDescriptorProto\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + "\x06number\x18\x02 \x01(\x05R\x06number\x12;\n" + - "\aoptions\x18\x03 \x01(\v2!.google.protobuf.EnumValueOptionsR\aoptions\"\xa7\x01\n" + + "\aoptions\x18\x03 \x01(\v2!.google.protobuf.EnumValueOptionsR\aoptions\"\xb5\x01\n" + "\x16ServiceDescriptorProto\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12>\n" + "\x06method\x18\x02 \x03(\v2&.google.protobuf.MethodDescriptorProtoR\x06method\x129\n" + - "\aoptions\x18\x03 \x01(\v2\x1f.google.protobuf.ServiceOptionsR\aoptions\"\x89\x02\n" + + "\aoptions\x18\x03 \x01(\v2\x1f.google.protobuf.ServiceOptionsR\aoptionsJ\x04\b\x04\x10\x05R\x06stream\"\x89\x02\n" + "\x15MethodDescriptorProto\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + "\n" + @@ -4843,7 +4851,7 @@ const file_google_protobuf_descriptor_proto_rawDesc = "" + "&deprecated_legacy_json_field_conflicts\x18\v \x01(\bB\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x127\n" + "\bfeatures\x18\f \x01(\v2\x1b.google.protobuf.FeatureSetR\bfeatures\x12X\n" + "\x14uninterpreted_option\x18\xe7\a \x03(\v2$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\b\xe8\a\x10\x80\x80\x80\x80\x02J\x04\b\x04\x10\x05J\x04\b\x05\x10\x06J\x04\b\x06\x10\aJ\x04\b\b\x10\tJ\x04\b\t\x10\n" + - "\"\x9d\r\n" + + "\"\xa1\r\n" + "\fFieldOptions\x12A\n" + "\x05ctype\x18\x01 \x01(\x0e2#.google.protobuf.FieldOptions.CType:\x06STRINGR\x05ctype\x12\x16\n" + "\x06packed\x18\x02 \x01(\bR\x06packed\x12G\n" + @@ -4852,9 +4860,9 @@ const file_google_protobuf_descriptor_proto_rawDesc = "" + "\x0funverified_lazy\x18\x0f \x01(\b:\x05falseR\x0eunverifiedLazy\x12%\n" + "\n" + "deprecated\x18\x03 \x01(\b:\x05falseR\n" + - "deprecated\x12\x19\n" + + "deprecated\x12\x1d\n" + "\x04weak\x18\n" + - " \x01(\b:\x05falseR\x04weak\x12(\n" + + " \x01(\b:\x05falseB\x02\x18\x01R\x04weak\x12(\n" + "\fdebug_redact\x18\x10 \x01(\b:\x05falseR\vdebugRedact\x12K\n" + "\tretention\x18\x11 \x01(\x0e2-.google.protobuf.FieldOptions.OptionRetentionR\tretention\x12H\n" + "\atargets\x18\x13 \x03(\x0e2..google.protobuf.FieldOptions.OptionTargetTypeR\atargets\x12W\n" + @@ -5029,14 +5037,15 @@ const file_google_protobuf_descriptor_proto_rawDesc = "" + "\bSemantic\x12\b\n" + "\x04NONE\x10\x00\x12\a\n" + "\x03SET\x10\x01\x12\t\n" + - "\x05ALIAS\x10\x02*\xa7\x02\n" + + "\x05ALIAS\x10\x02*\xbe\x02\n" + "\aEdition\x12\x13\n" + "\x0fEDITION_UNKNOWN\x10\x00\x12\x13\n" + "\x0eEDITION_LEGACY\x10\x84\a\x12\x13\n" + "\x0eEDITION_PROTO2\x10\xe6\a\x12\x13\n" + "\x0eEDITION_PROTO3\x10\xe7\a\x12\x11\n" + "\fEDITION_2023\x10\xe8\a\x12\x11\n" + - "\fEDITION_2024\x10\xe9\a\x12\x17\n" + + "\fEDITION_2024\x10\xe9\a\x12\x15\n" + + "\x10EDITION_UNSTABLE\x10\x8fN\x12\x17\n" + "\x13EDITION_1_TEST_ONLY\x10\x01\x12\x17\n" + "\x13EDITION_2_TEST_ONLY\x10\x02\x12\x1d\n" + "\x17EDITION_99997_TEST_ONLY\x10\x9d\x8d\x06\x12\x1d\n" + diff --git a/src/runtime/vendor/google.golang.org/protobuf/types/known/timestamppb/timestamp.pb.go b/src/runtime/vendor/google.golang.org/protobuf/types/known/timestamppb/timestamp.pb.go index 06d584c14..484c21fd5 100644 --- a/src/runtime/vendor/google.golang.org/protobuf/types/known/timestamppb/timestamp.pb.go +++ b/src/runtime/vendor/google.golang.org/protobuf/types/known/timestamppb/timestamp.pb.go @@ -172,13 +172,14 @@ import ( // ) to obtain a formatter capable of generating timestamps in this format. type Timestamp struct { state protoimpl.MessageState `protogen:"open.v1"` - // Represents seconds of UTC time since Unix epoch - // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to - // 9999-12-31T23:59:59Z inclusive. + // Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must + // be between -315576000000 and 315576000000 inclusive (which corresponds to + // 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z). Seconds int64 `protobuf:"varint,1,opt,name=seconds,proto3" json:"seconds,omitempty"` - // Non-negative fractions of a second at nanosecond resolution. Negative - // second values with fractions must still have non-negative nanos values - // that count forward in time. Must be from 0 to 999,999,999 + // Non-negative fractions of a second at nanosecond resolution. This field is + // the nanosecond portion of the duration, not an alternative to seconds. + // Negative second values with fractions must still have non-negative nanos + // values that count forward in time. Must be between 0 and 999,999,999 // inclusive. Nanos int32 `protobuf:"varint,2,opt,name=nanos,proto3" json:"nanos,omitempty"` unknownFields protoimpl.UnknownFields diff --git a/src/runtime/vendor/modules.txt b/src/runtime/vendor/modules.txt index 7ea2921ee..48f195b28 100644 --- a/src/runtime/vendor/modules.txt +++ b/src/runtime/vendor/modules.txt @@ -720,8 +720,8 @@ google.golang.org/grpc/serviceconfig google.golang.org/grpc/stats google.golang.org/grpc/status google.golang.org/grpc/tap -# google.golang.org/protobuf v1.36.7 -## explicit; go 1.22 +# google.golang.org/protobuf v1.36.11 +## explicit; go 1.23 google.golang.org/protobuf/encoding/protodelim google.golang.org/protobuf/encoding/protojson google.golang.org/protobuf/encoding/prototext