container-device-interface 1.1.1

CDI (Container Device Interface), is a specification, for container-runtimes, to support third-party devices.
Documentation
name: Update kata-containers on the runner

# Refreshes the kata-containers install and source tree the self-hosted
# runner uses to execute the CDI E2E CI (see .github/workflows/ci.yaml).
#
# For a given kata-containers release tag this workflow:
#   * checks out the matching kata-containers source at
#     ${KATA_SRC_DIR} (where ci.yaml expects tools/osbuilder & build/);
#   * normalizes ownership of the tree to the runner user so ci.yaml can run
#     plain git/make against it (docker builds and sudo leave root-owned
#     files behind, which trips git's "dubious ownership" guard);
#   * rebuilds rootfs-image-nvidia-gpu-tarball with KBUILD_SIGN_PIN so
#     ${ROOTFS_IMAGE_DIR} contains a kata-agent matching the release and a
#     *signed* nvidia driver (the guest kernel enforces module signatures);
#   * installs the kata-static-<tag>-arm64.tar.zst payload to /opt/kata
#     (the previous install is moved aside, not deleted);
#   * overlays the locally-built, signed vmlinuz-nvidia-gpu.container so the
#     booting kernel and the signed modules share the same signing key
#     (kata's released kernel is signed with a key we don't hold).
#
# /etc/kata-containers/configuration.toml is intentionally NOT touched: it
# carries runner-local settings. The new release defaults stay available
# under /opt/kata/share/defaults/kata-containers/ for manual review.

on:
  workflow_dispatch:
    inputs:
      kata-tag:
        description: "kata-containers release tag (e.g. 3.20.0). Leave empty for the latest release."
        required: false
        type: string
        default: ""
      # For dependency-transition windows (e.g. the oci-spec bump): rebuild
      # rootfs/kernel from a kata WIP ref without reverting the tree to a
      # release. Release-only steps (kata-static install, tag resolution)
      # are skipped; the installed runtime stays as-is.
      kata-ref:
        description: "Arbitrary kata ref (branch/SHA) from the canonical kata repo. Overrides kata-tag; skips the kata-static release install."
        required: false
        type: string
        default: ""

permissions:
  contents: read

concurrency:
  group: update-kata-${{ github.ref }}
  cancel-in-progress: false

env:
  KATA_REPO: kata-containers/kata-containers
  KATA_SRC_DIR: /home/container-device-interface-rs/actions-runner/_work/kata-containers
  KATA_INSTALL_PREFIX: /opt/kata
  ROOTFS_IMAGE_DIR: /home/container-device-interface-rs/actions-runner/_work/kata-containers/build/rootfs-image-nvidia-gpu/builddir/rootfs-image/ubuntu_rootfs
  IMAGE_BUILDER_DIR: /home/container-device-interface-rs/actions-runner/_work/kata-containers/tools/osbuilder/image-builder
  # kata's local-build output dir (relative to KATA_SRC_DIR) where the
  # nvidia-gpu kernel tarball lands; consumed by the overlay step below.
  LOCAL_BUILD_DIR: tools/packaging/kata-deploy/local-build/build
  # Passphrase for the kernel build's ephemeral module-signing key. The kernel
  # build generates a random key, embeds its public half in the vmlinuz + signs
  # the in-tree modules, then encrypts the private key with this pin; the rootfs
  # build (nvidia_chroot.sh) decrypts it with the SAME pin to sign nvidia.ko.
  # It only has to be non-empty and identical across the kernel and rootfs
  # signing of one build, so a hardcoded constant is fine: it guards a
  # throwaway per-build CI key, not a real secret. Setting it turns on
  # CONFIG_MODULE_SIG_FORCE + module signing.
  KBUILD_SIGN_PIN: cdi-rs-tests

jobs:
  update:
    runs-on: g5g.metal
    steps:
      - name: Ensure GitHub CLI is installed
        run: |
          set -euo pipefail

          if command -v gh >/dev/null 2>&1; then
            echo "gh already present: $(gh --version | head -n 1)"
            exit 0
          fi

          echo "gh not found, installing from the official apt repository..."
          export DEBIAN_FRONTEND=noninteractive
          if ! command -v curl >/dev/null 2>&1; then
            sudo -E apt-get update
            sudo -E apt-get install -y curl
          fi

          sudo install -m 0755 -d /etc/apt/keyrings
          curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
            | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null
          sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
            | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null

          sudo -E apt-get update
          sudo -E apt-get install -y gh

          echo "Installed $(gh --version | head -n 1)"

      - name: Resolve release tag
        id: tag
        if: ${{ inputs.kata-ref == '' }}
        env:
          GH_TOKEN: ${{ github.token }}
          REQUESTED_TAG: ${{ inputs.kata-tag }}
        run: |
          set -euo pipefail

          # `gh release view <tag>` maps to GET /repos/${KATA_REPO}/releases/tags/<tag>
          # and `gh release view` (no tag) to .../releases/latest. An empty or
          # "latest" request resolves the latest release. Capture stderr so a
          # transient API/network failure surfaces instead of being silently
          # relabelled "release not found" (the release may well exist).
          want="${REQUESTED_TAG}"
          [[ "${want}" == "latest" ]] && want=""

          err=$(mktemp)
          if ! tag=$(gh release view ${want:+"${want}"} \
                       --repo "${KATA_REPO}" \
                       --json tagName --jq '.tagName' 2>"${err}"); then
            echo "ERROR: 'gh release view ${want:-<latest>}' failed for ${KATA_REPO}:" >&2
            cat "${err}" >&2
            exit 1
          fi

          if [[ -z "${tag}" ]]; then
            echo "ERROR: could not resolve a tag for '${REQUESTED_TAG:-latest}' in ${KATA_REPO}" >&2
            exit 1
          fi

          echo "Resolved '${REQUESTED_TAG:-latest}' -> ${tag}"
          echo "tag=${tag}" >> "${GITHUB_OUTPUT}"

      - name: Sync kata-containers source tree to ${{ inputs.kata-ref || steps.tag.outputs.tag }}
        env:
          TAG: ${{ steps.tag.outputs.tag }}
          REF: ${{ inputs.kata-ref }}
        run: |
          set -euo pipefail

          parent=$(dirname "${KATA_SRC_DIR}")
          if [[ ! -d "${parent}" ]]; then
            sudo mkdir -p "${parent}"
            sudo chown "$(id -u)":"$(id -g)" "${parent}"
          fi

          if [[ ! -d "${KATA_SRC_DIR}/.git" ]]; then
            echo "No source tree at ${KATA_SRC_DIR}, cloning ${KATA_REPO}..."
            # Empty/stale dir would make git clone fail; clear it first.
            if [[ -e "${KATA_SRC_DIR}" ]]; then
              sudo rm -rf "${KATA_SRC_DIR}"
            fi
            git clone "https://github.com/${KATA_REPO}.git" "${KATA_SRC_DIR}"
          fi

          # Normalize ownership BEFORE any git operation: docker builds and
          # earlier sudo git runs leave root-owned files (including inside
          # .git), which makes git refuse the repo with "dubious ownership"
          # and breaks ci.yaml's plain-git manifest reset. Owning the whole
          # tree as the runner user fixes the root cause, so neither this
          # workflow nor ci.yaml needs sudo git or safe.directory hacks.
          sudo chown -R "$(id -u)":"$(id -g)" "${KATA_SRC_DIR}"

          cd "${KATA_SRC_DIR}"
          # Drop any local mutations from a previous rootfs build before
          # moving the tree to a new tag. The build/ tree is regenerated
          # by `make rootfs-image-nvidia-gpu-tarball` below.
          git reset --hard
          git clean -ffdx
          if [[ -n "${REF}" ]]; then
            # Canonical repo only - fork code never reaches the runner.
            git fetch -- "https://github.com/${KATA_REPO}.git" "${REF}"
            git checkout --detach FETCH_HEAD
          else
            git fetch --tags --force --prune origin
            git checkout --detach "refs/tags/${TAG}"
          fi

          echo "kata-containers @ $(git rev-parse HEAD) ($(git describe --tags --always))"

      - name: Rebuild rootfs-image-nvidia-gpu (signed nvidia driver)
        run: |
          set -euo pipefail

          cd "${KATA_SRC_DIR}"
          build_dir="${KATA_SRC_DIR}/${LOCAL_BUILD_DIR}"
          kernel_tb="kata-static-kernel-nvidia-gpu.tar.zst"

          # Build the kernel and rootfs in ONE invocation every run so both
          # signing stages share a single key. rootfs-image-nvidia-gpu-tarball
          # DEPS on kernel-nvidia-gpu-tarball: the kernel build generates the
          # ephemeral signing key, embeds its public half in the vmlinuz and
          # ships the KBUILD_SIGN_PIN-encrypted private key in the kernel
          # headers, then the rootfs build (nvidia_chroot.sh) decrypts it with
          # the same pin to sign nvidia.ko.
          #
          # The kernel is deliberately NOT cached across runs: a restored kernel
          # carries an old public key while this run always re-signs nvidia.ko
          # with a freshly generated key, so the guest kernel rejects the
          # module ("Loading of module with unavailable key is rejected"). A
          # fresh matched build is the whole point.
          #
          # Build as the runner user that owns the checkout (the kata-deploy
          # build runs git in a container whose uid is derived from ${USER};
          # building as root via sudo trips git's "dubious ownership" guard).
          # script -fec keeps a TTY so docker output streams cleanly.
          export USER="${USER:-$(id -un)}"
          script -fec "USE_DOCKER=true KBUILD_SIGN_PIN=${KBUILD_SIGN_PIN} make rootfs-image-nvidia-gpu-tarball"

          if [[ ! -d "${ROOTFS_IMAGE_DIR}" ]]; then
            echo "ERROR: expected ${ROOTFS_IMAGE_DIR} after build, not found" >&2
            exit 1
          fi
          if [[ ! -x "${IMAGE_BUILDER_DIR}/image_builder.sh" ]]; then
            echo "ERROR: ${IMAGE_BUILDER_DIR}/image_builder.sh missing" >&2
            exit 1
          fi
          if [[ ! -f "${build_dir}/${kernel_tb}" ]]; then
            echo "ERROR: signed kernel tarball ${kernel_tb} not produced by the build" >&2
            ls -l "${build_dir}" >&2 || true
            exit 1
          fi

          # The containerized build leaves root-owned artifacts behind; own
          # them now so the next run's git clean and ci.yaml's plain git both
          # keep working without sudo.
          sudo chown -R "$(id -u)":"$(id -g)" "${KATA_SRC_DIR}"

          echo "rootfs ready at ${ROOTFS_IMAGE_DIR} (signed nvidia modules)"

      - name: Download kata-static asset
        id: download
        # Release-only: an arbitrary ref has no published kata-static asset;
        # the installed runtime stays at its current release.
        if: ${{ inputs.kata-ref == '' }}
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ steps.tag.outputs.tag }}
        run: |
          set -euo pipefail

          workdir=$(mktemp -d)
          echo "workdir=${workdir}" >> "${GITHUB_OUTPUT}"
          cd "${workdir}"

          gh release download "${TAG}" \
            --repo "${KATA_REPO}" \
            --pattern 'kata-static-*-arm64.tar.zst'

          asset=$(find . -maxdepth 1 -name 'kata-static-*-arm64.tar.zst' -printf '%f\n' 2>/dev/null | head -n 1 || true)
          if [[ -z "${asset}" ]]; then
            echo "ERROR: no kata-static arm64 asset in release ${TAG}" >&2
            exit 1
          fi
          echo "Downloaded ${asset} ($(du -h "${asset}" | cut -f1))"
          echo "asset=${workdir}/${asset}" >> "${GITHUB_OUTPUT}"

      - name: Install kata-static to /opt/kata
        if: ${{ inputs.kata-ref == '' }}
        env:
          TAG: ${{ steps.tag.outputs.tag }}
          ASSET: ${{ steps.download.outputs.asset }}
        run: |
          set -euo pipefail

          if [[ -d "${KATA_INSTALL_PREFIX}" ]]; then
            stamp=$(date -u +%Y%m%dT%H%M%SZ)
            backup="${KATA_INSTALL_PREFIX}.bak.${stamp}"
            sudo mv "${KATA_INSTALL_PREFIX}" "${backup}"
            echo "Previous install backed up to ${backup}"
          fi

          # kata-static tarballs are zstd-compressed and lay down files at
          # /opt/kata when extracted from /.
          sudo tar --zstd -xf "${ASSET}" -C /

          if [[ ! -x "${KATA_INSTALL_PREFIX}/bin/containerd-shim-kata-v2" ]]; then
            echo "ERROR: containerd-shim-kata-v2 not found after install" >&2
            exit 1
          fi

          echo "Installed kata-static ${TAG} to ${KATA_INSTALL_PREFIX}"

      - name: Overlay locally-built signed nvidia-gpu kernel
        run: |
          set -euo pipefail

          # The released kernel enforces module signatures against kata's
          # release key, but our nvidia modules are signed with our own
          # ephemeral key. Boot OUR kernel so the signatures match. This must
          # run AFTER the release kata-static install above, which would
          # otherwise clobber the kernel. The kernel tarball lays files down
          # under /opt/kata when extracted from /.
          kernel_tb="${KATA_SRC_DIR}/${LOCAL_BUILD_DIR}/kata-static-kernel-nvidia-gpu.tar.zst"
          if [[ ! -f "${kernel_tb}" ]]; then
            echo "ERROR: ${kernel_tb} missing; nvidia-gpu kernel not built" >&2
            exit 1
          fi
          sudo tar --zstd -xf "${kernel_tb}" -C /

          kernel="${KATA_INSTALL_PREFIX}/share/kata-containers/vmlinuz-nvidia-gpu.container"
          if [[ ! -e "${kernel}" ]]; then
            echo "ERROR: ${kernel} not present after overlay" >&2
            exit 1
          fi
          echo "Installed locally-built signed kernel: $(readlink -f "${kernel}")"

      - name: Refresh /usr/local/bin symlinks
        run: |
          set -euo pipefail

          # nerdctl resolves containerd-shim-kata-v2 via $PATH; the runner keeps
          # a symlink at /usr/local/bin pointing into /opt/kata. Re-create it
          # idempotently so a broken/missing link can't silently break the CI.
          sudo ln -sfn "${KATA_INSTALL_PREFIX}/bin/containerd-shim-kata-v2" \
            /usr/local/bin/containerd-shim-kata-v2

          target=$(readlink -f /usr/local/bin/containerd-shim-kata-v2 || true)
          if [[ "${target}" != "${KATA_INSTALL_PREFIX}/bin/containerd-shim-kata-v2" ]]; then
            echo "ERROR: /usr/local/bin/containerd-shim-kata-v2 resolves to '${target}'" >&2
            exit 1
          fi
          ls -l /usr/local/bin/containerd-shim-kata-v2

      - name: Cleanup downloaded asset
        if: always() && steps.download.outputs.workdir != ''
        env:
          WORKDIR: ${{ steps.download.outputs.workdir }}
        run: rm -rf "${WORKDIR}"

      - name: Summary
        env:
          TAG: ${{ inputs.kata-ref || steps.tag.outputs.tag }}
        run: |
          set -euo pipefail

          src_head=$(git -C "${KATA_SRC_DIR}" rev-parse --short HEAD)
          shim_version=$("${KATA_INSTALL_PREFIX}/bin/containerd-shim-kata-v2" --version 2>/dev/null | head -n 1 || echo "unknown")

          {
            echo "## kata-containers updated to \`${TAG}\`"
            echo ""
            echo "| item | value |"
            echo "| --- | --- |"
            echo "| source tree | \`${KATA_SRC_DIR}\` @ \`${src_head}\` |"
            echo "| install prefix | \`${KATA_INSTALL_PREFIX}\` |"
            echo "| shim version | \`${shim_version}\` |"
            echo "| rootfs dir | \`${ROOTFS_IMAGE_DIR}\` |"
            echo "| image builder | \`${IMAGE_BUILDER_DIR}\` |"
            echo ""
            echo "**Note:** \`/etc/kata-containers/configuration.toml\` was not modified."
            echo "Diff against \`${KATA_INSTALL_PREFIX}/share/defaults/kata-containers/\` if upstream defaults moved."
          } >> "${GITHUB_STEP_SUMMARY}"