Compare commits

..

18 Commits

Author SHA1 Message Date
Fabiano Fidêncio
f97388b0d9 versions: bump containerd active version to 2.2
SSIA

Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
2026-02-07 19:12:49 +01:00
Fabiano Fidêncio
481aed7886 tests: cri: Re-enable podsandboxapi tests
SSIA

Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
2026-02-07 19:12:30 +01:00
Manuel Huber
d9d1073cf1 gpu: Install packages for devkit
Introduce a new function to install additional packages into the
devkit flavor. With modprobe, we avoid errors on pod startup
related to loading nvidia kernel modules in the NVRC phase.
Note, the production flavor gets modprobe from busybox, see its
configuration file containing CONFIG_MODPROBE=y.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-06 09:58:32 +01:00
Manuel Huber
a786582d0b rootfs: deprecate initramfs dm-verity mode
Remove the initramfs folder, its build steps, and use the kernel
based dm-verity enforcement for the handlers which used the
initramfs mode. Also, remove the initramfs verity mode
capability from the shims and their configs.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
cf7f340b39 tests: Read and overwrite kernel_verity_parameters
Read the kernel_verity_paramers from the shim config and adjust
the root hash for the negative test.
Further, improve some of the test logic by using shared
functions. This especially ensures we don't read the full
journalctl logs on a node but only the portion of the logs we are
actually supposed to look at.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
7958be8634 runtime: Make kernel_verity_params overwritable
Similar to the kernel_params annotation, add a
kernel_verity_params annotation and add logic to make these
parameters overwritable. For instance, this can be used in test
logic to provide bogus dm-verity hashes for negative tests.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
7700095ea8 runtime-rs: Make kernel_verity_params overwritable
Similar to the kernel_params annotation, add a
kernel_verity_params annotation and add logic to make these
parameters overwritable. For instance, this can be used in test
logic to provide bogus dm-verity hashes for negative tests.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
472b50fa42 runtime-rs: Enable kernelinit dm-verity variant
This change introduces the kernel_verity_parameters knob to the
rust based shim, picking up dm-verity information in a new config
field (the corresponding build variable is already produced by
the shim build). The change extends the shim to parse dm-verity
information from this parameter and to construct the kernel command
line appropriately, based on the indicated initramfs or kernelinit
build variant.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
f639c3fa17 runtime: Enable kernelinit dm-verity variant
This change introduces the kernel_verity_parameters knob to the
Go based shim, picking up dm-verity information in a new config
field (the corresponding build variable is already produced by
the shim build). The change extends the shim to parse dm-verity
information from this parameter and to construct the kernel command
line appropriately, based on the indicated initramfs or kernelinit
build variant.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
e120dd4cc6 tests: cc: Remove quotes from kernel command line
With dm-mod.create parameters using quotes, we remove the
backslashes used to escape these quotes from the output we
retrieve. This will enable attestation tests to work with the
kernelinit dm-verity mode.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
976df22119 rootfs: Change condition for cryptsetup-bin
Measured rootfs mode and CDH secure storage feature require the
cryptsetup-bin and e2fsprogs components in the guest.
This change makes this more explicity - confidential guests are
users of the CDH secure container image layer storage feature.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
a3c4e0b64f rootfs: Introduce kernelinit dm-verity mode
This change introduces the kernelinit dm-verity mode, allowing
initramfs-less dm-verity enforcement against the rootfs image.
For this, the change introduces a new variable with dm-verity
information. This variable will be picked up by shim
configurations in subsequent commits.
This will allow the shims to build the kernel command line
with dm-verity information based on the existing
kernel_parameters configuration knob and a new
kernel_verity_params configuration knob. The latter
specifically provides the relevant dm-verity information.
This new configuration knob avoids merging the verity
parameters into the kernel_params field. Avoiding this, no
cumbersome escape logic is required as we do not need to pass the
dm-mod.create="..." parameter directly in the kernel_parameters,
but only relevant dm-verity parameters in semi-structured manner
(see above). The only place where the final command line is
assembled is in the shims. Further, this is a line easy to comment
out for developers to disable dm-verity enforcement (or for CI
tasks).

This change produces the new kernelinit dm-verity parameters for
the NVIDIA runtime handlers, and modifies the format of how
these parameters are prepared for all handlers. With this, the
parameters are currently no longer provided to the
kernel_params configuration knob for any runtime handler.
This change alone should thus not be used as dm-verity
information will no longer be picked up by the shims.

systemd-analyze on the coco-dev handler shows that using the
kernelinit mode on a local machine, less time is spent in the
kernel phase, slightly speeding up pod start-up. On that machine,
the average of 172.5ms was reduced to 141ms (4 measurements, each
with a basic pod manifest), i.e., the kernel phase duration is
improved by about 18 percent.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
83a0bd1360 gpu: use dm-verity for the non-TEE GPU handler
Use a dm-verity protected rootfs image for the non-TEE NVIDIA
GPU handler as well.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
02ed4c99bc rootfs: Use maxdepth=1 to search for kata tarballs
These tarballs are in the top layer of the build directory,
no need to traverse all sub-directories.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
d37db5f068 rootfs: Restore "gpu: Handle root_hash.txt ..."
This reverts commit 923f97bc66 in
order to re-instantiate the logic from commit
e4a13b9a4a.

The latter commit was previously reverted due to the NVIDIA GPU TEE
handler using an initrd, not an image.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
f1ca547d66 initramfs: introduce log function
Log to /dev/kmsg, this way logs will show up and not get lost.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
6d0bb49716 runtime: nvidia: Use img and sanitize whitespaces
Shift NVIDIA shim configurations to use an image instead of an initrd,
and remove trailing whitespaces from the configs.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
Manuel Huber
282014000f tests: cc: support initrd, image for attestation
Allow using an image instead of an initrd. For confidential
guests using images, the assumption is that the guest kernel uses
dm-verity protection, implicitly measuring the rootfs image via
the kernel command line's dm-verity information.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-02-05 23:04:35 +01:00
66 changed files with 922 additions and 2499 deletions

View File

@@ -26,8 +26,6 @@ jobs:
matrix:
containerd_version: ['active']
vmm: ['dragonball', 'cloud-hypervisor', 'qemu-runtime-rs']
# TODO: enable me when https://github.com/containerd/containerd/issues/11640 is fixed
if: false
runs-on: ubuntu-22.04
env:
CONTAINERD_VERSION: ${{ matrix.containerd_version }}

View File

@@ -26,8 +26,6 @@ jobs:
matrix:
containerd_version: ['active']
vmm: ['qemu-runtime-rs']
# TODO: enable me when https://github.com/containerd/containerd/issues/11640 is fixed
if: false
runs-on: s390x-large
env:
CONTAINERD_VERSION: ${{ matrix.containerd_version }}
@@ -48,7 +46,7 @@ jobs:
TARGET_BRANCH: ${{ inputs.target-branch }}
- name: Install dependencies
run: bash tests/integration/cri-containerd/gha-run.sh
run: bash tests/integration/cri-containerd/gha-run.sh install-dependencies
env:
GH_TOKEN: ${{ github.token }}

View File

@@ -1,75 +0,0 @@
name: Build helm multi-arch image
on:
schedule:
# Run every Sunday at 12:00 UTC (12 hours after kubectl image build)
- cron: '0 12 * * 0'
workflow_dispatch:
# Allow manual triggering
push:
branches:
- main
paths:
- 'tools/packaging/helm/Dockerfile'
- '.github/workflows/build-helm-image.yaml'
permissions: {}
env:
REGISTRY: quay.io
IMAGE_NAME: kata-containers/helm
jobs:
build-and-push:
name: Build and push multi-arch image
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to Quay.io
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ vars.QUAY_DEPLOYER_USERNAME }}
password: ${{ secrets.QUAY_DEPLOYER_PASSWORD }}
- name: Get helm version
id: helm-version
run: |
HELM_VERSION=$(curl -s https://api.github.com/repos/helm/helm/releases/latest | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
echo "version=${HELM_VERSION}" >> "$GITHUB_OUTPUT"
- name: Generate image metadata
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=raw,value={{date 'YYYYMMDD'}}
type=raw,value=${{ steps.helm-version.outputs.version }}
type=sha,prefix=
- name: Build and push multi-arch image
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0
with:
context: tools/packaging/helm/
file: tools/packaging/helm/Dockerfile
platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -284,15 +284,11 @@ jobs:
echo "${QUAY_DEPLOYER_PASSWORD}" | helm registry login quay.io --username "${QUAY_DEPLOYER_USERNAME}" --password-stdin
echo "${GITHUB_TOKEN}" | helm registry login ghcr.io --username "${GITHUB_ACTOR}" --password-stdin
- name: Push helm charts to the OCI registries
- name: Push helm chart to the OCI registries
run: |
release_version=$(./tools/packaging/release/release.sh release-version)
# Push kata-deploy chart
helm push "kata-deploy-${release_version}.tgz" oci://quay.io/kata-containers/kata-deploy-charts
helm push "kata-deploy-${release_version}.tgz" oci://ghcr.io/kata-containers/kata-deploy-charts
# Push kata-lifecycle-manager chart
helm push "kata-lifecycle-manager-${release_version}.tgz" oci://quay.io/kata-containers/kata-deploy-charts
helm push "kata-lifecycle-manager-${release_version}.tgz" oci://ghcr.io/kata-containers/kata-deploy-charts
publish-release:
name: publish-release

View File

@@ -1,118 +0,0 @@
# Kata Containers Lifecycle Management
## Overview
Kata Containers lifecycle management in Kubernetes consists of two operations:
1. **Installation** - Deploy Kata Containers to cluster nodes
2. **Upgrades** - Update Kata Containers to newer versions without disrupting workloads
The Kata Containers project provides two Helm charts to address these needs:
| Chart | Purpose |
|-------|---------|
| `kata-deploy` | Initial installation and configuration |
| `kata-lifecycle-manager` | Orchestrated rolling upgrades with verification |
---
## Installation with kata-deploy
The `kata-deploy` Helm chart installs Kata Containers across all (or selected) nodes using a Kubernetes DaemonSet. When deployed, it:
- Installs Kata runtime binaries on each node
- Configures the container runtime (containerd) to use Kata
- Registers RuntimeClasses (`kata-qemu-nvidia-gpu-snp`, `kata-qemu-nvidia-gpu-tdx`, `kata-qemu-nvidia-gpu`, etc.)
After installation, workloads can use Kata isolation by specifying `runtimeClassName: kata-qemu-nvidia-gpu-snp` (or another Kata RuntimeClass) in their pod spec.
---
## Upgrades with kata-lifecycle-manager
### The Problem
Standard `helm upgrade kata-deploy` updates all nodes simultaneously via the DaemonSet. This approach:
- Provides no per-node verification
- Offers no controlled rollback mechanism
- Can leave the cluster in an inconsistent state if something fails
### The Solution
The `kata-lifecycle-manager` Helm chart uses Argo Workflows to orchestrate upgrades with the following guarantees:
| Guarantee | Description |
|-----------|-------------|
| **Sequential Processing** | Nodes are upgraded one at a time |
| **Per-Node Verification** | A user-provided pod validates Kata functionality after each node upgrade |
| **Fail-Fast** | If verification fails, the workflow stops immediately |
| **Automatic Rollback** | On failure, Helm rollback is executed and the node is restored |
### Upgrade Flow
For each node in the cluster:
1. **Cordon** - Mark node as unschedulable
2. **Drain** (optional) - Evict existing workloads
3. **Upgrade** - Run `helm upgrade kata-deploy` targeting this node
4. **Wait** - Ensure kata-deploy DaemonSet pod is ready
5. **Verify** - Run verification pod to confirm Kata works
6. **Uncordon** - Mark node as schedulable again
If verification fails on any node, the workflow:
- Rolls back the Helm release
- Uncordons the node
- Stops processing (remaining nodes are not upgraded)
### Verification Pod
Users must provide a verification pod that tests Kata functionality. This pod:
- Uses a Kata RuntimeClass
- Is scheduled on the specific node being verified
- Runs whatever validation logic the user requires (smoke tests, attestation checks, etc.)
**Basic GPU Verification Example:**
For clusters with NVIDIA GPUs, the CUDA VectorAdd sample provides a more comprehensive verification:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: ${TEST_POD}
spec:
runtimeClassName: kata-qemu-nvidia-gpu-snp # or kata-qemu-nvidia-gpu-tdx
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: ${NODE}
containers:
- name: cuda-vectoradd
image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04
resources:
limits:
nvidia.com/pgpu: "1"
memory: 16Gi
```
This verifies that GPU passthrough works correctly with the upgraded Kata runtime.
The placeholders `${NODE}` and `${TEST_POD}` are substituted at runtime.
---
## Demo Recordings
| Demo | Description | Link |
|------|-------------|------|
| Sunny Path | Successful upgrade from 3.24.0 to 3.25.0 | [TODO] |
| Rainy Path | Failed verification triggers rollback | [TODO] |
---
## References
- [kata-deploy Helm Chart](tools/packaging/kata-deploy/helm-chart/README.md)
- [kata-lifecycle-manager Helm Chart](tools/packaging/kata-deploy/helm-chart/kata-lifecycle-manager/README.md)
- [kata-lifecycle-manager Design Document](docs/design/kata-lifecycle-manager-design.md)

View File

@@ -28,15 +28,13 @@ Bug fixes are released as part of `MINOR` or `MAJOR` releases only. `PATCH` is a
## Release Process
### Bump the `VERSION` and `Chart.yaml` files
### Bump the `VERSION` and `Chart.yaml` file
When the `kata-containers/kata-containers` repository is ready for a new release,
first create a PR to set the release in the [`VERSION`](./../VERSION) file and update the
`version` and `appVersion` in the following `Chart.yaml` files:
- [`kata-deploy/Chart.yaml`](./../tools/packaging/kata-deploy/helm-chart/kata-deploy/Chart.yaml)
- [`kata-lifecycle-manager/Chart.yaml`](./../tools/packaging/kata-deploy/helm-chart/kata-lifecycle-manager/Chart.yaml)
Have the PR merged before proceeding.
`version` and `appVersion` in the
[`Chart.yaml`](./../tools/packaging/kata-deploy/helm-chart/kata-deploy/Chart.yaml) file and
have it merged.
### Lock the `main` branch

View File

@@ -19,7 +19,6 @@ Kata Containers design documents:
- [Design for direct-assigned volume](direct-blk-device-assignment.md)
- [Design for core-scheduling](core-scheduling.md)
- [Virtualization Reference Architecture](kata-vra.md)
- [Design for kata-lifecycle-manager Helm chart](kata-lifecycle-manager-design.md)
---
- [Design proposals](proposals)

View File

@@ -1,502 +0,0 @@
# Kata Containers Lifecycle Manager Design
## Summary
This document proposes a Helm chart-based orchestration solution for Kata Containers that
enables controlled, node-by-node upgrades with verification and rollback capabilities
using Argo Workflows.
## Motivation
### Problem Statement
Upgrading Kata Containers in a production Kubernetes cluster presents several challenges:
1. **Workload Scheduling Control**: New Kata workloads should not be scheduled on a node
during upgrade until the new runtime is verified.
2. **Verification Gap**: There is no standardized way to verify that Kata is working correctly
after an upgrade before allowing workloads to return to the node. This solution addresses
the gap by running a user-provided verification pod on each upgraded node.
3. **Rollback Complexity**: If an upgrade fails, administrators must manually coordinate
rollback across multiple nodes.
4. **Controlled Rollout**: Operators need the ability to upgrade nodes incrementally
(canary approach) with fail-fast behavior if any node fails verification.
5. **Multi-Architecture Support**: The upgrade tooling must work across all architectures
supported by Kata Containers (amd64, arm64, s390x, ppc64le).
### Current State
The `kata-deploy` Helm chart provides installation and configuration of Kata Containers,
including a post-install verification job. However, there is no built-in mechanism for
orchestrating upgrades across nodes in a controlled manner.
## Goals
1. Provide a standardized, automated way to upgrade Kata Containers node-by-node
2. Ensure each node is verified before returning to service
3. Support user-defined verification logic
4. Automatically rollback if verification fails
5. Work with the existing `kata-deploy` Helm chart
6. Support all Kata-supported architectures
## Non-Goals
1. Initial Kata Containers installation (use kata-deploy Helm chart for that)
2. Managing Kubernetes cluster upgrades
3. Providing Kata-specific verification logic (this is user responsibility)
4. Managing Argo Workflows installation
## Argo Workflows Dependency
### What Works Without Argo
The following components work independently of Argo Workflows:
| Component | Description |
|-----------|-------------|
| **kata-deploy Helm chart** | Full installation, configuration, `RuntimeClasses` |
| **Post-install verification** | Helm hook runs verification pod after install |
| **Label-gated deployment** | Progressive rollout via node labels |
| **Manual upgrades** | User can script: cordon, helm upgrade, verify, `uncordon` |
Users who do not want Argo can still:
- Install and configure Kata via kata-deploy
- Perform upgrades manually or with custom scripts
- Use the verification pod pattern in their own automation
### What Requires Argo
The kata-lifecycle-manager Helm chart provides orchestration via Argo Workflows:
| Feature | Description |
|---------|-------------|
| **Automated node-by-node upgrades** | Sequential processing with fail-fast |
| **Taint-based node selection** | Select nodes by taint key/value |
| **`WorkflowTemplate`** | Reusable upgrade workflow |
| **Rollback entrypoint** | `argo submit --entrypoint rollback-node` |
| **Status tracking** | Node annotations updated at each phase |
### For Users Already Using Argo
If your cluster already has Argo Workflows installed:
```bash
# Install kata-lifecycle-manager - integrates with your existing Argo installation
helm install kata-lifecycle-manager oci://ghcr.io/kata-containers/kata-deploy-charts/kata-lifecycle-manager \
--set argoNamespace=argo \
--set-file defaults.verificationPod=./verification-pod.yaml
# Trigger upgrades via argo CLI or integrate with existing workflows
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager -p target-version=3.25.0
```
kata-lifecycle-manager can also be triggered by other Argo workflows, CI/CD pipelines, or `GitOps`
tools that support Argo.
### For Users Not Wanting Argo
If you prefer not to use Argo Workflows:
1. **Use kata-deploy directly** - handles installation and basic verification
2. **Script your own orchestration** - example approach:
```bash
#!/bin/bash
# Manual upgrade script (no Argo required)
set -euo pipefail
VERSION="3.25.0"
# Upgrade each node with Kata runtime
kubectl get nodes -l katacontainers.io/kata-runtime=true -o name | while read -r node_path; do
NODE="${node_path#node/}"
echo "Upgrading $NODE..."
kubectl cordon "$NODE"
helm upgrade kata-deploy oci://ghcr.io/kata-containers/kata-deploy-charts/kata-deploy \
--namespace kube-system \
--version "$VERSION" \
--reuse-values \
--wait
# Wait for DaemonSet pod on this node
kubectl rollout status daemonset/kata-deploy -n kube-system
# Run verification (apply your pod, wait, check exit code)
kubectl apply -f verification-pod.yaml
kubectl wait pod/kata-verify --for=jsonpath='{.status.phase}'=Succeeded --timeout=180s
kubectl delete pod/kata-verify
kubectl uncordon "$NODE"
echo "$NODE upgraded successfully"
done
```
This approach requires more manual effort but avoids the Argo dependency.
## Proposed Design
### Architecture Overview
```text
┌─────────────────────────────────────────────────────────────────┐
│ Argo Workflows Controller │
│ (pre-installed) │
└────────────────────────────┬────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ kata-lifecycle-manager Helm Chart │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ WorkflowTemplate │ │
│ │ - upgrade-all-nodes (entrypoint) │ │
│ │ - upgrade-single-node (per-node steps) │ │
│ │ - rollback-node (manual recovery) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ RBAC Resources │ │
│ │ - ServiceAccount │ │
│ │ - ClusterRole (node, pod, helm operations) │ │
│ │ - ClusterRoleBinding │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ kata-deploy Helm Chart │
│ (existing installation) │
└─────────────────────────────────────────────────────────────────┘
```
### Upgrade Flow
For each node selected by the upgrade label:
```text
┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────┐
│ Prepare │───▶│ Cordon │───▶│ Upgrade │───▶│Wait Ready │
│ (annotate) │ │ (mark │ │ (helm │ │(kata-deploy│
│ │ │unschedulable)│ │ upgrade) │ │ DaemonSet) │
└────────────┘ └──────────────┘ └────────────┘ └────────────┘
┌────────────┐ ┌──────────────┐ ┌────────────┐
│ Complete │◀───│ Uncordon │◀───│ Verify │
│ (annotate │ │ (mark │ │ (user pod)│
│ version) │ │schedulable) │ │ │
└────────────┘ └──────────────┘ └────────────┘
```
**Note:** Drain is not required for Kata upgrades. Running Kata VMs continue using
the in-memory binaries. Only new workloads use the upgraded binaries. Cordon ensures
the verification pod runs before any new workloads are scheduled with the new runtime.
**Optional Drain:** For users who prefer to evict workloads before any maintenance
operation, an optional drain step can be enabled via `drain-enabled=true`. When
enabled, an additional drain step runs after cordon and before upgrade.
### Node Selection Model
Nodes can be selected for upgrade using **labels**, **taints**, or **both**.
**Label-based selection:**
```bash
# Select nodes by label
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p node-selector="katacontainers.io/kata-lifecycle-manager-window=true"
```
**Taint-based selection:**
Some organizations use taints to mark nodes for maintenance. The workflow supports
selecting nodes by taint key and optionally taint value:
```bash
# Select nodes with a specific taint
kubectl taint nodes worker-1 kata-lifecycle-manager=pending:NoSchedule
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p node-taint-key=kata-lifecycle-manager \
-p node-taint-value=pending
```
**Combined selection:**
Labels and taints can be used together for precise targeting:
```bash
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p node-selector="node-pool=kata-pool" \
-p node-taint-key=maintenance
```
This allows operators to:
1. Upgrade a single canary node first
2. Gradually add nodes to the upgrade window
3. Control upgrade timing via `GitOps` or automation
4. Integrate with existing taint-based maintenance workflows
### Node Pool Support
The node selector and taint selector parameters enable basic node pool targeting:
```bash
# Upgrade only nodes matching a specific node pool label
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p node-selector="node-pool=kata-pool"
```
**Current Capabilities:**
| Feature | Status | Chart | Notes |
|---------|--------|-------|-------|
| Label-based selection | Supported | kata-lifecycle-manager | Works with any label combination |
| Taint-based selection | Supported | kata-lifecycle-manager | Select by taint key/value |
| Sequential upgrades | Supported | kata-lifecycle-manager | One node at a time with fail-fast |
| Pool-specific verification pods | Not supported | kata-lifecycle-manager | Same verification for all nodes |
| Pool-ordered upgrades | Not supported | kata-lifecycle-manager | Upgrade pool A before pool B |
See the [Potential Enhancements](#potential-enhancements) section for future work.
### Verification Model
**Verification runs on each node that is upgraded.** The node is only `uncordoned` after
its verification pod succeeds. If verification fails, automatic rollback is triggered
to restore the previous version before `uncordoning` the node.
**Common failure modes detected by verification:**
- Pod stuck in Pending/`ContainerCreating` (runtime can't start VM)
- Pod crashes immediately (containerd/CRI-O configuration issues)
- Pod times out (resource issues, image pull failures)
- Pod exits with non-zero code (verification logic failed)
All of these trigger automatic rollback. The workflow logs include pod status, events,
and logs to help diagnose the issue.
The user provides a complete Pod YAML that:
- Uses the Kata runtime class they want to verify
- Contains their verification logic (e.g., attestation checks)
- Exits 0 on success, non-zero on failure
- Includes tolerations for cordoned nodes (verification runs while node is cordoned)
- Includes a `nodeSelector` to ensure it runs on the specific node being upgraded
When upgrading multiple nodes (via label selector), nodes are processed sequentially.
For each node, the following placeholders are substituted with that node's specific values,
ensuring the verification pod runs on the exact node that was just upgraded:
- `${NODE}` - The hostname of the node being upgraded/verified
- `${TEST_POD}` - A generated unique pod name
Example verification pod:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: ${TEST_POD}
spec:
runtimeClassName: kata-qemu
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: ${NODE}
tolerations:
- operator: Exists # Required: node is cordoned during verification
containers:
- name: verify
image: quay.io/kata-containers/alpine-bash-curl:latest
command: ["uname", "-a"]
```
This design keeps verification logic entirely in the user's domain, supporting:
- Different runtime classes (`kata-qemu`, `kata-qemu-snp`, `kata-qemu-tdx`, etc.)
- TEE-specific attestation verification
- GPU/accelerator validation
- Custom application smoke tests
### Sequential Execution with Fail-Fast
Nodes are upgraded strictly sequentially using recursive Argo templates. This design
ensures that if any node fails verification, the workflow stops immediately before
touching remaining nodes, preventing a mixed-version fleet.
Alternative approaches considered:
- **`withParam` + semaphore**: Provides cleaner UI but semaphore only controls concurrency,
not failure propagation. Other nodes would still proceed after one fails.
- **`withParam` + `failFast`**: Would be ideal, but Argo only supports `failFast` for DAG
tasks, not for steps with `withParam`.
The recursive template approach (`upgrade-node-chain`) naturally provides fail-fast
behavior because if any step in the chain fails, the recursion stops.
### Status Tracking
Node upgrade status is tracked via Kubernetes annotations:
| Annotation | Values |
|------------|--------|
| `katacontainers.io/kata-lifecycle-manager-status` | preparing, cordoned, draining, upgrading, verifying, completed, rolling-back, rolled-back |
| `katacontainers.io/kata-current-version` | Version string (e.g., "3.25.0") |
This enables:
- Monitoring upgrade progress via `kubectl get nodes`
- Integration with external monitoring systems
- Recovery from interrupted upgrades
### Rollback Support
**Automatic rollback on verification failure:** If the verification pod fails (non-zero exit),
kata-lifecycle-manager automatically:
1. Runs `helm rollback` to revert to the previous Helm release
2. Waits for kata-deploy DaemonSet to be ready with the previous version
3. `Uncordons` the node
4. Annotates the node with `rolled-back` status
This ensures nodes are never left in a broken state.
**Manual rollback:** For cases where you need to rollback a successfully upgraded node:
```bash
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
--entrypoint rollback-node \
-p node-name=worker-1
```
## Components
### Container Images
Two multi-architecture container images are built and published:
| Image | Purpose | Architectures |
|-------|---------|---------------|
| `quay.io/kata-containers/kubectl:latest` | Kubernetes operations | amd64, arm64, s390x, ppc64le |
| `quay.io/kata-containers/helm:latest` | Helm operations | amd64, arm64, s390x, ppc64le |
Images are rebuilt weekly to pick up security updates and tool version upgrades.
### Helm Chart Structure
```text
kata-lifecycle-manager/
├── Chart.yaml # Chart metadata
├── values.yaml # Configurable defaults
├── README.md # Usage documentation
└── templates/
├── _helpers.tpl # Template helpers
├── rbac.yaml # ServiceAccount, ClusterRole, ClusterRoleBinding
└── workflow-template.yaml # Argo `WorkflowTemplate`
```
### RBAC Requirements
The workflow requires the following permissions:
| Resource | Verbs | Purpose |
|----------|-------|---------|
| nodes | get, list, watch, patch | `cordon`/`uncordon`, annotations |
| pods | get, list, watch, create, delete | Verification pods |
| pods/log | get | Verification output |
| `daemonsets` | get, list, watch | Wait for `kata-deploy` |
## User Experience
### Installation
```bash
# Install kata-lifecycle-manager with verification config
helm install kata-lifecycle-manager oci://ghcr.io/kata-containers/kata-deploy-charts/kata-lifecycle-manager \
--set-file defaults.verificationPod=/path/to/verification-pod.yaml
```
### Triggering an Upgrade
```bash
# Label nodes for upgrade
kubectl label node worker-1 katacontainers.io/kata-lifecycle-manager-window=true
# Submit upgrade workflow
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0
# Watch progress
argo watch @latest
```
### Monitoring
```bash
kubectl get nodes \
-L katacontainers.io/kata-runtime \
-L katacontainers.io/kata-lifecycle-manager-status \
-L katacontainers.io/kata-current-version
```
## Security Considerations
1. **Namespace-Scoped Templates**: The chart creates a `WorkflowTemplate` (namespace-scoped)
rather than `ClusterWorkflowTemplate` by default, reducing blast radius.
2. **Required Verification**: The chart fails to install if `defaults.verificationPod` is
not provided, ensuring upgrades are always verified.
3. **Minimal RBAC**: The `ServiceAccount` has only the permissions required for upgrade
operations.
4. **User-Controlled Verification**: Verification logic is entirely user-defined, avoiding
any hardcoded assumptions about what "working" means.
## Integration with Release Process
The `kata-lifecycle-manager` chart is:
- Packaged alongside `kata-deploy` during releases
- Published to the same OCI registries (`quay.io`, `ghcr.io`)
- Versioned to match `kata-deploy`
## Potential Enhancements
The following enhancements could be considered if needed:
### kata-lifecycle-manager
1. **Pool-Specific Verification**: Different verification pods for different node pools
(e.g., GPU nodes vs. CPU-only nodes).
2. **Ordered Pool Upgrades**: Upgrade node pool A completely before starting pool B.
## Alternatives Considered
### 1. DaemonSet-Based Upgrades
Using a DaemonSet to coordinate upgrades on each node.
**Rejected because**: DaemonSets don't provide the node-by-node sequencing and
verification workflow needed for controlled upgrades.
### 2. Operator Pattern
Building a Kubernetes Operator to manage upgrades.
**Rejected because**: Adds significant complexity and maintenance burden. Argo Workflows
is already widely adopted and provides the orchestration primitives needed.
### 3. Shell Script Orchestration
Providing a shell script that loops through nodes.
**Rejected because**: Less reliable, harder to monitor, no built-in retry/recovery,
and doesn't integrate with Kubernetes-native tooling.
## References
- [kata-deploy Helm Chart](https://github.com/kata-containers/kata-containers/tree/main/tools/packaging/kata-deploy/helm-chart/kata-deploy)
- [Argo Workflows](https://argoproj.github.io/argo-workflows/)
- [Helm Documentation](https://helm.sh/docs/)

View File

@@ -149,6 +149,9 @@ pub const KATA_ANNO_CFG_HYPERVISOR_KERNEL_HASH: &str =
/// A sandbox annotation for passing additional guest kernel parameters.
pub const KATA_ANNO_CFG_HYPERVISOR_KERNEL_PARAMS: &str =
"io.katacontainers.config.hypervisor.kernel_params";
/// A sandbox annotation for passing guest dm-verity parameters.
pub const KATA_ANNO_CFG_HYPERVISOR_KERNEL_VERITY_PARAMS: &str =
"io.katacontainers.config.hypervisor.kernel_verity_params";
/// A sandbox annotation for passing a container guest image path.
pub const KATA_ANNO_CFG_HYPERVISOR_IMAGE_PATH: &str = "io.katacontainers.config.hypervisor.image";
/// A sandbox annotation for passing a container guest image SHA-512 hash value.
@@ -630,6 +633,9 @@ impl Annotation {
KATA_ANNO_CFG_HYPERVISOR_KERNEL_PARAMS => {
hv.boot_info.replace_kernel_params(value);
}
KATA_ANNO_CFG_HYPERVISOR_KERNEL_VERITY_PARAMS => {
hv.boot_info.replace_kernel_verity_params(value)?;
}
KATA_ANNO_CFG_HYPERVISOR_IMAGE_PATH => {
hv.boot_info.validate_boot_path(value)?;
hv.boot_info.image = value.to_string();

View File

@@ -76,6 +76,134 @@ const VIRTIO_FS_INLINE: &str = "inline-virtio-fs";
const MAX_BRIDGE_SIZE: u32 = 5;
const KERNEL_PARAM_DELIMITER: &str = " ";
/// Block size (in bytes) used by dm-verity block size validation.
pub const VERITY_BLOCK_SIZE_BYTES: u64 = 512;
/// Parsed kernel dm-verity parameters.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct KernelVerityParams {
/// Root hash value.
pub root_hash: String,
/// Salt used to generate verity hash tree.
pub salt: String,
/// Number of data blocks in the verity mapping.
pub data_blocks: u64,
/// Data block size in bytes.
pub data_block_size: u64,
/// Hash block size in bytes.
pub hash_block_size: u64,
}
/// Parse and validate kernel dm-verity parameters.
pub fn parse_kernel_verity_params(params: &str) -> Result<Option<KernelVerityParams>> {
if params.trim().is_empty() {
return Ok(None);
}
let mut values = HashMap::new();
for field in params.split(',') {
let field = field.trim();
if field.is_empty() {
continue;
}
let mut parts = field.splitn(2, '=');
let key = parts.next().unwrap_or("");
let value = parts.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid kernel_verity_params entry: {field}"),
)
})?;
if key.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid kernel_verity_params entry: {field}"),
));
}
values.insert(key.to_string(), value.to_string());
}
let root_hash = values
.get("root_hash")
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"Missing kernel_verity_params root_hash",
)
})?
.to_string();
let salt = values.get("salt").cloned().unwrap_or_default();
let parse_uint_field = |name: &str| -> Result<u64> {
match values.get(name) {
Some(value) if !value.is_empty() => value.parse::<u64>().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid kernel_verity_params {} '{}': {}", name, value, e),
)
}),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Missing kernel_verity_params {name}"),
)),
}
};
let data_blocks = parse_uint_field("data_blocks")?;
let data_block_size = parse_uint_field("data_block_size")?;
let hash_block_size = parse_uint_field("hash_block_size")?;
if salt.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Missing kernel_verity_params salt",
));
}
if data_blocks == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid kernel_verity_params data_blocks: must be non-zero",
));
}
if data_block_size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid kernel_verity_params data_block_size: must be non-zero",
));
}
if hash_block_size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid kernel_verity_params hash_block_size: must be non-zero",
));
}
if data_block_size % VERITY_BLOCK_SIZE_BYTES != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid kernel_verity_params data_block_size: must be multiple of {}",
VERITY_BLOCK_SIZE_BYTES
),
));
}
if hash_block_size % VERITY_BLOCK_SIZE_BYTES != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid kernel_verity_params hash_block_size: must be multiple of {}",
VERITY_BLOCK_SIZE_BYTES
),
));
}
Ok(Some(KernelVerityParams {
root_hash,
salt,
data_blocks,
data_block_size,
hash_block_size,
}))
}
lazy_static! {
static ref HYPERVISOR_PLUGINS: Mutex<HashMap<String, Arc<dyn ConfigPlugin>>> =
@@ -294,6 +422,10 @@ pub struct BootInfo {
#[serde(default)]
pub kernel_params: String,
/// Guest kernel dm-verity parameters.
#[serde(default)]
pub kernel_verity_params: String,
/// Path to initrd file on host.
#[serde(default)]
pub initrd: String,
@@ -441,6 +573,17 @@ impl BootInfo {
self.kernel_params = all_params.join(KERNEL_PARAM_DELIMITER);
}
/// Replace kernel dm-verity parameters after validation.
pub fn replace_kernel_verity_params(&mut self, new_params: &str) -> Result<()> {
if new_params.trim().is_empty() {
return Ok(());
}
parse_kernel_verity_params(new_params)?;
self.kernel_verity_params = new_params.to_string();
Ok(())
}
/// Validate guest kernel image annotation.
pub fn validate_boot_path(&self, path: &str) -> Result<()> {
validate_path!(path, "path {} is invalid{}")?;

View File

@@ -148,8 +148,7 @@ ifneq (,$(QEMUCMD))
endif
endif
ROOTMEASURECONFIG ?= ""
KERNELTDXPARAMS += $(ROOTMEASURECONFIG)
KERNELVERITYPARAMS ?= ""
# TDX
DEFSHAREDFS_QEMU_TDX_VIRTIOFS := none
@@ -176,8 +175,8 @@ DEFMEMSLOTS := 10
DEFMAXMEMSZ := 0
##VAR DEFBRIDGES=<number> Default number of bridges
DEFBRIDGES := 1
DEFENABLEANNOTATIONS := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\", \"default_vcpus\", \"default_memory\"]
DEFENABLEANNOTATIONS_COCO := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\", \"default_vcpus\", \"default_memory\", \"cc_init_data\"]
DEFENABLEANNOTATIONS := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\", \"kernel_verity_params\", \"default_vcpus\", \"default_memory\"]
DEFENABLEANNOTATIONS_COCO := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\", \"kernel_verity_params\", \"default_vcpus\", \"default_memory\", \"cc_init_data\"]
DEFDISABLEGUESTSECCOMP := true
DEFDISABLEGUESTEMPTYDIR := false
##VAR DEFAULTEXPFEATURES=[features] Default experimental features enabled
@@ -527,6 +526,7 @@ USER_VARS += MACHINEACCELERATORS
USER_VARS += CPUFEATURES
USER_VARS += DEFMACHINETYPE_CLH
USER_VARS += KERNELPARAMS
USER_VARS += KERNELVERITYPARAMS
USER_VARS += KERNELPARAMS_DB
USER_VARS += KERNELPARAMS_FC
USER_VARS += LIBEXECDIR

View File

@@ -74,6 +74,11 @@ valid_hypervisor_paths = @QEMUVALIDHYPERVISORPATHS@
# container and look for 'default-kernel-parameters' log entries.
kernel_params = "@KERNELTDXPARAMS@"
# Optional dm-verity parameters (comma-separated key=value list):
# root_hash=...,salt=...,data_blocks=...,data_block_size=...,hash_block_size=...
# These are used by the runtime to assemble dm-verity kernel params.
kernel_verity_params = "@KERNELVERITYPARAMS@"
# Path to the firmware.
# If you want that qemu uses the default firmware leave this option empty
firmware = "@FIRMWARETDXPATH@"

View File

@@ -151,7 +151,11 @@ impl CloudHypervisorInner {
#[cfg(target_arch = "aarch64")]
let console_param_debug = KernelParams::from_string("console=ttyAMA0,115200n8");
let mut rootfs_param = KernelParams::new_rootfs_kernel_params(rootfs_driver, rootfs_type)?;
let mut rootfs_params = KernelParams::new_rootfs_kernel_params(
&cfg.boot_info.kernel_verity_params,
rootfs_driver,
rootfs_type,
)?;
let mut console_params = if enable_debug {
if confidential_guest {
@@ -165,8 +169,7 @@ impl CloudHypervisorInner {
params.append(&mut console_params);
// Add the rootfs device
params.append(&mut rootfs_param);
params.append(&mut rootfs_params);
// Now add some additional options required for CH
let extra_options = [

View File

@@ -144,13 +144,14 @@ impl DragonballInner {
let mut kernel_params = KernelParams::new(self.config.debug_info.enable_debug);
if self.config.boot_info.initrd.is_empty() {
// get rootfs driver
// When booting from the image, add rootfs and verity parameters here.
let rootfs_driver = self.config.blockdev_info.block_device_driver.clone();
kernel_params.append(&mut KernelParams::new_rootfs_kernel_params(
let mut rootfs_params = KernelParams::new_rootfs_kernel_params(
&self.config.boot_info.kernel_verity_params,
&rootfs_driver,
&self.config.boot_info.rootfs_type,
)?);
)?;
kernel_params.append(&mut rootfs_params);
}
kernel_params.append(&mut KernelParams::from_string(

View File

@@ -86,12 +86,12 @@ impl FcInner {
let mut kernel_params = KernelParams::new(self.config.debug_info.enable_debug);
kernel_params.push(Param::new("pci", "off"));
kernel_params.push(Param::new("iommu", "off"));
let rootfs_driver = self.config.blockdev_info.block_device_driver.clone();
kernel_params.append(&mut KernelParams::new_rootfs_kernel_params(
&rootfs_driver,
let mut rootfs_params = KernelParams::new_rootfs_kernel_params(
&self.config.boot_info.kernel_verity_params,
&self.config.blockdev_info.block_device_driver,
&self.config.boot_info.rootfs_type,
)?);
)?;
kernel_params.append(&mut rootfs_params);
kernel_params.append(&mut KernelParams::from_string(
&self.config.boot_info.kernel_params,
));

View File

@@ -11,6 +11,7 @@ use crate::{
VM_ROOTFS_ROOT_BLK, VM_ROOTFS_ROOT_PMEM,
};
use kata_types::config::LOG_VPORT_OPTION;
use kata_types::config::hypervisor::{parse_kernel_verity_params, VERITY_BLOCK_SIZE_BYTES};
use kata_types::fs::{
VM_ROOTFS_FILESYSTEM_EROFS, VM_ROOTFS_FILESYSTEM_EXT4, VM_ROOTFS_FILESYSTEM_XFS,
};
@@ -20,7 +21,76 @@ use kata_types::fs::{
const VSOCK_LOGS_PORT: &str = "1025";
const KERNEL_KV_DELIMITER: &str = "=";
const KERNEL_PARAM_DELIMITER: &str = " ";
const KERNEL_PARAM_DELIMITER: char = ' ';
// Split kernel params on spaces, but keep quoted substrings intact.
// Example: dm-mod.create="dm-verity,,,ro,0 736328 verity 1 /dev/vda1 /dev/vda2 ...".
fn split_kernel_params(params_string: &str) -> Vec<String> {
let mut params = Vec::new();
let mut current = String::new();
let mut in_quote = false;
for c in params_string.chars() {
if c == '"' {
in_quote = !in_quote;
current.push(c);
continue;
}
if c == KERNEL_PARAM_DELIMITER && !in_quote {
let trimmed = current.trim();
if !trimmed.is_empty() {
params.push(trimmed.to_string());
}
current.clear();
continue;
}
current.push(c);
}
let trimmed = current.trim();
if !trimmed.is_empty() {
params.push(trimmed.to_string());
}
params
}
struct KernelVerityConfig {
root_hash: String,
salt: String,
data_blocks: u64,
data_block_size: u64,
hash_block_size: u64,
}
fn new_kernel_verity_params(params_string: &str) -> Result<Option<KernelVerityConfig>> {
let cfg = parse_kernel_verity_params(params_string)
.map_err(|err| anyhow!(err.to_string()))?;
Ok(cfg.map(|params| KernelVerityConfig {
root_hash: params.root_hash,
salt: params.salt,
data_blocks: params.data_blocks,
data_block_size: params.data_block_size,
hash_block_size: params.hash_block_size,
}))
}
fn kernel_verity_root_flags(rootfs_type: &str) -> Result<String> {
let normalized = if rootfs_type.is_empty() {
VM_ROOTFS_FILESYSTEM_EXT4
} else {
rootfs_type
};
match normalized {
VM_ROOTFS_FILESYSTEM_EXT4 => Ok("data=ordered,errors=remount-ro ro".to_string()),
VM_ROOTFS_FILESYSTEM_XFS | VM_ROOTFS_FILESYSTEM_EROFS => Ok("ro".to_string()),
_ => Err(anyhow!("Unsupported rootfs type {}", rootfs_type)),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
@@ -71,7 +141,11 @@ impl KernelParams {
Self { params }
}
pub(crate) fn new_rootfs_kernel_params(rootfs_driver: &str, rootfs_type: &str) -> Result<Self> {
pub(crate) fn new_rootfs_kernel_params(
kernel_verity_params: &str,
rootfs_driver: &str,
rootfs_type: &str,
) -> Result<Self> {
let mut params = vec![];
match rootfs_driver {
@@ -119,7 +193,52 @@ impl KernelParams {
params.push(Param::new("rootfstype", rootfs_type));
Ok(Self { params })
let mut params = Self { params };
let cfg = match new_kernel_verity_params(kernel_verity_params)? {
Some(cfg) => cfg,
None => return Ok(params),
};
let (root_device, hash_device) = match rootfs_driver {
VM_ROOTFS_DRIVER_PMEM => ("/dev/pmem0p1", "/dev/pmem0p2"),
VM_ROOTFS_DRIVER_BLK | VM_ROOTFS_DRIVER_BLK_CCW | VM_ROOTFS_DRIVER_MMIO => {
("/dev/vda1", "/dev/vda2")
}
_ => return Err(anyhow!("Unsupported rootfs driver {}", rootfs_driver)),
};
let data_sectors = (cfg.data_block_size / VERITY_BLOCK_SIZE_BYTES) * cfg.data_blocks;
let root_flags = kernel_verity_root_flags(rootfs_type)?;
let dm_cmd = format!(
"dm-verity,,,ro,0 {} verity 1 {} {} {} {} {} 0 sha256 {} {}",
data_sectors,
root_device,
hash_device,
cfg.data_block_size,
cfg.hash_block_size,
cfg.data_blocks,
cfg.root_hash,
cfg.salt
);
params.remove_all_by_key("root".to_string());
params.remove_all_by_key("rootflags".to_string());
params.remove_all_by_key("rootfstype".to_string());
params.push(Param {
key: "dm-mod.create".to_string(),
value: format!("\"{}\"", dm_cmd),
});
params.push(Param::new("root", "/dev/dm-0"));
params.push(Param::new("rootflags", &root_flags));
if rootfs_type.is_empty() {
params.push(Param::new("rootfstype", VM_ROOTFS_FILESYSTEM_EXT4));
} else {
params.push(Param::new("rootfstype", rootfs_type));
}
Ok(params)
}
pub(crate) fn append(&mut self, params: &mut KernelParams) {
@@ -138,7 +257,7 @@ impl KernelParams {
pub(crate) fn from_string(params_string: &str) -> Self {
let mut params = vec![];
let parameters_vec: Vec<&str> = params_string.split(KERNEL_PARAM_DELIMITER).collect();
let parameters_vec = split_kernel_params(params_string);
for param in parameters_vec.iter() {
if param.is_empty() {
@@ -170,7 +289,7 @@ impl KernelParams {
parameters.push(param.to_string()?);
}
Ok(parameters.join(KERNEL_PARAM_DELIMITER))
Ok(parameters.join(&KERNEL_PARAM_DELIMITER.to_string()))
}
}
@@ -347,7 +466,8 @@ mod tests {
for (i, t) in tests.iter().enumerate() {
let msg = format!("test[{i}]: {t:?}");
let result = KernelParams::new_rootfs_kernel_params(t.rootfs_driver, t.rootfs_type);
let result =
KernelParams::new_rootfs_kernel_params("", t.rootfs_driver, t.rootfs_type);
let msg = format!("{msg}, result: {result:?}");
if t.result.is_ok() {
assert!(result.is_ok(), "{}", msg);
@@ -359,4 +479,55 @@ mod tests {
}
}
}
#[test]
fn test_kernel_verity_params() -> Result<()> {
let params = KernelParams::new_rootfs_kernel_params(
"root_hash=abc,salt=def,data_blocks=1,data_block_size=4096,hash_block_size=4096",
VM_ROOTFS_DRIVER_BLK,
VM_ROOTFS_FILESYSTEM_EXT4,
)?;
let params_string = params.to_string()?;
assert!(params_string.contains("dm-mod.create="));
assert!(params_string.contains("root=/dev/dm-0"));
assert!(params_string.contains("rootfstype=ext4"));
let err = KernelParams::new_rootfs_kernel_params(
"root_hash=abc,data_blocks=1,data_block_size=4096,hash_block_size=4096",
VM_ROOTFS_DRIVER_BLK,
VM_ROOTFS_FILESYSTEM_EXT4,
)
.err()
.expect("expected missing salt error");
assert!(format!("{err}").contains("Missing kernel_verity_params salt"));
let err = KernelParams::new_rootfs_kernel_params(
"root_hash=abc,salt=def,data_block_size=4096,hash_block_size=4096",
VM_ROOTFS_DRIVER_BLK,
VM_ROOTFS_FILESYSTEM_EXT4,
)
.err()
.expect("expected missing data_blocks error");
assert!(format!("{err}").contains("Missing kernel_verity_params data_blocks"));
let err = KernelParams::new_rootfs_kernel_params(
"root_hash=abc,salt=def,data_blocks=foo,data_block_size=4096,hash_block_size=4096",
VM_ROOTFS_DRIVER_BLK,
VM_ROOTFS_FILESYSTEM_EXT4,
)
.err()
.expect("expected invalid data_blocks error");
assert!(format!("{err}").contains("Invalid kernel_verity_params data_blocks"));
let err = KernelParams::new_rootfs_kernel_params(
"root_hash=abc,salt=def,data_blocks=1,data_block_size=4096,hash_block_size=4096,badfield",
VM_ROOTFS_DRIVER_BLK,
VM_ROOTFS_FILESYSTEM_EXT4,
)
.err()
.expect("expected invalid entry error");
assert!(format!("{err}").contains("Invalid kernel_verity_params entry"));
Ok(())
}
}

View File

@@ -179,16 +179,13 @@ impl Kernel {
let mut kernel_params = KernelParams::new(config.debug_info.enable_debug);
if config.boot_info.initrd.is_empty() {
// QemuConfig::validate() has already made sure that if initrd is
// empty, image cannot be so we don't need to re-check that here
kernel_params.append(
&mut KernelParams::new_rootfs_kernel_params(
&config.boot_info.vm_rootfs_driver,
&config.boot_info.rootfs_type,
)
.context("adding rootfs params failed")?,
);
let mut rootfs_params = KernelParams::new_rootfs_kernel_params(
&config.boot_info.kernel_verity_params,
&config.boot_info.vm_rootfs_driver,
&config.boot_info.rootfs_type,
)
.context("adding rootfs/verity params failed")?;
kernel_params.append(&mut rootfs_params);
}
kernel_params.append(&mut KernelParams::from_string(

View File

@@ -152,9 +152,9 @@ FIRMWARETDVFVOLUMEPATH :=
FIRMWARESNPPATH := $(PREFIXDEPS)/share/ovmf/AMDSEV.fd
ROOTMEASURECONFIG ?= ""
KERNELTDXPARAMS += $(ROOTMEASURECONFIG)
KERNELQEMUCOCODEVPARAMS += $(ROOTMEASURECONFIG)
KERNELVERITYPARAMS ?= ""
KERNELVERITYPARAMS_NV ?= ""
KERNELVERITYPARAMS_CONFIDENTIAL_NV ?= ""
# Name of default configuration file the runtime will use.
CONFIG_FILE = configuration.toml
@@ -217,8 +217,8 @@ DEFMEMSLOTS := 10
DEFMAXMEMSZ := 0
#Default number of bridges
DEFBRIDGES := 1
DEFENABLEANNOTATIONS := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\"]
DEFENABLEANNOTATIONS_COCO := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\", \"default_vcpus\", \"default_memory\", \"cc_init_data\"]
DEFENABLEANNOTATIONS := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\", \"kernel_verity_params\"]
DEFENABLEANNOTATIONS_COCO := [\"enable_iommu\", \"virtio_fs_extra_args\", \"kernel_params\", \"kernel_verity_params\", \"default_vcpus\", \"default_memory\", \"cc_init_data\"]
DEFDISABLEGUESTSECCOMP := true
DEFDISABLEGUESTEMPTYDIR := false
#Default experimental features enabled
@@ -655,6 +655,8 @@ USER_VARS += DEFAULTMEMORY_NV
USER_VARS += DEFAULTVFIOPORT_NV
USER_VARS += DEFAULTPCIEROOTPORT_NV
USER_VARS += KERNELPARAMS_NV
USER_VARS += KERNELVERITYPARAMS_NV
USER_VARS += KERNELVERITYPARAMS_CONFIDENTIAL_NV
USER_VARS += DEFAULTTIMEOUT_NV
USER_VARS += DEFSANDBOXCGROUPONLY_NV
USER_VARS += DEFROOTFSTYPE
@@ -681,6 +683,7 @@ USER_VARS += TDXCPUFEATURES
USER_VARS += DEFMACHINETYPE_CLH
USER_VARS += DEFMACHINETYPE_STRATOVIRT
USER_VARS += KERNELPARAMS
USER_VARS += KERNELVERITYPARAMS
USER_VARS += KERNELTDXPARAMS
USER_VARS += KERNELQEMUCOCODEVPARAMS
USER_VARS += LIBEXECDIR

View File

@@ -52,6 +52,11 @@ valid_hypervisor_paths = @QEMUVALIDHYPERVISORPATHS@
# container and look for 'default-kernel-parameters' log entries.
kernel_params = "@KERNELQEMUCOCODEVPARAMS@"
# Optional dm-verity parameters (comma-separated key=value list):
# root_hash=...,salt=...,data_blocks=...,data_block_size=...,hash_block_size=...
# These are used by the runtime to assemble dm-verity kernel params.
kernel_verity_params = "@KERNELVERITYPARAMS@"
# Path to the firmware.
# If you want that qemu uses the default firmware leave this option empty
firmware = "@FIRMWAREPATH@"

View File

@@ -15,7 +15,7 @@
[hypervisor.qemu]
path = "@QEMUSNPPATH@"
kernel = "@KERNELPATH_CONFIDENTIAL_NV@"
initrd = "@INITRDPATH_CONFIDENTIAL_NV@"
image = "@IMAGEPATH_CONFIDENTIAL_NV@"
machine_type = "@MACHINETYPE@"
@@ -92,6 +92,11 @@ snp_guest_policy = 196608
# container and look for 'default-kernel-parameters' log entries.
kernel_params = "@KERNELPARAMS_NV@"
# Optional dm-verity parameters (comma-separated key=value list):
# root_hash=...,salt=...,data_blocks=...,data_block_size=...,hash_block_size=...
# These are used by the runtime to assemble dm-verity kernel params.
kernel_verity_params = "@KERNELVERITYPARAMS_CONFIDENTIAL_NV@"
# Path to the firmware.
# If you want that qemu uses the default firmware leave this option empty
firmware = "@FIRMWARESNPPATH@"

View File

@@ -14,7 +14,7 @@
[hypervisor.qemu]
path = "@QEMUTDXEXPERIMENTALPATH@"
kernel = "@KERNELPATH_CONFIDENTIAL_NV@"
initrd = "@INITRDPATH_CONFIDENTIAL_NV@"
image = "@IMAGEPATH_CONFIDENTIAL_NV@"
machine_type = "@MACHINETYPE@"
tdx_quote_generation_service_socket_port = @QEMUTDXQUOTEGENERATIONSERVICESOCKETPORT@
@@ -69,6 +69,11 @@ valid_hypervisor_paths = @QEMUTDXEXPERIMENTALVALIDHYPERVISORPATHS@
# container and look for 'default-kernel-parameters' log entries.
kernel_params = "@KERNELPARAMS_NV@"
# Optional dm-verity parameters (comma-separated key=value list):
# root_hash=...,salt=...,data_blocks=...,data_block_size=...,hash_block_size=...
# These are used by the runtime to assemble dm-verity kernel params.
kernel_verity_params = "@KERNELVERITYPARAMS_CONFIDENTIAL_NV@"
# Path to the firmware.
# If you want that qemu uses the default firmware leave this option empty
firmware = "@FIRMWARETDVFPATH@"

View File

@@ -14,7 +14,7 @@
[hypervisor.qemu]
path = "@QEMUPATH@"
kernel = "@KERNELPATH_NV@"
initrd = "@INITRDPATH_NV@"
image = "@IMAGEPATH_NV@"
machine_type = "@MACHINETYPE@"
# rootfs filesystem type:
@@ -51,6 +51,11 @@ valid_hypervisor_paths = @QEMUVALIDHYPERVISORPATHS@
# container and look for 'default-kernel-parameters' log entries.
kernel_params = "@KERNELPARAMS_NV@"
# Optional dm-verity parameters (comma-separated key=value list):
# root_hash=...,salt=...,data_blocks=...,data_block_size=...,hash_block_size=...
# These are used by the runtime to assemble dm-verity kernel params.
kernel_verity_params = "@KERNELVERITYPARAMS_NV@"
# Path to the firmware.
# If you want that qemu uses the default firmware leave this option empty
firmware = "@FIRMWAREPATH@"

View File

@@ -68,6 +68,11 @@ valid_hypervisor_paths = @QEMUVALIDHYPERVISORPATHS@
# container and look for 'default-kernel-parameters' log entries.
kernel_params = "@KERNELTDXPARAMS@"
# Optional dm-verity parameters (comma-separated key=value list):
# root_hash=...,salt=...,data_blocks=...,data_block_size=...,hash_block_size=...
# These are used by the runtime to assemble dm-verity kernel params.
kernel_verity_params = "@KERNELVERITYPARAMS@"
# Path to the firmware.
# If you want that qemu uses the default firmware leave this option empty
firmware = "@FIRMWARETDVFPATH@"

View File

@@ -93,6 +93,7 @@ type hypervisor struct {
MachineAccelerators string `toml:"machine_accelerators"`
CPUFeatures string `toml:"cpu_features"`
KernelParams string `toml:"kernel_params"`
KernelVerityParams string `toml:"kernel_verity_params"`
MachineType string `toml:"machine_type"`
QgsPort uint32 `toml:"tdx_quote_generation_service_socket_port"`
BlockDeviceDriver string `toml:"block_device_driver"`
@@ -387,6 +388,10 @@ func (h hypervisor) kernelParams() string {
return h.KernelParams
}
func (h hypervisor) kernelVerityParams() string {
return h.KernelVerityParams
}
func (h hypervisor) machineType() string {
if h.MachineType == "" {
return defaultMachineType
@@ -814,6 +819,7 @@ func newFirecrackerHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
RootfsType: rootfsType,
FirmwarePath: firmware,
KernelParams: vc.DeserializeParams(vc.KernelParamFields(kernelParams)),
KernelVerityParams: h.kernelVerityParams(),
NumVCPUsF: h.defaultVCPUs(),
DefaultMaxVCPUs: h.defaultMaxVCPUs(),
MemorySize: h.defaultMemSz(),
@@ -948,6 +954,7 @@ func newQemuHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
MachineAccelerators: machineAccelerators,
CPUFeatures: cpuFeatures,
KernelParams: vc.DeserializeParams(vc.KernelParamFields(kernelParams)),
KernelVerityParams: h.kernelVerityParams(),
HypervisorMachineType: machineType,
QgsPort: h.qgsPort(),
NumVCPUsF: h.defaultVCPUs(),
@@ -1088,6 +1095,7 @@ func newClhHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
FirmwarePath: firmware,
MachineAccelerators: machineAccelerators,
KernelParams: vc.DeserializeParams(vc.KernelParamFields(kernelParams)),
KernelVerityParams: h.kernelVerityParams(),
HypervisorMachineType: machineType,
NumVCPUsF: h.defaultVCPUs(),
DefaultMaxVCPUs: h.defaultMaxVCPUs(),
@@ -1165,16 +1173,17 @@ func newDragonballHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
kernelParams := h.kernelParams()
return vc.HypervisorConfig{
KernelPath: kernel,
ImagePath: image,
RootfsType: rootfsType,
KernelParams: vc.DeserializeParams(vc.KernelParamFields(kernelParams)),
NumVCPUsF: h.defaultVCPUs(),
DefaultMaxVCPUs: h.defaultMaxVCPUs(),
MemorySize: h.defaultMemSz(),
MemSlots: h.defaultMemSlots(),
EntropySource: h.GetEntropySource(),
Debug: h.Debug,
KernelPath: kernel,
ImagePath: image,
RootfsType: rootfsType,
KernelParams: vc.DeserializeParams(vc.KernelParamFields(kernelParams)),
KernelVerityParams: h.kernelVerityParams(),
NumVCPUsF: h.defaultVCPUs(),
DefaultMaxVCPUs: h.defaultMaxVCPUs(),
MemorySize: h.defaultMemSz(),
MemSlots: h.defaultMemSlots(),
EntropySource: h.GetEntropySource(),
Debug: h.Debug,
}, nil
}
@@ -1249,6 +1258,7 @@ func newStratovirtHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
ImagePath: image,
RootfsType: rootfsType,
KernelParams: vc.DeserializeParams(strings.Fields(kernelParams)),
KernelVerityParams: h.kernelVerityParams(),
HypervisorMachineType: machineType,
NumVCPUsF: h.defaultVCPUs(),
DefaultMaxVCPUs: h.defaultMaxVCPUs(),

View File

@@ -636,6 +636,15 @@ func addHypervisorPathOverrides(ocispec specs.Spec, config *vc.SandboxConfig, ru
}
}
if value, ok := ocispec.Annotations[vcAnnotations.KernelVerityParams]; ok {
if value != "" {
if _, err := vc.ParseKernelVerityParams(value); err != nil {
return fmt.Errorf("invalid kernel_verity_params in annotation: %w", err)
}
config.HypervisorConfig.KernelVerityParams = value
}
}
return nil
}

View File

@@ -466,8 +466,8 @@ func (clh *cloudHypervisor) enableProtection() error {
}
}
func getNonUserDefinedKernelParams(rootfstype string, disableNvdimm bool, dax bool, debug bool, confidential bool, iommu bool) ([]Param, error) {
params, err := GetKernelRootParams(rootfstype, disableNvdimm, dax)
func getNonUserDefinedKernelParams(rootfstype string, disableNvdimm bool, dax bool, debug bool, confidential bool, iommu bool, kernelVerityParams string) ([]Param, error) {
params, err := GetKernelRootParams(rootfstype, disableNvdimm, dax, kernelVerityParams)
if err != nil {
return []Param{}, err
}
@@ -587,7 +587,7 @@ func (clh *cloudHypervisor) CreateVM(ctx context.Context, id string, network Net
disableNvdimm := (clh.config.DisableImageNvdimm || clh.config.ConfidentialGuest)
enableDax := !disableNvdimm
params, err := getNonUserDefinedKernelParams(hypervisorConfig.RootfsType, disableNvdimm, enableDax, clh.config.Debug, clh.config.ConfidentialGuest, clh.config.IOMMU)
params, err := getNonUserDefinedKernelParams(hypervisorConfig.RootfsType, disableNvdimm, enableDax, clh.config.Debug, clh.config.ConfidentialGuest, clh.config.IOMMU, hypervisorConfig.KernelVerityParams)
if err != nil {
return err
}

View File

@@ -699,7 +699,12 @@ func (fc *firecracker) fcInitConfiguration(ctx context.Context) error {
return err
}
params, err := GetKernelRootParams(fc.config.RootfsType, true, false)
params, err := GetKernelRootParams(
fc.config.RootfsType,
true,
false,
fc.config.KernelVerityParams,
)
if err != nil {
return err
}

View File

@@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/pkg/errors"
@@ -126,18 +127,56 @@ const (
EROFS RootfsType = "erofs"
)
func GetKernelRootParams(rootfstype string, disableNvdimm bool, dax bool) ([]Param, error) {
var kernelRootParams []Param
func GetKernelRootParams(rootfstype string, disableNvdimm bool, dax bool, kernelVerityParams string) ([]Param, error) {
cfg, err := ParseKernelVerityParams(kernelVerityParams)
if err != nil {
return []Param{}, err
}
// EXT4 filesystem is used by default.
if rootfstype == "" {
rootfstype = string(EXT4)
}
if cfg != nil {
rootDevice := "/dev/pmem0p1"
hashDevice := "/dev/pmem0p2"
if disableNvdimm {
rootDevice = "/dev/vda1"
hashDevice = "/dev/vda2"
}
dataSectors := (cfg.dataBlockSize / 512) * cfg.dataBlocks
verityCmd := fmt.Sprintf(
"dm-verity,,,ro,0 %d verity 1 %s %s %d %d %d 0 sha256 %s %s",
dataSectors,
rootDevice,
hashDevice,
cfg.dataBlockSize,
cfg.hashBlockSize,
cfg.dataBlocks,
cfg.rootHash,
cfg.salt,
)
rootFlags, err := kernelVerityRootFlags(rootfstype)
if err != nil {
return []Param{}, err
}
return []Param{
{Key: "dm-mod.create", Value: fmt.Sprintf("\"%s\"", verityCmd)},
{Key: "root", Value: "/dev/dm-0"},
{Key: "rootflags", Value: rootFlags},
{Key: "rootfstype", Value: rootfstype},
}, nil
}
if disableNvdimm && dax {
return []Param{}, fmt.Errorf("Virtio-Blk does not support DAX")
}
kernelRootParams := []Param{}
if disableNvdimm {
// Virtio-Blk
kernelRootParams = append(kernelRootParams, Param{"root", string(VirtioBlk)})
@@ -171,10 +210,116 @@ func GetKernelRootParams(rootfstype string, disableNvdimm bool, dax bool) ([]Par
}
kernelRootParams = append(kernelRootParams, Param{"rootfstype", rootfstype})
return kernelRootParams, nil
}
const (
verityBlockSizeBytes = 512
)
type kernelVerityConfig struct {
rootHash string
salt string
dataBlocks uint64
dataBlockSize uint64
hashBlockSize uint64
}
func ParseKernelVerityParams(params string) (*kernelVerityConfig, error) {
if strings.TrimSpace(params) == "" {
return nil, nil
}
values := map[string]string{}
for _, field := range strings.Split(params, ",") {
field = strings.TrimSpace(field)
if field == "" {
continue
}
parts := strings.SplitN(field, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid kernel_verity_params entry: %q", field)
}
values[parts[0]] = parts[1]
}
cfg := &kernelVerityConfig{
rootHash: values["root_hash"],
salt: values["salt"],
}
if cfg.rootHash == "" {
return nil, fmt.Errorf("missing kernel_verity_params root_hash")
}
parseUintField := func(name string) (uint64, error) {
value, ok := values[name]
if !ok || value == "" {
return 0, fmt.Errorf("missing kernel_verity_params %s", name)
}
parsed, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid kernel_verity_params %s %q: %w", name, value, err)
}
return parsed, nil
}
dataBlocks, err := parseUintField("data_blocks")
if err != nil {
return nil, err
}
dataBlockSize, err := parseUintField("data_block_size")
if err != nil {
return nil, err
}
hashBlockSize, err := parseUintField("hash_block_size")
if err != nil {
return nil, err
}
if cfg.salt == "" {
return nil, fmt.Errorf("missing kernel_verity_params salt")
}
if dataBlocks == 0 {
return nil, fmt.Errorf("invalid kernel_verity_params data_blocks: must be non-zero")
}
if dataBlockSize == 0 {
return nil, fmt.Errorf("invalid kernel_verity_params data_block_size: must be non-zero")
}
if hashBlockSize == 0 {
return nil, fmt.Errorf("invalid kernel_verity_params hash_block_size: must be non-zero")
}
if dataBlockSize%verityBlockSizeBytes != 0 {
return nil, fmt.Errorf("invalid kernel_verity_params data_block_size: must be multiple of %d", verityBlockSizeBytes)
}
if hashBlockSize%verityBlockSizeBytes != 0 {
return nil, fmt.Errorf("invalid kernel_verity_params hash_block_size: must be multiple of %d", verityBlockSizeBytes)
}
cfg.dataBlocks = dataBlocks
cfg.dataBlockSize = dataBlockSize
cfg.hashBlockSize = hashBlockSize
return cfg, nil
}
func kernelVerityRootFlags(rootfstype string) (string, error) {
// EXT4 filesystem is used by default.
if rootfstype == "" {
rootfstype = string(EXT4)
}
switch RootfsType(rootfstype) {
case EROFS:
return "ro", nil
case XFS:
return "ro", nil
case EXT4:
return "data=ordered,errors=remount-ro ro", nil
default:
return "", fmt.Errorf("unsupported rootfs type")
}
}
// DeviceType describes a virtualized device type.
type DeviceType int
@@ -483,6 +628,9 @@ type HypervisorConfig struct {
// KernelParams are additional guest kernel parameters.
KernelParams []Param
// KernelVerityParams are additional guest dm-verity parameters.
KernelVerityParams string
// HypervisorParams are additional hypervisor parameters.
HypervisorParams []Param

View File

@@ -22,6 +22,7 @@ func TestGetKernelRootParams(t *testing.T) {
expected []Param
disableNvdimm bool
dax bool
verityParams string
error bool
}{
// EXT4
@@ -34,6 +35,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: false,
dax: false,
verityParams: "",
error: false,
},
{
@@ -45,6 +47,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: false,
dax: true,
verityParams: "",
error: false,
},
{
@@ -56,6 +59,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: true,
dax: false,
verityParams: "",
error: false,
},
@@ -69,6 +73,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: false,
dax: false,
verityParams: "",
error: false,
},
{
@@ -80,6 +85,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: false,
dax: true,
verityParams: "",
error: false,
},
{
@@ -91,6 +97,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: true,
dax: false,
verityParams: "",
error: false,
},
@@ -104,6 +111,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: false,
dax: false,
verityParams: "",
error: false,
},
{
@@ -115,6 +123,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: false,
dax: true,
verityParams: "",
error: false,
},
{
@@ -126,6 +135,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: true,
dax: false,
verityParams: "",
error: false,
},
@@ -139,6 +149,7 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: false,
dax: false,
verityParams: "",
error: true,
},
@@ -152,12 +163,61 @@ func TestGetKernelRootParams(t *testing.T) {
},
disableNvdimm: true,
dax: true,
verityParams: "",
error: true,
},
{
rootfstype: string(EXT4),
expected: []Param{
{
Key: "dm-mod.create",
Value: "\"dm-verity,,,ro,0 8 verity 1 /dev/vda1 /dev/vda2 4096 4096 1 0 sha256 abc def\"",
},
{Key: "root", Value: "/dev/dm-0"},
{Key: "rootflags", Value: "data=ordered,errors=remount-ro ro"},
{Key: "rootfstype", Value: string(EXT4)},
},
disableNvdimm: true,
dax: false,
verityParams: "root_hash=abc,salt=def,data_blocks=1,data_block_size=4096,hash_block_size=4096",
error: false,
},
{
rootfstype: string(EXT4),
expected: []Param{},
disableNvdimm: false,
dax: false,
verityParams: "root_hash=abc,data_blocks=1,data_block_size=4096,hash_block_size=4096",
error: true,
},
{
rootfstype: string(EXT4),
expected: []Param{},
disableNvdimm: false,
dax: false,
verityParams: "root_hash=abc,salt=def,data_block_size=4096,hash_block_size=4096",
error: true,
},
{
rootfstype: string(EXT4),
expected: []Param{},
disableNvdimm: false,
dax: false,
verityParams: "root_hash=abc,salt=def,data_blocks=foo,data_block_size=4096,hash_block_size=4096",
error: true,
},
{
rootfstype: string(EXT4),
expected: []Param{},
disableNvdimm: false,
dax: false,
verityParams: "root_hash=abc,salt=def,data_blocks=1,data_block_size=4096,hash_block_size=4096,badfield",
error: true,
},
}
for _, t := range tests {
kernelRootParams, err := GetKernelRootParams(t.rootfstype, t.disableNvdimm, t.dax)
kernelRootParams, err := GetKernelRootParams(t.rootfstype, t.disableNvdimm, t.dax, t.verityParams)
if t.error {
assert.Error(err)
continue

View File

@@ -84,6 +84,9 @@ const (
// KernelParams is a sandbox annotation for passing additional guest kernel parameters.
KernelParams = kataAnnotHypervisorPrefix + "kernel_params"
// KernelVerityParams is a sandbox annotation for passing guest dm-verity parameters.
KernelVerityParams = kataAnnotHypervisorPrefix + "kernel_verity_params"
// MachineType is a sandbox annotation to specify the type of machine being emulated by the hypervisor.
MachineType = kataAnnotHypervisorPrefix + "machine_type"

View File

@@ -773,7 +773,12 @@ func (q *qemuArchBase) setEndpointDevicePath(endpoint Endpoint, bridgeAddr int,
func (q *qemuArchBase) handleImagePath(config HypervisorConfig) error {
if config.ImagePath != "" {
kernelRootParams, err := GetKernelRootParams(config.RootfsType, q.disableNvdimm, false)
kernelRootParams, err := GetKernelRootParams(
config.RootfsType,
q.disableNvdimm,
false,
config.KernelVerityParams,
)
if err != nil {
return err
}
@@ -781,7 +786,12 @@ func (q *qemuArchBase) handleImagePath(config HypervisorConfig) error {
q.qemuMachine.Options = strings.Join([]string{
q.qemuMachine.Options, qemuNvdimmOption,
}, ",")
kernelRootParams, err = GetKernelRootParams(config.RootfsType, q.disableNvdimm, q.dax)
kernelRootParams, err = GetKernelRootParams(
config.RootfsType,
q.disableNvdimm,
q.dax,
config.KernelVerityParams,
)
if err != nil {
return err
}

View File

@@ -83,7 +83,12 @@ func newQemuArch(config HypervisorConfig) (qemuArch, error) {
}
if config.ImagePath != "" {
kernelParams, err := GetKernelRootParams(config.RootfsType, true, false)
kernelParams, err := GetKernelRootParams(
config.RootfsType,
true,
false,
config.KernelVerityParams,
)
if err != nil {
return nil, err
}

View File

@@ -337,7 +337,12 @@ func (s *stratovirt) getKernelParams(machineType string, initrdPath string) (str
var kernelParams []Param
if initrdPath == "" {
params, err := GetKernelRootParams(s.config.RootfsType, true, false)
params, err := GetKernelRootParams(
s.config.RootfsType,
true,
false,
s.config.KernelVerityParams,
)
if err != nil {
return "", err
}

View File

@@ -151,26 +151,30 @@ setup() {
[[ -n "$qemu_cmd" ]] || { echo "Could not find QEMU command line"; return 1; }
kernel_path=$(echo "$qemu_cmd" | grep -oP -- '-kernel \K[^ ]+')
initrd_path=$(echo "$qemu_cmd" | grep -oP -- '-initrd \K[^ ]+')
initrd_path=$(echo "$qemu_cmd" | grep -oP -- '-initrd \K[^ ]+' || true)
firmware_path=$(echo "$qemu_cmd" | grep -oP -- '-bios \K[^ ]+')
vcpu_count=$(echo "$qemu_cmd" | grep -oP -- '-smp \K\d+')
append=$(echo "$qemu_cmd" | sed -n 's/.*-append \(.*\) -bios.*/\1/p')
# Remove escape backslashes for quotes from output for dm-mod.create parameters
append="${append//\\\"/\"}"
launch_measurement=$(PATH="${PATH}:${HOME}/.local/bin" sev-snp-measure \
--mode=snp \
--vcpus="${vcpu_count}" \
--vcpu-type=EPYC-v4 \
--output-format=base64 \
--ovmf="${firmware_path}" \
--kernel="${kernel_path}" \
--initrd="${initrd_path}" \
--append="${append}" \
measure_args=(
--mode=snp
--vcpus="${vcpu_count}"
--vcpu-type=EPYC-v4
--output-format=base64
--ovmf="${firmware_path}"
--kernel="${kernel_path}"
--append="${append}"
)
if [[ -n "${initrd_path}" ]]; then
measure_args+=(--initrd="${initrd_path}")
fi
launch_measurement=$(PATH="${PATH}:${HOME}/.local/bin" sev-snp-measure "${measure_args[@]}")
# set launch measurement as reference value
# set launch measurement as reference value
kbs_config_command set-sample-reference-value snp_launch_measurement "${launch_measurement}"
# Get the reported firmware version(s) for this machine
firmware=$(sudo snphost show tcb | grep -A 5 "Reported TCB")
@@ -204,7 +208,6 @@ setup() {
kbs_config_command set-sample-reference-value snp_launch_measurement abcd
[ "$result" -eq 0 ]
}

View File

@@ -9,6 +9,11 @@ load "${BATS_TEST_DIRNAME}/../../common.bash"
load "${BATS_TEST_DIRNAME}/lib.sh"
load "${BATS_TEST_DIRNAME}/tests_common.sh"
# Currently only the Go runtime provides the config path used here.
# If a Rust hypervisor runs this test, mirror the enabling_hypervisor
# pattern in tests/common.bash to select the correct runtime-rs config.
shim_config_file="/opt/kata/share/defaults/kata-containers/configuration-${KATA_HYPERVISOR}.toml"
check_and_skip() {
case "${KATA_HYPERVISOR}" in
qemu-tdx|qemu-coco-dev)
@@ -29,26 +34,29 @@ setup() {
setup_common || die "setup_common failed"
}
@test "Test cannnot launch pod with measured boot enabled and incorrect hash" {
@test "Test cannot launch pod with measured boot enabled and incorrect hash" {
pod_config="$(new_pod_config nginx "kata-${KATA_HYPERVISOR}")"
auto_generate_policy "${pod_config_dir}" "${pod_config}"
incorrect_hash="1111111111111111111111111111111111111111111111111111111111111111"
# To avoid editing that file on the worker node, here it will be
# enabled via pod annotations.
# Read verity parameters from config, then override via annotations.
kernel_verity_params=$(exec_host "$node" "sed -n 's/^kernel_verity_params = \"\\(.*\\)\"/\\1/p' ${shim_config_file}" || true)
[ -n "${kernel_verity_params}" ] || die "Missing kernel_verity_params in ${shim_config_file}"
kernel_verity_params=$(printf '%s\n' "$kernel_verity_params" | sed -E "s/root_hash=[^,]*/root_hash=${incorrect_hash}/")
set_metadata_annotation "$pod_config" \
"io.katacontainers.config.hypervisor.kernel_params" \
"rootfs_verity.scheme=dm-verity rootfs_verity.hash=$incorrect_hash"
"io.katacontainers.config.hypervisor.kernel_verity_params" \
"${kernel_verity_params}"
# Run on a specific node so we know from where to inspect the logs
set_node "$pod_config" "$node"
# For debug sake
echo "Pod $pod_config file:"
cat $pod_config
kubectl apply -f $pod_config
waitForProcess "60" "3" "exec_host $node journalctl -t kata | grep \"verity: .* metadata block .* is corrupted\""
assert_pod_container_creating "$pod_config"
assert_logs_contain "$node" kata "${node_start_time}" "verity: .* metadata block .* is corrupted"
}
teardown() {

View File

@@ -179,7 +179,7 @@ assert_pod_fail() {
local container_config="$1"
local duration="${2:-120}"
echo "In assert_pod_fail: $container_config"
echo "In assert_pod_fail: ${container_config}"
echo "Attempt to create the container but it should fail"
retry_kubectl_apply "${container_config}"
@@ -192,13 +192,13 @@ assert_pod_fail() {
local sleep_time=5
while true; do
echo "Waiting for a container to fail"
sleep ${sleep_time}
sleep "${sleep_time}"
elapsed_time=$((elapsed_time+sleep_time))
if [[ $(kubectl get pod "${pod_name}" \
-o jsonpath='{.status.containerStatuses[0].state.waiting.reason}') = *BackOff* ]]; then
return 0
fi
if [ $elapsed_time -gt $duration ]; then
if [[ "${elapsed_time}" -gt "${duration}" ]]; then
echo "The container does not get into a failing state" >&2
break
fi
@@ -207,6 +207,46 @@ assert_pod_fail() {
}
# Create a pod then assert it remains in ContainerCreating.
#
# Parameters:
# $1 - the pod configuration file.
# $2 - the duration to wait (seconds). Defaults to 60. (optional)
#
assert_pod_container_creating() {
local container_config="$1"
local duration="${2:-60}"
echo "In assert_pod_container_creating: ${container_config}"
echo "Attempt to create the container but it should stay in creating state"
retry_kubectl_apply "${container_config}"
if ! pod_name=$(kubectl get pods -o jsonpath='{.items..metadata.name}'); then
echo "Failed to create the pod"
return 1
fi
local elapsed_time=0
local sleep_time=5
while true; do
sleep "${sleep_time}"
elapsed_time=$((elapsed_time+sleep_time))
reason=$(kubectl get pod "${pod_name}" -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}' 2>/dev/null || true)
phase=$(kubectl get pod "${pod_name}" -o jsonpath='{.status.phase}' 2>/dev/null || true)
if [[ "${phase}" != "Pending" ]]; then
echo "Expected pod to remain Pending, got phase: ${phase}" >&2
return 1
fi
if [[ -n "${reason}" && "${reason}" != "ContainerCreating" ]]; then
echo "Expected ContainerCreating, got: ${reason}" >&2
return 1
fi
if [[ "${elapsed_time}" -ge "${duration}" ]]; then
return 0
fi
done
}
# Check the pulled rootfs on host for given node and sandbox_id
#
# Parameters:
@@ -381,4 +421,3 @@ get_node_kata_sandbox_id() {
done
echo $kata_sandbox_id
}

View File

@@ -7,5 +7,6 @@ dracut/dracut.conf.d/15-extra-libs.conf
kata-centos-dnf.conf
kata-containers-initrd.img
kata-containers.img
root_hash*.txt
rootfs-builder/centos/RPM-GPG-KEY-*
typescript

View File

@@ -23,6 +23,7 @@ ARCH=${ARCH:-$(uname -m)}
[ "${TARGET_ARCH}" == "aarch64" ] && TARGET_ARCH=arm64
TARGET_OS=${TARGET_OS:-linux}
[ "${CROSS_BUILD}" == "true" ] && BUILDX=buildx && PLATFORM="--platform=${TARGET_OS}/${TARGET_ARCH}"
BUILD_VARIANT=${BUILD_VARIANT:-}
readonly script_name="${0##*/}"
readonly script_dir=$(dirname "$(readlink -f "$0")")
@@ -177,6 +178,7 @@ build_with_container() {
--env USER="$(id -u)" \
--env GROUP="$(id -g)" \
--env IMAGE_SIZE_ALIGNMENT_MB="${IMAGE_SIZE_ALIGNMENT_MB}" \
--env BUILD_VARIANT="${BUILD_VARIANT}" \
-v /dev:/dev \
-v "${script_dir}":"/osbuilder" \
-v "${script_dir}/../scripts":"/scripts" \
@@ -394,7 +396,7 @@ create_disk() {
else
rootfs_end_unit="MiB"
fi
if [ "${MEASURED_ROOTFS}" == "yes" ]; then
if [[ "${MEASURED_ROOTFS}" == "yes" ]]; then
info "Creating partitions with hash device"
# The hash data will take less than one percent disk space to store
hash_start=$(echo $img_size | awk '{print $1 * 0.99}' |cut -d $(locale decimal_point) -f 1)
@@ -484,10 +486,49 @@ create_rootfs_image() {
fsck.ext4 -D -y "${device}p1"
fi
if [ "${MEASURED_ROOTFS}" == "yes" ] && [ -b "${device}p2" ]; then
if [[ "${MEASURED_ROOTFS}" == "yes" ]] && [[ -b "${device}p2" ]]; then
info "veritysetup format rootfs device: ${device}p1, hash device: ${device}p2"
local image_dir=$(dirname "${image}")
veritysetup format "${device}p1" "${device}p2" > "${image_dir}"/root_hash.txt 2>&1
local -r image_dir=$(dirname "${image}")
local verity_output
verity_output=$(veritysetup format --no-superblock "${device}p1" "${device}p2" 2>&1)
build_kernel_verity_params() {
local -r output="$1"
local root_hash
local salt
local data_blocks
local data_block_size
local hash_block_size
read_verity_field() {
local -r label="$1"
local value
value=$(printf '%s\n' "${output}" | sed -n "s/^${label}:[[:space:]]*//p")
value="${value// \[*/}"
[[ -n "${value}" ]] || die "Missing '${label}' in verity output for ${image}"
echo "${value}"
}
root_hash=$(read_verity_field "Root hash")
salt=$(read_verity_field "Salt")
data_blocks=$(read_verity_field "Data blocks")
data_block_size=$(read_verity_field "Data block size")
hash_block_size=$(read_verity_field "Hash block size")
printf 'root_hash=%s,salt=%s,data_blocks=%s,data_block_size=%s,hash_block_size=%s' \
"${root_hash}" \
"${salt}" \
"${data_blocks}" \
"${data_block_size}" \
"${hash_block_size}"
}
local kernel_verity_params
kernel_verity_params="$(build_kernel_verity_params "${verity_output}")"
printf '%s\n' "${kernel_verity_params}" > "${image_dir}"/root_hash_"${BUILD_VARIANT}".txt
OK "Root hash file created for variant: ${BUILD_VARIANT}"
fi
losetup -d "${device}"
@@ -500,7 +541,7 @@ create_erofs_rootfs_image() {
local block_size="$3"
local agent_bin="$4"
if [ "$block_size" -ne 4096 ]; then
if [[ "${block_size}" -ne 4096 ]]; then
die "Invalid block size for erofs"
fi
@@ -508,7 +549,8 @@ create_erofs_rootfs_image() {
die "Could not setup loop device"
fi
local mount_dir=$(mktemp -p "${TMPDIR:-/tmp}" -d osbuilder-mount-dir.XXXX)
local mount_dir
mount_dir=$(mktemp -p "${TMPDIR:-/tmp}" -d osbuilder-mount-dir.XXXX)
info "Copying content from rootfs to root partition"
cp -a "${rootfs}"/* "${mount_dir}"
@@ -522,10 +564,10 @@ create_erofs_rootfs_image() {
info "Setup systemd"
setup_systemd "${mount_dir}"
readonly fsimage="$(mktemp)"
local -r fsimage="$(mktemp)"
mkfs.erofs -Enoinline_data "${fsimage}" "${mount_dir}"
local img_size="$(stat -c"%s" "${fsimage}")"
local img_size_mb="$(((("${img_size}" + 1048576) / 1048576) + 1 + "${rootfs_start}"))"
local -r img_size="$(stat -c"%s" "${fsimage}")"
local -r img_size_mb="$(((("${img_size}" + 1048576) / 1048576) + 1 + "${rootfs_start}"))"
create_disk "${image}" "${img_size_mb}" "ext4" "${rootfs_start}"

View File

@@ -147,6 +147,18 @@ install_nvidia_dcgm() {
datacenter-gpu-manager-exporter
}
install_devkit_packages() {
is_feature_enabled "devkit" || {
echo "chroot: Skipping devkit packages installation"
return
}
echo "chroot: Install devkit packages"
eval "${APT_INSTALL}" kmod
apt-mark hold kmod
}
cleanup_rootfs() {
echo "chroot: Cleanup NVIDIA GPU rootfs"
@@ -174,4 +186,5 @@ install_userspace_components
install_nvidia_fabricmanager
install_nvidia_ctk
install_nvidia_dcgm
install_devkit_packages
cleanup_rootfs

View File

@@ -24,7 +24,7 @@ KBUILD_SIGN_PIN=${KBUILD_SIGN_PIN:-}
AGENT_POLICY="${AGENT_POLICY:-no}"
NVIDIA_GPU_STACK=${NVIDIA_GPU_STACK:?NVIDIA_GPU_STACK must be set}
VARIANT=${VARIANT:?VARIANT must be set}
BUILD_VARIANT=${BUILD_VARIANT:?BUILD_VARIANT must be set}
ARCH=${ARCH:?ARCH must be set}
machine_arch="${ARCH}"
@@ -37,7 +37,7 @@ else
die "Unsupported architecture: ${machine_arch}"
fi
readonly stage_one="${BUILD_DIR:?}/rootfs-${VARIANT:?}-stage-one"
readonly stage_one="${BUILD_DIR:?}/rootfs-${BUILD_VARIANT:?}-stage-one"
setup_nvidia-nvrc() {
local url ver

View File

@@ -60,9 +60,9 @@ REPO_COMPONENTS=${REPO_COMPONENTS:-""}
KBUILD_SIGN_PIN=${KBUILD_SIGN_PIN:-""}
NVIDIA_GPU_STACK=${NVIDIA_GPU_STACK:-""}
VARIANT=${VARIANT:-""}
BUILD_VARIANT=${BUILD_VARIANT:-""}
[[ "${VARIANT}" == "nvidia-gpu"* ]] && source "${script_dir}/nvidia/nvidia_rootfs.sh"
[[ "${BUILD_VARIANT}" == "nvidia-gpu"* ]] && source "${script_dir}/nvidia/nvidia_rootfs.sh"
#For cross build
CROSS_BUILD=${CROSS_BUILD:-false}
@@ -571,7 +571,7 @@ build_rootfs_distro()
--env REPO_COMPONENTS="${REPO_COMPONENTS}" \
--env OSBUILDER_VERSION="${OSBUILDER_VERSION}" \
--env OS_VERSION="${OS_VERSION}" \
--env VARIANT="${VARIANT}" \
--env BUILD_VARIANT="${BUILD_VARIANT}" \
--env INSIDE_CONTAINER=1 \
--env SECCOMP="${SECCOMP}" \
--env SELINUX="${SELINUX}" \
@@ -913,13 +913,13 @@ main()
init="${ROOTFS_DIR}/sbin/init"
setup_rootfs
if [ "${VARIANT}" = "nvidia-gpu" ]; then
if [ "${BUILD_VARIANT}" = "nvidia-gpu" ]; then
setup_nvidia_gpu_rootfs_stage_one
setup_nvidia_gpu_rootfs_stage_two
return $?
fi
if [ "${VARIANT}" = "nvidia-gpu-confidential" ]; then
if [ "${BUILD_VARIANT}" = "nvidia-gpu-confidential" ]; then
setup_nvidia_gpu_rootfs_stage_one "confidential"
setup_nvidia_gpu_rootfs_stage_two "confidential"
return $?

View File

@@ -10,7 +10,8 @@ OS_VERSION=${OS_VERSION:-""}
[ -z "$OS_VERSION" ] && echo "OS_VERSION is required, but was not set" && exit 1
PACKAGES="chrony iptables dbus"
[ "$AGENT_INIT" = no ] && PACKAGES+=" init"
[ "$MEASURED_ROOTFS" = yes ] && PACKAGES+=" cryptsetup-bin e2fsprogs"
# CDH secure storage feature requires these tools in the guest
[[ "${CONFIDENTIAL_GUEST:-no}" = "yes" ]] && PACKAGES+=" cryptsetup-bin e2fsprogs"
[ "$SECCOMP" = yes ] && PACKAGES+=" libseccomp2"
[ "$(uname -m)" = "s390x" ] && PACKAGES+=" libcurl4 libnghttp2-14"
REPO_COMPONENTS=${REPO_COMPONENTS:-main}

View File

@@ -38,7 +38,7 @@ build_initrd() {
info "initrd os: $os_name"
info "initrd os version: $os_version"
make initrd \
VARIANT="${image_initrd_suffix}" \
BUILD_VARIANT="${image_initrd_suffix}" \
DISTRO="$os_name" \
DEBUG="${DEBUG:-}" \
OS_VERSION="${os_version}" \
@@ -68,7 +68,7 @@ build_image() {
info "image os: $os_name"
info "image os version: $os_version"
make image \
VARIANT="${image_initrd_suffix}" \
BUILD_VARIANT="${image_initrd_suffix}" \
DISTRO="${os_name}" \
DEBUG="${DEBUG:-}" \
USE_DOCKER="1" \
@@ -86,8 +86,9 @@ build_image() {
fi
mv -f "kata-containers.img" "${install_dir}/${artifact_name}"
if [ -e "root_hash.txt" ]; then
cp root_hash.txt "${install_dir}/"
if [[ -e "root_hash_${image_initrd_suffix}.txt" ]]; then
info "Copying root hash file for variant: ${image_initrd_suffix} ${PWD}"
cp "root_hash_${image_initrd_suffix}.txt" "${install_dir}/"
fi
(
cd "${install_dir}"

View File

@@ -1,34 +0,0 @@
# Copyright (c) 2026 Kata Contributors
#
# SPDX-License-Identifier: Apache-2.0
# Helm image based on kata-containers/kubectl for multi-arch support
# Used for kata-lifecycle-manager workflows and other helm operations
# hadolint ignore=DL3007
FROM quay.io/kata-containers/kubectl:latest
# Use bash with pipefail for safer pipe handling
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Download and install helm
# hadolint ignore=DL3018
RUN ARCH=$(uname -m) && \
case "${ARCH}" in \
x86_64) HELM_ARCH=amd64 ;; \
aarch64) HELM_ARCH=arm64 ;; \
ppc64le) HELM_ARCH=ppc64le ;; \
s390x) HELM_ARCH=s390x ;; \
*) echo "Unsupported architecture: ${ARCH}" && exit 1 ;; \
esac && \
HELM_VERSION=$(curl -s https://api.github.com/repos/helm/helm/releases/latest | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && \
curl -fL --progress-bar -o /tmp/helm.tar.gz \
"https://get.helm.sh/helm-${HELM_VERSION}-linux-${HELM_ARCH}.tar.gz" && \
tar -xzf /tmp/helm.tar.gz -C /tmp && \
mv "/tmp/linux-${HELM_ARCH}/helm" /usr/local/bin/helm && \
rm -rf /tmp/helm.tar.gz "/tmp/linux-${HELM_ARCH}" && \
chmod +x /usr/local/bin/helm && \
helm version --short
# Default to bash shell
CMD ["/bin/bash"]

View File

@@ -448,7 +448,3 @@ kata-qemu-snp-cicd kata-qemu-snp-cicd 77s
kata-qemu-tdx-cicd kata-qemu-tdx-cicd 77s
kata-stratovirt-cicd kata-stratovirt-cicd 77s
```
## Related Charts
- [kata-lifecycle-manager](kata-lifecycle-manager/README.md) - Argo Workflows-based lifecycle management for controlled, node-by-node upgrades with verification and automatic rollback

View File

@@ -1,32 +0,0 @@
# Copyright (c) 2026 The Kata Containers Authors
# SPDX-License-Identifier: Apache-2.0
apiVersion: v2
name: kata-lifecycle-manager
description: Argo Workflows-based lifecycle management for Kata Containers
type: application
# Chart version - follows kata-containers versioning
version: "3.26.0"
# App version - matches kata-containers
appVersion: "3.26.0"
keywords:
- kata-containers
- lifecycle-management
- upgrade
- argo-workflows
- gitops
home: https://katacontainers.io
sources:
- https://github.com/kata-containers/kata-containers
maintainers:
- name: Kata Containers Community
annotations:
kata-containers.io/companion-chart: kata-deploy

View File

@@ -1,333 +0,0 @@
# Kata Lifecycle Manager Helm Chart
Argo Workflows-based lifecycle management for Kata Containers.
This chart installs a namespace-scoped `WorkflowTemplate` that performs controlled,
node-by-node upgrades of kata-deploy with verification and automatic rollback on failure.
## Prerequisites
- Kubernetes cluster with kata-deploy installed via Helm
- [Argo Workflows](https://argoproj.github.io/argo-workflows/) v3.4+ installed
- `helm` and `argo` CLI tools
- **Verification pod spec** (see [Verification Pod](#verification-pod-required))
## Installation
```bash
# From OCI registry (when published)
helm install kata-lifecycle-manager oci://ghcr.io/kata-containers/kata-deploy-charts/kata-lifecycle-manager
# From local source
helm install kata-lifecycle-manager ./kata-lifecycle-manager
```
## Verification Pod (Required)
A verification pod is **required** to validate each node after upgrade. The chart
will fail to install without one.
### Option A: Bake into kata-lifecycle-manager (recommended)
Provide the verification pod when installing the chart:
```bash
helm install kata-lifecycle-manager ./kata-lifecycle-manager \
--set-file defaults.verificationPod=./my-verification-pod.yaml
```
This verification pod is baked into the `WorkflowTemplate` and used for all upgrades.
### Option B: Override at workflow submission
One-off override for a specific upgrade run. The pod spec must be base64-encoded
because Argo workflow parameters don't handle multi-line YAML reliably:
```bash
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p verification-pod="$(base64 -w0 < ./my-verification-pod.yaml)"
```
**Note:** During helm upgrade, kata-`kata-deploy`'s own verification is disabled
(`--set verification.pod=""`). This is because kata-`kata-deploy`'s verification is
cluster-wide (designed for initial install), while kata-lifecycle-manager performs
per-node verification with proper placeholder substitution.
### Verification Pod Spec
Create a pod spec that validates your Kata deployment. The pod should exit 0 on success,
non-zero on failure.
**Example (`my-verification-pod.yaml`):**
```yaml
apiVersion: v1
kind: Pod
metadata:
name: ${TEST_POD}
spec:
runtimeClassName: kata-qemu
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: ${NODE}
tolerations:
- operator: Exists
containers:
- name: verify
image: quay.io/kata-containers/alpine-bash-curl:latest
command:
- sh
- -c
- |
echo "=== Kata Verification ==="
echo "Node: ${NODE}"
echo "Kernel: $(uname -r)"
echo "SUCCESS: Pod running with Kata runtime"
```
### Placeholders
| Placeholder | Description |
|-------------|-------------|
| `${NODE}` | Node hostname being upgraded/verified |
| `${TEST_POD}` | Generated unique pod name |
**You are responsible for:**
- Setting the `runtimeClassName` in your pod spec
- Defining the verification logic in your container
- Using the exit code to indicate success (0) or failure (non-zero)
**Failure modes detected:**
- Pod stuck in Pending/`ContainerCreating` (runtime can't start VM)
- Pod crashes immediately (containerd/CRI-O configuration issues)
- Pod times out (resource issues, image pull failures)
- Pod exits with non-zero code (verification logic failed)
All of these trigger automatic rollback.
## Usage
### 1. Select Nodes for Upgrade
Nodes can be selected using **labels**, **taints**, or **both**.
**Option A: Label-based selection (default)**
```bash
# Label nodes for upgrade
kubectl label node worker-1 katacontainers.io/kata-lifecycle-manager-window=true
# Trigger upgrade
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p node-selector="katacontainers.io/kata-lifecycle-manager-window=true"
```
**Option B: Taint-based selection**
```bash
# Taint nodes for upgrade
kubectl taint nodes worker-1 kata-lifecycle-manager=pending:NoSchedule
# Trigger upgrade using taint selector
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p node-taint-key=kata-lifecycle-manager \
-p node-taint-value=pending
```
**Option C: Combined selection**
```bash
# Use both labels and taints for precise targeting
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p node-selector="node-pool=kata-pool" \
-p node-taint-key=kata-lifecycle-manager
```
### 2. Trigger Upgrade
```bash
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0
# Watch progress
argo watch @latest
```
### 3. Sequential Upgrade Behavior
Nodes are upgraded **sequentially** (one at a time) to ensure fleet consistency.
If any node fails verification, the workflow stops immediately and that node is
rolled back. This prevents ending up with a mixed fleet where some nodes have
the new version and others have the old version.
## Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `argoNamespace` | Namespace for Argo resources | `argo` |
| `defaults.helmRelease` | kata-deploy Helm release name | `kata-deploy` |
| `defaults.helmNamespace` | kata-deploy namespace | `kube-system` |
| `defaults.nodeSelector` | Node label selector (optional if using taints) | `""` |
| `defaults.nodeTaintKey` | Taint key for node selection | `""` |
| `defaults.nodeTaintValue` | Taint value filter (optional) | `""` |
| `defaults.verificationNamespace` | Namespace for verification pods | `default` |
| `defaults.verificationPod` | Pod YAML for verification **(required)** | `""` |
| `defaults.drainEnabled` | Enable node drain before upgrade | `false` |
| `defaults.drainTimeout` | Timeout for drain operation | `300s` |
| `images.helm` | Helm container image (multi-arch) | `quay.io/kata-containers/helm:latest` |
| `images.kubectl` | `kubectl` container image (multi-arch) | `quay.io/kata-containers/kubectl:latest` |
## Workflow Parameters
When submitting a workflow, you can override:
| Parameter | Description |
|-----------|-------------|
| `target-version` | **Required** - Target Kata version |
| `helm-release` | Helm release name |
| `helm-namespace` | Namespace of kata-deploy |
| `node-selector` | Label selector for nodes |
| `node-taint-key` | Taint key for node selection |
| `node-taint-value` | Taint value filter |
| `verification-namespace` | Namespace for verification pods |
| `verification-pod` | Pod YAML with placeholders |
| `drain-enabled` | Whether to drain nodes before upgrade |
| `drain-timeout` | Timeout for drain operation |
## Upgrade Flow
For each node selected by the node-selector label:
1. **Prepare**: Annotate node with upgrade status
2. **Cordon**: Mark node as `unschedulable`
3. **Drain** (optional): Evict pods if `drain-enabled=true`
4. **Upgrade**: Run `helm upgrade` for kata-deploy
5. **Wait**: Wait for kata-deploy DaemonSet pod to be ready
6. **Verify**: Run verification pod and check exit code
7. **On Success**: `Uncordon` node, proceed to next node
8. **On Failure**: Automatic rollback, `uncordon`, workflow stops
Nodes are upgraded **sequentially** (one at a time). If verification fails on any node,
the workflow stops immediately, preventing a mixed-version fleet.
### When to Use Drain
**Default (drain disabled):** Drain is not required for Kata upgrades. Running Kata
VMs continue using the in-memory binaries. Only new workloads use the upgraded
binaries.
**Optional drain:** Enable drain if you prefer to evict all workloads before any
maintenance operation, or if your organization's operational policies require it:
```bash
# Enable drain when installing the chart
helm install kata-lifecycle-manager ./kata-lifecycle-manager \
--set defaults.drainEnabled=true \
--set defaults.drainTimeout=600s \
--set-file defaults.verificationPod=./my-verification-pod.yaml
# Or override at workflow submission time
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0 \
-p drain-enabled=true \
-p drain-timeout=600s
```
## Rollback
**Automatic rollback on verification failure:** If the verification pod fails (non-zero exit),
kata-lifecycle-manager automatically:
1. Runs `helm rollback` to revert to the previous Helm release
2. Waits for kata-deploy DaemonSet to be ready with the previous version
3. `Uncordons` the node
4. Annotates the node with `rolled-back` status
This ensures nodes are never left in a broken state.
**Manual rollback:** For cases where you need to rollback a successfully upgraded node:
```bash
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
--entrypoint rollback-node \
-p node-name=worker-1
```
## Monitoring
Check node annotations to monitor upgrade progress:
```bash
kubectl get nodes \
-L katacontainers.io/kata-lifecycle-manager-status \
-L katacontainers.io/kata-current-version
```
| Annotation | Description |
|------------|-------------|
| `katacontainers.io/kata-lifecycle-manager-status` | Current upgrade phase |
| `katacontainers.io/kata-current-version` | Version after successful upgrade |
Status values:
- `preparing` - Upgrade starting
- `cordoned` - Node marked `unschedulable`
- `draining` - Draining pods (only if drain-enabled=true)
- `upgrading` - Helm upgrade in progress
- `verifying` - Verification pod running
- `completed` - Upgrade successful
- `rolling-back` - Rollback in progress (automatic on verification failure)
- `rolled-back` - Rollback completed
## Known Limitations
### Cluster-Wide DaemonSet Updates
The kata-deploy Helm chart uses a DaemonSet, which means `helm upgrade` updates
all nodes simultaneously at the Kubernetes level, even though this workflow
processes nodes sequentially for verification.
**Current behavior:**
1. Node A is cordoned and upgraded
2. Node A verification passes, Node A is `uncordoned`
3. New workloads can now start on Node A using the **new** Kata version
4. Node B verification fails
5. Automatic rollback reverts kata-deploy cluster-wide to the **old** version
6. Workloads that started on Node A between steps 2-5 continue running with
the new version's in-memory binaries, while new workloads use the old version
This is generally acceptable because:
- Running Kata VMs continue functioning (they use in-memory binaries)
- New workloads use the rolled-back version
- The cluster reaches a consistent state for new workloads
**Future improvement:** A two-phase approach could cordon all target nodes upfront,
perform the upgrade, verify all nodes, and only `uncordon` after all verifications
pass. This would prevent any new workloads from using the new version until the
entire upgrade is validated, at the cost of longer node unavailability.
## For Projects Using kata-deploy
Any project that uses the kata-deploy Helm chart can install this companion chart
to get upgrade orchestration:
```bash
# Install kata-deploy
helm install kata-deploy oci://ghcr.io/kata-containers/kata-deploy-charts/kata-deploy \
--namespace kube-system
# Install upgrade tooling with your verification config
helm install kata-lifecycle-manager oci://ghcr.io/kata-containers/kata-deploy-charts/kata-lifecycle-manager \
--set-file defaults.verificationPod=./my-verification-pod.yaml
# Trigger upgrade
argo submit -n argo --from workflowtemplate/kata-lifecycle-manager \
-p target-version=3.25.0
```
## License
Apache License 2.0

View File

@@ -1,46 +0,0 @@
{{/*
Copyright (c) 2026 The Kata Containers Authors
SPDX-License-Identifier: Apache-2.0
*/}}
{{/*
Expand the name of the chart.
*/}}
{{- define "kata-lifecycle-manager.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "kata-lifecycle-manager.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "kata-lifecycle-manager.labels" -}}
helm.sh/chart: {{ include "kata-lifecycle-manager.name" . }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "kata-lifecycle-manager.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: kata-containers
{{- end }}
{{/*
ServiceAccount name
*/}}
{{- define "kata-lifecycle-manager.serviceAccountName" -}}
{{- include "kata-lifecycle-manager.fullname" . }}
{{- end }}

View File

@@ -1,89 +0,0 @@
{{/*
Copyright (c) 2026 The Kata Containers Authors
SPDX-License-Identifier: Apache-2.0
*/}}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "kata-lifecycle-manager.serviceAccountName" . }}
namespace: {{ .Values.argoNamespace }}
labels:
{{- include "kata-lifecycle-manager.labels" . | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "kata-lifecycle-manager.fullname" . }}
labels:
{{- include "kata-lifecycle-manager.labels" . | nindent 4 }}
rules:
# Node operations (cordon, drain, label)
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch", "patch", "update"]
# Pod operations (eviction for drain, verification pods, Argo output parameters)
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "create", "delete", "patch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["pods/eviction"]
verbs: ["create"]
# DaemonSet operations (kata-deploy is a DaemonSet)
- apiGroups: ["apps"]
resources: ["daemonsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# RuntimeClass operations (kata-deploy creates RuntimeClasses)
- apiGroups: ["node.k8s.io"]
resources: ["runtimeclasses"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Helm needs these for release management
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# kata-deploy creates a ServiceAccount
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# kata-deploy creates RBAC resources
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Jobs (kata-deploy may have post-install/post-delete hooks)
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Events for debugging
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list", "watch", "create"]
# Argo Workflows task results (for output parameters)
- apiGroups: ["argoproj.io"]
resources: ["workflowtaskresults"]
verbs: ["create", "patch"]
# CRDs (kata-deploy may reference NFD CRDs)
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Node Feature Discovery resources (kata-deploy NFD integration)
- apiGroups: ["nfd.k8s-sigs.io"]
resources: ["nodefeaturerules"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "kata-lifecycle-manager.fullname" . }}
labels:
{{- include "kata-lifecycle-manager.labels" . | nindent 4 }}
subjects:
- kind: ServiceAccount
name: {{ include "kata-lifecycle-manager.serviceAccountName" . }}
namespace: {{ .Values.argoNamespace }}
roleRef:
kind: ClusterRole
name: {{ include "kata-lifecycle-manager.fullname" . }}
apiGroup: rbac.authorization.k8s.io

View File

@@ -1,775 +0,0 @@
{{/*
Copyright (c) 2026 The Kata Containers Authors
SPDX-License-Identifier: Apache-2.0
Argo WorkflowTemplate for orchestrating kata-deploy upgrades.
Uses native Argo resource templates where possible, and standard
helm/kubectl images for operations requiring those tools.
*/}}
{{- if not .Values.defaults.verificationPod }}
{{- fail "defaults.verificationPod is required. Provide a pod spec that validates your Kata deployment using --set-file defaults.verificationPod=./your-pod.yaml" }}
{{- end }}
---
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: kata-lifecycle-manager
namespace: {{ .Values.argoNamespace }}
labels:
{{- include "kata-lifecycle-manager.labels" . | nindent 4 }}
spec:
entrypoint: upgrade-all-nodes
serviceAccountName: {{ include "kata-lifecycle-manager.serviceAccountName" . }}
podGC:
strategy: OnWorkflowSuccess
arguments:
parameters:
- name: target-version
description: "Target Kata Containers version (e.g., 3.25.0)"
- name: helm-release
value: {{ .Values.defaults.helmRelease | quote }}
description: "Helm release name"
- name: helm-namespace
value: {{ .Values.defaults.helmNamespace | quote }}
description: "Namespace where kata-deploy is installed"
- name: node-selector
value: {{ .Values.defaults.nodeSelector | quote }}
description: "Label selector for nodes to upgrade (optional if using taint selection)"
- name: node-taint-key
value: {{ .Values.defaults.nodeTaintKey | quote }}
description: "Taint key for node selection (optional, alternative to label selector)"
- name: node-taint-value
value: {{ .Values.defaults.nodeTaintValue | quote }}
description: "Taint value filter (optional, only used with node-taint-key)"
- name: helm-image
value: {{ .Values.images.helm | quote }}
description: "Helm container image"
- name: kubectl-image
value: {{ .Values.images.kubectl | quote }}
description: "Kubectl container image"
- name: verification-namespace
value: {{ .Values.defaults.verificationNamespace | quote }}
description: "Namespace for verification pods"
- name: verification-pod
value: {{ .Values.defaults.verificationPod | b64enc | quote }}
description: "Base64-encoded pod YAML for verification (uses placeholders NODE, TEST_POD)"
- name: drain-enabled
value: {{ .Values.defaults.drainEnabled | quote }}
description: "Whether to drain nodes before upgrade"
- name: drain-timeout
value: {{ .Values.defaults.drainTimeout | quote }}
description: "Timeout for node drain"
templates:
# =========================================================================
# MAIN ENTRYPOINT
# =========================================================================
- name: upgrade-all-nodes
steps:
- - name: validate-prerequisites
template: check-prerequisites
- - name: get-nodes
template: get-target-nodes
- - name: show-upgrade-plan
template: print-upgrade-plan
arguments:
parameters:
- name: nodes-json
value: "{{`{{steps.get-nodes.outputs.parameters.nodes}}`}}"
- name: node-count
value: "{{`{{steps.get-nodes.outputs.parameters.node-count}}`}}"
- - name: upgrade-nodes-sequentially
template: upgrade-node-chain
arguments:
parameters:
- name: nodes-json
value: "{{`{{steps.get-nodes.outputs.parameters.nodes}}`}}"
- name: current-index
value: "0"
- - name: summary
template: print-summary
# =========================================================================
# CHECK PREREQUISITES (fail fast if verification pod not configured)
# =========================================================================
- name: check-prerequisites
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.helm-image}}`}}"
command: [sh]
source: |
set -e
RELEASE="{{`{{workflow.parameters.helm-release}}`}}"
NS="{{`{{workflow.parameters.helm-namespace}}`}}"
VERIFICATION_POD="{{`{{workflow.parameters.verification-pod}}`}}"
echo "=============================================="
echo " VALIDATING PREREQUISITES"
echo "=============================================="
echo ""
# Check verification pod availability
echo "Checking verification pod configuration..."
if [ -n "$VERIFICATION_POD" ]; then
echo "✓ Verification pod configured"
else
echo ""
echo "ERROR: No verification pod configured!"
echo ""
echo "The upgrade cannot proceed without a verification pod."
echo ""
echo "This should not happen if kata-lifecycle-manager was installed correctly."
echo "Reinstall with: helm upgrade kata-lifecycle-manager ... --set-file defaults.verificationPod=<path>"
echo ""
exit 1
fi
# Check kata-deploy release exists
echo "Checking kata-deploy Helm release..."
if helm status "$RELEASE" -n "$NS" &>/dev/null; then
CURRENT_VERSION=$(helm get metadata "$RELEASE" -n "$NS" -o json 2>/dev/null | grep -o '"version":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
echo "✓ Found Helm release: $RELEASE (chart version: $CURRENT_VERSION)"
else
echo ""
echo "ERROR: Helm release '$RELEASE' not found in namespace '$NS'"
echo ""
echo "Make sure kata-deploy is installed via Helm before running upgrade."
exit 1
fi
echo ""
echo "=============================================="
echo " ALL PREREQUISITES PASSED"
echo "=============================================="
# =========================================================================
# PRINT UPGRADE PLAN (shows all nodes before starting)
# =========================================================================
- name: print-upgrade-plan
inputs:
parameters:
- name: nodes-json
- name: node-count
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.kubectl-image}}`}}"
command: [sh]
source: |
apk add --no-cache -q jq 2>/dev/null || true
NODES_JSON='{{`{{inputs.parameters.nodes-json}}`}}'
NODE_COUNT={{`{{inputs.parameters.node-count}}`}}
VERSION="{{`{{workflow.parameters.target-version}}`}}"
echo "=============================================="
echo " KATA CONTAINERS UPGRADE PLAN"
echo "=============================================="
echo ""
echo "Target Version: $VERSION"
echo "Total Nodes: $NODE_COUNT"
echo "Mode: Sequential (one at a time)"
echo ""
echo "Nodes to upgrade (in order):"
echo "----------------------------------------------"
INDEX=1
echo "$NODES_JSON" | jq -r '.[]' | while read NODE; do
echo " $INDEX. $NODE"
INDEX=$((INDEX + 1))
done
echo ""
echo "----------------------------------------------"
echo "Upgrade will stop immediately on first failure"
echo "=============================================="
# =========================================================================
# SEQUENTIAL NODE CHAIN (recursive: upgrades one node, then next)
# Stops immediately on first failure - no mixed fleet possible
# =========================================================================
- name: upgrade-node-chain
inputs:
parameters:
- name: nodes-json
- name: current-index
steps:
# Extract current node info
- - name: node-info
template: get-node-at-index
arguments:
parameters:
- name: nodes-json
value: "{{`{{inputs.parameters.nodes-json}}`}}"
- name: index
value: "{{`{{inputs.parameters.current-index}}`}}"
# Upgrade current node (shows all sub-steps: prepare, cordon, upgrade, verify...)
- - name: upgrade
template: upgrade-single-node
arguments:
parameters:
- name: node-name
value: "{{`{{steps.node-info.outputs.parameters.node-name}}`}}"
# Continue to next node only if this one succeeded and more nodes exist
- - name: next
template: upgrade-node-chain
when: "{{`{{steps.node-info.outputs.parameters.has-more}}`}} == true"
arguments:
parameters:
- name: nodes-json
value: "{{`{{inputs.parameters.nodes-json}}`}}"
- name: current-index
value: "{{`{{steps.node-info.outputs.parameters.next-index}}`}}"
# =========================================================================
# GET NODE AT INDEX (helper for sequential chain)
# =========================================================================
- name: get-node-at-index
inputs:
parameters:
- name: nodes-json
- name: index
outputs:
parameters:
- name: node-name
valueFrom:
path: /tmp/node-name.txt
- name: has-more
valueFrom:
path: /tmp/has-more.txt
- name: next-index
valueFrom:
path: /tmp/next-index.txt
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.kubectl-image}}`}}"
command: [sh]
source: |
set -e
apk add --no-cache -q jq 2>/dev/null || true
NODES_JSON='{{`{{inputs.parameters.nodes-json}}`}}'
INDEX={{`{{inputs.parameters.index}}`}}
NODE=$(echo "$NODES_JSON" | jq -r ".[$INDEX]")
TOTAL=$(echo "$NODES_JSON" | jq 'length')
NEXT=$((INDEX + 1))
echo "$NODE" > /tmp/node-name.txt
echo "$NEXT" > /tmp/next-index.txt
if [ "$NEXT" -lt "$TOTAL" ]; then
echo "true" > /tmp/has-more.txt
else
echo "false" > /tmp/has-more.txt
fi
echo "=== Node $((INDEX + 1)) of $TOTAL: $NODE ==="
# =========================================================================
# GET TARGET NODES (supports label selector, taint selector, or both)
# =========================================================================
- name: get-target-nodes
# Tolerate system taints so workflow pods can schedule during upgrades
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
outputs:
parameters:
- name: nodes
valueFrom:
path: /tmp/nodes.json
- name: node-count
valueFrom:
path: /tmp/count.txt
script:
image: "{{`{{workflow.parameters.kubectl-image}}`}}"
command: [bash]
source: |
set -e
LABEL_SELECTOR="{{`{{workflow.parameters.node-selector}}`}}"
TAINT_KEY="{{`{{workflow.parameters.node-taint-key}}`}}"
TAINT_VALUE="{{`{{workflow.parameters.node-taint-value}}`}}"
# Get nodes based on label selector (or all nodes if no selector)
if [[ -n "$LABEL_SELECTOR" ]]; then
NODE_NAMES=$(kubectl get nodes -l "$LABEL_SELECTOR" -o jsonpath='{.items[*].metadata.name}')
else
NODE_NAMES=$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}')
fi
if [[ -z "$NODE_NAMES" ]]; then
echo "[]" > /tmp/nodes.json
echo "0" > /tmp/count.txt
echo "No nodes found matching label selector"
exit 0
fi
# If taint key specified, filter nodes by taint
if [[ -n "$TAINT_KEY" ]]; then
TAINT_FILTERED=""
for node in $NODE_NAMES; do
# Get taints for this node
if [[ -n "$TAINT_VALUE" ]]; then
# Check for specific taint key=value
HAS_TAINT=$(kubectl get node "$node" -o jsonpath="{.spec.taints[?(@.key=='$TAINT_KEY' && @.value=='$TAINT_VALUE')].key}" 2>/dev/null || echo "")
else
# Check for taint key only
HAS_TAINT=$(kubectl get node "$node" -o jsonpath="{.spec.taints[?(@.key=='$TAINT_KEY')].key}" 2>/dev/null || echo "")
fi
if [[ -n "$HAS_TAINT" ]]; then
TAINT_FILTERED="$TAINT_FILTERED $node"
fi
done
NODE_NAMES=$(echo "$TAINT_FILTERED" | xargs)
fi
if [[ -z "$NODE_NAMES" ]]; then
echo "[]" > /tmp/nodes.json
echo "0" > /tmp/count.txt
echo "No nodes found matching selection criteria"
exit 0
fi
# Convert space-separated names to sorted newline-separated
NODE_LIST=$(echo "$NODE_NAMES" | tr ' ' '\n' | sort)
NODE_COUNT=$(echo "$NODE_LIST" | wc -l)
# Output as JSON array (manually build it without jq)
JSON_ARRAY="["
FIRST=true
for node in $NODE_LIST; do
if [[ "$FIRST" == "true" ]]; then
JSON_ARRAY="${JSON_ARRAY}\"${node}\""
FIRST=false
else
JSON_ARRAY="${JSON_ARRAY},\"${node}\""
fi
done
JSON_ARRAY="${JSON_ARRAY}]"
echo "$JSON_ARRAY" > /tmp/nodes.json
echo "$NODE_COUNT" > /tmp/count.txt
echo "Found $NODE_COUNT nodes for upgrade:"
echo "$NODE_LIST"
# =========================================================================
# UPGRADE SINGLE NODE
# =========================================================================
- name: upgrade-single-node
inputs:
parameters:
- name: node-name
steps:
- - name: prepare
template: prepare-node
arguments:
parameters:
- name: node-name
value: "{{`{{inputs.parameters.node-name}}`}}"
- - name: cordon
template: cordon-node
arguments:
parameters:
- name: node-name
value: "{{`{{inputs.parameters.node-name}}`}}"
- - name: drain
template: drain-node
when: "{{`{{workflow.parameters.drain-enabled}}`}} == true"
arguments:
parameters:
- name: node-name
value: "{{`{{inputs.parameters.node-name}}`}}"
- - name: upgrade
template: helm-upgrade
arguments:
parameters:
- name: node-name
value: "{{`{{inputs.parameters.node-name}}`}}"
- - name: wait-ready
template: wait-kata-ready
arguments:
parameters:
- name: node-name
value: "{{`{{inputs.parameters.node-name}}`}}"
- - name: verify-and-complete
template: verify-and-complete-node
arguments:
parameters:
- name: node-name
value: "{{`{{inputs.parameters.node-name}}`}}"
# =========================================================================
# PREPARE NODE
# =========================================================================
- name: prepare-node
inputs:
parameters:
- name: node-name
resource:
action: patch
mergeStrategy: merge
manifest: |
apiVersion: v1
kind: Node
metadata:
name: "{{`{{inputs.parameters.node-name}}`}}"
annotations:
katacontainers.io/kata-lifecycle-manager-status: "preparing"
# =========================================================================
# CORDON NODE
# =========================================================================
- name: cordon-node
inputs:
parameters:
- name: node-name
resource:
action: patch
mergeStrategy: merge
manifest: |
apiVersion: v1
kind: Node
metadata:
name: "{{`{{inputs.parameters.node-name}}`}}"
annotations:
katacontainers.io/kata-lifecycle-manager-status: "cordoned"
spec:
unschedulable: true
# =========================================================================
# DRAIN NODE (optional, only runs if drain-enabled is true)
# =========================================================================
- name: drain-node
inputs:
parameters:
- name: node-name
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.kubectl-image}}`}}"
command: [bash]
source: |
set -e
NODE="{{`{{inputs.parameters.node-name}}`}}"
TIMEOUT="{{`{{workflow.parameters.drain-timeout}}`}}"
# On failure: mark node as failed and uncordon
cleanup_on_failure() {
echo "ERROR: Drain failed, cleaning up node $NODE"
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="failed" || true
kubectl uncordon "$NODE" || true
}
trap cleanup_on_failure EXIT
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="draining"
kubectl drain "$NODE" \
--ignore-daemonsets \
--delete-emptydir-data \
--force \
--timeout="$TIMEOUT"
# Success - remove the trap
trap - EXIT
# =========================================================================
# HELM UPGRADE
# =========================================================================
- name: helm-upgrade
inputs:
parameters:
- name: node-name
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.helm-image}}`}}"
command: [bash]
source: |
set -e
NODE="{{`{{inputs.parameters.node-name}}`}}"
VERSION="{{`{{workflow.parameters.target-version}}`}}"
RELEASE="{{`{{workflow.parameters.helm-release}}`}}"
NS="{{`{{workflow.parameters.helm-namespace}}`}}"
CHART="oci://ghcr.io/kata-containers/kata-deploy-charts/kata-deploy"
apk add --no-cache -q kubectl
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="upgrading"
# On failure: mark node as failed and uncordon
cleanup_on_failure() {
echo "ERROR: Helm upgrade failed, cleaning up node $NODE"
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="failed" || true
kubectl uncordon "$NODE" || true
}
trap cleanup_on_failure EXIT
# Disable kata-deploy's verification (--set verification.pod="") because:
# - kata-deploy verification is cluster-wide (runs once after helm upgrade)
# - kata-lifecycle-manager does per-node verification in verify-and-complete-node
# The per-node verification is more appropriate for rolling upgrades.
helm upgrade "$RELEASE" "$CHART" \
--namespace "$NS" \
--version "$VERSION" \
--reuse-values \
--set verification.pod="" \
--rollback-on-failure \
--timeout 10m \
--wait
# Success - remove the trap so we don't run cleanup
trap - EXIT
# =========================================================================
# WAIT FOR KATA-DEPLOY READY
# =========================================================================
- name: wait-kata-ready
inputs:
parameters:
- name: node-name
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.kubectl-image}}`}}"
command: [bash]
source: |
set -e
NODE="{{`{{inputs.parameters.node-name}}`}}"
NS="{{`{{workflow.parameters.helm-namespace}}`}}"
# On failure: mark node as failed and uncordon
cleanup_on_failure() {
echo "ERROR: Timed out waiting for kata-deploy, cleaning up node $NODE"
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="failed" || true
kubectl uncordon "$NODE" || true
}
trap cleanup_on_failure EXIT
for i in $(seq 1 60); do
POD=$(kubectl get pods -n "$NS" -l name=kata-deploy \
--field-selector spec.nodeName="$NODE" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$POD" ]]; then
if kubectl wait pod "$POD" -n "$NS" --for=condition=Ready --timeout=10s 2>/dev/null; then
echo "kata-deploy pod $POD is ready"
trap - EXIT
exit 0
fi
fi
echo "Waiting... ($i/60)"
sleep 5
done
exit 1
# =========================================================================
# VERIFY AND COMPLETE NODE (with automatic rollback on failure)
# =========================================================================
- name: verify-and-complete-node
inputs:
parameters:
- name: node-name
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.helm-image}}`}}"
command: [bash]
source: |
# Don't use set -e - we need to reach rollback logic even on errors
NODE="{{`{{inputs.parameters.node-name}}`}}"
VERIFY_NS="{{`{{workflow.parameters.verification-namespace}}`}}"
RELEASE="{{`{{workflow.parameters.helm-release}}`}}"
NS="{{`{{workflow.parameters.helm-namespace}}`}}"
VERSION="{{`{{workflow.parameters.target-version}}`}}"
VERIFICATION_POD="{{`{{workflow.parameters.verification-pod}}`}}"
TEST_POD="kata-verify-${NODE}-$(date +%s)"
# Install kubectl (helm image is based on kubectl image but just in case)
apk add --no-cache -q kubectl 2>/dev/null || true
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="verifying"
# Decode verification pod spec (base64-encoded)
echo "Using verification pod from workflow parameters"
echo "$VERIFICATION_POD" | base64 -d > /tmp/verify-pod.yaml
# Apply verification pod with placeholder substitution
sed -i "s|\${NODE}|$NODE|g" /tmp/verify-pod.yaml
sed -i "s|\${TEST_POD}|$TEST_POD|g" /tmp/verify-pod.yaml
if ! kubectl apply -n "$VERIFY_NS" -f /tmp/verify-pod.yaml; then
echo "ERROR: Failed to create verification pod"
VERIFY_SUCCESS=false
fi
# Cleanup function for verification pod
cleanup_pod() {
kubectl delete pod "$TEST_POD" -n "$VERIFY_NS" --ignore-not-found --wait=false
}
trap cleanup_pod EXIT
# Wait for verification pod to complete (only if pod was created)
# This catches all failure modes:
# - Pod stuck in Pending/ContainerCreating (runtime can't start VM)
# - Pod crashes immediately (containerd/CRI-O config issues)
# - Pod times out (resource issues, image pull failures)
# - Pod exits with non-zero code (verification logic failed)
if [ "${VERIFY_SUCCESS:-}" != "false" ]; then
VERIFY_SUCCESS=false
if kubectl wait pod "$TEST_POD" -n "$VERIFY_NS" --for=jsonpath='{.status.phase}'=Succeeded --timeout=180s; then
echo "=== Verification Succeeded ==="
kubectl logs "$TEST_POD" -n "$VERIFY_NS" || true
VERIFY_SUCCESS=true
else
echo "=== Verification Failed ==="
echo ""
echo "Pod status:"
kubectl get pod "$TEST_POD" -n "$VERIFY_NS" -o wide || true
echo ""
echo "Pod events and details:"
kubectl describe pod "$TEST_POD" -n "$VERIFY_NS" || true
echo ""
echo "Pod logs (if available):"
kubectl logs "$TEST_POD" -n "$VERIFY_NS" || true
fi
fi
# Clean up verification pod
cleanup_pod
trap - EXIT
if [ "$VERIFY_SUCCESS" = "true" ]; then
# Success path: uncordon and mark complete
echo "Uncordoning node $NODE..."
kubectl uncordon "$NODE"
kubectl annotate node "$NODE" --overwrite \
katacontainers.io/kata-lifecycle-manager-status="completed" \
katacontainers.io/kata-current-version="$VERSION"
echo "Node $NODE upgrade completed successfully"
exit 0
else
# Failure path: automatic rollback
echo "Initiating automatic rollback for node $NODE..."
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="rolling-back"
helm rollback "$RELEASE" -n "$NS" --wait --timeout 10m
# Wait for kata-deploy to be ready after rollback
echo "Waiting for kata-deploy to be ready after rollback..."
for i in $(seq 1 60); do
POD=$(kubectl get pods -n "$NS" -l name=kata-deploy \
--field-selector spec.nodeName="$NODE" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$POD" ]]; then
if kubectl wait pod "$POD" -n "$NS" --for=condition=Ready --timeout=10s 2>/dev/null; then
echo "kata-deploy pod $POD is ready after rollback"
break
fi
fi
echo "Waiting for rollback to complete... ($i/60)"
sleep 5
done
# Uncordon and mark as rolled back
kubectl uncordon "$NODE"
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="rolled-back"
echo "Node $NODE rolled back to previous version"
# Exit with error so workflow shows the failure
exit 1
fi
# =========================================================================
# PRINT SUMMARY
# =========================================================================
- name: print-summary
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.kubectl-image}}`}}"
command: [bash]
source: |
echo "=== KATA UPGRADE SUMMARY ==="
kubectl get nodes \
-L katacontainers.io/kata-runtime \
-L katacontainers.io/kata-lifecycle-manager-status \
-L katacontainers.io/kata-current-version
# =========================================================================
# ROLLBACK (can be called manually)
# =========================================================================
- name: rollback-node
inputs:
parameters:
- name: node-name
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
- key: node.kubernetes.io/disk-pressure
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
script:
image: "{{`{{workflow.parameters.helm-image}}`}}"
command: [sh]
source: |
set -e
NODE="{{`{{inputs.parameters.node-name}}`}}"
RELEASE="{{`{{workflow.parameters.helm-release}}`}}"
NS="{{`{{workflow.parameters.helm-namespace}}`}}"
apk add --no-cache -q kubectl
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="rolling-back"
helm rollback "$RELEASE" -n "$NS" --wait --timeout 10m
kubectl annotate node "$NODE" --overwrite katacontainers.io/kata-lifecycle-manager-status="rolled-back"
kubectl uncordon "$NODE"

View File

@@ -1,63 +0,0 @@
# Copyright (c) 2026 The Kata Containers Authors
# SPDX-License-Identifier: Apache-2.0
# Argo Workflows namespace where the WorkflowTemplate will be created
argoNamespace: argo
# Default workflow parameters (can be overridden when submitting workflows)
defaults:
# Helm release name for kata-deploy
helmRelease: kata-deploy
# Namespace where kata-deploy is installed
helmNamespace: kube-system
# Label selector for nodes to upgrade (optional if using taint selection)
nodeSelector: ""
# Taint-based node selection (optional, alternative to label selector)
# Select nodes that have a taint with this key
nodeTaintKey: ""
# Optional: filter by taint value (only used if nodeTaintKey is set)
nodeTaintValue: ""
# Namespace for verification pods
verificationNamespace: default
# Verification pod spec (REQUIRED)
#
# A verification pod is required to validate each node after upgrade.
# The chart will fail to install without one.
#
# Provide via:
# helm install kata-lifecycle-manager ... \
# --set-file defaults.verificationPod=/path/to/your-verification-pod.yaml
#
# Or override at workflow submission (base64-encoded):
# argo submit ... -p verification-pod="$(base64 -w0 < pod.yaml)"
#
# Note: kata-deploy's own verification is disabled during upgrade because
# it is cluster-wide (designed for initial install), while kata-lifecycle-manager
# performs per-node verification with proper placeholder substitution.
#
# Placeholders substituted at runtime:
# ${NODE} - the node being upgraded/verified
# ${TEST_POD} - generated unique pod name
#
verificationPod: ""
# Optional: Drain nodes before upgrade (default: false)
# Not required for Kata upgrades since running VMs use in-memory binaries.
# Enable if you prefer to evict workloads before upgrading.
drainEnabled: false
# Timeout for node drain (only used if drainEnabled is true)
drainTimeout: "300s"
# Container images used by workflow steps
images:
# Helm image for helm upgrade operations (multi-arch)
helm: quay.io/kata-containers/helm:latest
# Kubectl image for kubernetes operations (multi-arch)
kubectl: quay.io/kata-containers/kubectl:latest

View File

@@ -99,7 +99,7 @@ TDSHIM_CONTAINER_BUILDER="${TDSHIM_CONTAINER_BUILDER:-}"
TOOLS_CONTAINER_BUILDER="${TOOLS_CONTAINER_BUILDER:-}"
VIRTIOFSD_CONTAINER_BUILDER="${VIRTIOFSD_CONTAINER_BUILDER:-}"
AGENT_INIT="${AGENT_INIT:-no}"
MEASURED_ROOTFS="${MEASURED_ROOTFS:-}"
MEASURED_ROOTFS="${MEASURED_ROOTFS:-no}"
USE_CACHE="${USE_CACHE:-}"
BUSYBOX_CONF_FILE=${BUSYBOX_CONF_FILE:-}
NVIDIA_GPU_STACK="${NVIDIA_GPU_STACK:-}"

View File

@@ -185,7 +185,10 @@ get_kernel_modules_dir() {
}
cleanup_and_fail_shim_v2_specifics() {
rm -f "${repo_root_dir}/tools/packaging/kata-deploy/local-build/build/shim-v2-root_hash.txt"
for variant in confidential nvidia-gpu nvidia-gpu-confidential; do
local root_hash_file="${repo_root_dir}/tools/packaging/kata-deploy/local-build/build/shim-v2-root_hash_${variant}.txt"
[[ -f "${root_hash_file}" ]] && rm -f "${root_hash_file}"
done
return $(cleanup_and_fail "${1:-}" "${2:-}")
}
@@ -209,31 +212,37 @@ cleanup_and_fail() {
}
install_cached_shim_v2_tarball_get_root_hash() {
if [ "${MEASURED_ROOTFS}" != "yes" ]; then
return 0
fi
local tarball_dir="${repo_root_dir}/tools/packaging/kata-deploy/local-build/build"
local image_conf_tarball="kata-static-rootfs-image-confidential.tar.zst"
local root_hash_basedir="./opt/kata/share/kata-containers/"
tar --zstd -xvf "${tarball_dir}/${image_conf_tarball}" ${root_hash_basedir}root_hash.txt --transform s,${root_hash_basedir},,
mv root_hash.txt "${tarball_dir}/root_hash.txt"
for variant in confidential nvidia-gpu nvidia-gpu-confidential; do
local image_conf_tarball="kata-static-rootfs-image-${variant}.tar.zst"
local tarball_path="${tarball_dir}/${image_conf_tarball}"
local root_hash_path="${root_hash_basedir}root_hash_${variant}.txt"
# If variant does not exist we skip the current iteration.
[[ ! -f "${tarball_path}" ]] && continue
tar --zstd -tf "${tarball_path}" "${root_hash_path}" >/dev/null 2>&1 || continue
tar --zstd -xvf "${tarball_path}" "${root_hash_path}" --transform s,"${root_hash_basedir}",, || die "Failed to extract root hash from ${tarball_path}"
mv "root_hash_${variant}.txt" "${tarball_dir}/"
done
return 0
}
install_cached_shim_v2_tarball_compare_root_hashes() {
if [ "${MEASURED_ROOTFS}" != "yes" ]; then
return 0
fi
local found_any=""
local tarball_dir="${repo_root_dir}/tools/packaging/kata-deploy/local-build/build"
[ -f shim-v2-root_hash.txt ] || return 1
for variant in confidential nvidia-gpu nvidia-gpu-confidential; do
# Skip if one or the other does not exist.
[[ ! -f "${tarball_dir}/root_hash_${variant}.txt" ]] && continue
diff "${tarball_dir}/root_hash.txt" shim-v2-root_hash.txt || return 1
diff "${tarball_dir}/root_hash_${variant}.txt" "shim-v2-root_hash_${variant}.txt" || return 1
found_any="yes"
done
[[ -z "${found_any}" ]] && return 0
return 0
}
@@ -473,9 +482,9 @@ install_image() {
#Install guest image for confidential guests
install_image_confidential() {
if [ "${ARCH}" == "s390x" ]; then
export MEASURED_ROOTFS=no
export MEASURED_ROOTFS="no"
else
export MEASURED_ROOTFS=yes
export MEASURED_ROOTFS="yes"
fi
install_image "confidential"
}
@@ -583,7 +592,7 @@ install_initrd() {
#Install guest initrd for confidential guests
install_initrd_confidential() {
export MEASURED_ROOTFS=no
export MEASURED_ROOTFS="no"
install_initrd "confidential"
}
@@ -610,6 +619,7 @@ install_initrd_confidential() {
# Install NVIDIA GPU image
install_image_nvidia_gpu() {
export AGENT_POLICY
export MEASURED_ROOTFS="yes"
local version=$(get_from_kata_deps .externals.nvidia.driver.version)
EXTRA_PKGS="apt curl ${EXTRA_PKGS}"
NVIDIA_GPU_STACK=${NVIDIA_GPU_STACK:-"driver=${version},compute,dcgm"}
@@ -619,6 +629,7 @@ install_image_nvidia_gpu() {
# Install NVIDIA GPU initrd
install_initrd_nvidia_gpu() {
export AGENT_POLICY
export MEASURED_ROOTFS="no"
local version=$(get_from_kata_deps .externals.nvidia.driver.version)
EXTRA_PKGS="apt curl ${EXTRA_PKGS}"
NVIDIA_GPU_STACK=${NVIDIA_GPU_STACK:-"driver=${version},compute,dcgm"}
@@ -628,9 +639,9 @@ install_initrd_nvidia_gpu() {
# Instal NVIDIA GPU confidential image
install_image_nvidia_gpu_confidential() {
export AGENT_POLICY
export MEASURED_ROOTFS="yes"
local version=$(get_from_kata_deps .externals.nvidia.driver.version)
EXTRA_PKGS="apt curl ${EXTRA_PKGS}"
# TODO: export MEASURED_ROOTFS=yes
NVIDIA_GPU_STACK=${NVIDIA_GPU_STACK:-"driver=${version},compute,dcgm"}
install_image "nvidia-gpu-confidential"
}
@@ -638,9 +649,9 @@ install_image_nvidia_gpu_confidential() {
# Install NVIDIA GPU confidential initrd
install_initrd_nvidia_gpu_confidential() {
export AGENT_POLICY
export MEASURED_ROOTFS="no"
local version=$(get_from_kata_deps .externals.nvidia.driver.version)
EXTRA_PKGS="apt curl ${EXTRA_PKGS}"
# TODO: export MEASURED_ROOTFS=yes
NVIDIA_GPU_STACK=${NVIDIA_GPU_STACK:-"driver=${version},compute,dcgm"}
install_initrd "nvidia-gpu-confidential"
}
@@ -743,9 +754,9 @@ install_kernel() {
install_kernel_confidential() {
if [ "${ARCH}" == "s390x" ]; then
export MEASURED_ROOTFS=no
export MEASURED_ROOTFS="no"
else
export MEASURED_ROOTFS=yes
export MEASURED_ROOTFS="yes"
fi
install_kernel_helper \
@@ -755,7 +766,7 @@ install_kernel_confidential() {
}
install_kernel_cca_confidential() {
export MEASURED_ROOTFS=yes
export MEASURED_ROOTFS="yes"
install_kernel_helper \
"assets.kernel-arm-experimental.confidential" \
@@ -779,6 +790,7 @@ install_kernel_nvidia_gpu_dragonball_experimental() {
#Install GPU enabled kernel asset
install_kernel_nvidia_gpu() {
export MEASURED_ROOTFS="yes"
install_kernel_helper \
"assets.kernel.nvidia" \
"kernel-nvidia-gpu" \
@@ -787,6 +799,7 @@ install_kernel_nvidia_gpu() {
#Install GPU and TEE enabled kernel asset
install_kernel_nvidia_gpu_confidential() {
export MEASURED_ROOTFS="yes"
install_kernel_helper \
"assets.kernel.nvidia-confidential" \
"kernel-nvidia-gpu-confidential" \
@@ -1019,19 +1032,22 @@ install_shimv2() {
export MEASURED_ROOTFS
export RUNTIME_CHOICE
if [ "${MEASURED_ROOTFS}" = "yes" ]; then
local image_conf_tarball="${workdir}/kata-static-rootfs-image-confidential.tar.zst"
if [ ! -f "${image_conf_tarball}" ]; then
die "Building the shim-v2 with MEASURED_ROOTFS support requires a rootfs confidential image tarball"
fi
for variant in confidential nvidia-gpu nvidia-gpu-confidential; do
local image_conf_tarball
image_conf_tarball="$(find "${workdir}" -maxdepth 1 -name "kata-static-rootfs-image-${variant}.tar.zst" 2>/dev/null | head -n 1)"
# Only one variant may be built at a time so we need to
# skip one or the other if not available.
[[ -f "${image_conf_tarball}" ]] || continue
local root_hash_basedir="./opt/kata/share/kata-containers/"
if ! tar --zstd -xvf ${image_conf_tarball} --transform s,${root_hash_basedir},, ${root_hash_basedir}root_hash.txt; then
die "Building the shim-v2 with MEASURED_ROOTFS support requires a rootfs confidential image tarball built with MEASURED_ROOTFS support"
local root_hash_path="${root_hash_basedir}root_hash_${variant}.txt"
tar --zstd -tf "${image_conf_tarball}" "${root_hash_path}" >/dev/null 2>&1 || continue
if ! tar --zstd -xvf "${image_conf_tarball}" --transform s,"${root_hash_basedir}",, "${root_hash_path}"; then
die "Cannot extract root hash from ${image_conf_tarball}"
fi
mv root_hash.txt ${workdir}/root_hash.txt
fi
mv "root_hash_${variant}.txt" "${workdir}/root_hash_${variant}.txt"
done
DESTDIR="${destdir}" PREFIX="${prefix}" "${shimv2_builder}"
}
@@ -1462,8 +1478,10 @@ handle_build() {
tar --zstd -tvf "${modules_final_tarball_path}"
;;
shim-v2)
if [ "${MEASURED_ROOTFS}" = "yes" ]; then
mv ${workdir}/root_hash.txt ${workdir}/shim-v2-root_hash.txt
if [[ "${MEASURED_ROOTFS}" == "yes" ]]; then
for variant in confidential nvidia-gpu nvidia-gpu-confidential; do
[[ -f "${workdir}/root_hash_${variant}.txt" ]] && mv "${workdir}/root_hash_${variant}.txt" "${workdir}/shim-v2-root_hash_${variant}.txt"
done
fi
;;
esac
@@ -1521,10 +1539,15 @@ handle_build() {
)
;;
shim-v2)
if [ "${MEASURED_ROOTFS}" = "yes" ]; then
files_to_push+=(
"shim-v2-root_hash.txt"
)
if [[ "${MEASURED_ROOTFS}" == "yes" ]]; then
local found_any=""
for variant in confidential nvidia-gpu nvidia-gpu-confidential; do
# The variants could be built independently we need to check if
# they exist and then push them to the registry
[[ -f "${workdir}/shim-v2-root_hash_${variant}.txt" ]] && files_to_push+=("shim-v2-root_hash_${variant}.txt")
found_any="yes"
done
[[ -z "${found_any}" ]] && die "No files to push for shim-v2 with MEASURED_ROOTFS support"
fi
;;
*)

View File

@@ -28,7 +28,6 @@ readonly default_kernel_config_dir="${script_dir}/configs"
# Default path to search for kernel config fragments
readonly default_config_frags_dir="${script_dir}/configs/fragments"
readonly default_config_whitelist="${script_dir}/configs/fragments/whitelist.conf"
readonly default_initramfs="${script_dir}/initramfs.cpio.gz"
# xPU vendor
readonly VENDOR_INTEL="intel"
readonly VENDOR_NVIDIA="nvidia"
@@ -135,12 +134,6 @@ arch_to_kernel() {
esac
}
# When building for measured rootfs the initramfs image should be previously built.
check_initramfs_or_die() {
[ -f "${default_initramfs}" ] || \
die "Initramfs for measured rootfs not found at ${default_initramfs}"
}
get_git_kernel() {
local kernel_path="${2:-}"
@@ -312,15 +305,11 @@ get_kernel_frag_path() {
all_configs="${all_configs} ${dpu_configs}"
fi
if [ "${measured_rootfs}" == "true" ]; then
if [[ "${measured_rootfs}" == "true" ]]; then
info "Enabling config for confidential guest trust storage protection"
local cryptsetup_configs="$(ls ${common_path}/confidential_containers/cryptsetup.conf)"
local cryptsetup_configs
cryptsetup_configs="$(ls "${common_path}"/confidential_containers/cryptsetup.conf)"
all_configs="${all_configs} ${cryptsetup_configs}"
check_initramfs_or_die
info "Enabling config for confidential guest measured boot"
local initramfs_configs="$(ls ${common_path}/confidential_containers/initramfs.conf)"
all_configs="${all_configs} ${initramfs_configs}"
fi
if [[ "${conf_guest}" != "" ]];then
@@ -504,12 +493,6 @@ setup_kernel() {
[ -n "${hypervisor_target}" ] || hypervisor_target="kvm"
[ -n "${kernel_config_path}" ] || kernel_config_path=$(get_default_kernel_config "${kernel_version}" "${hypervisor_target}" "${arch_target}" "${kernel_path}")
if [ "${measured_rootfs}" == "true" ]; then
check_initramfs_or_die
info "Copying initramfs from: ${default_initramfs}"
cp "${default_initramfs}" ./
fi
info "Copying config file from: ${kernel_config_path}"
cp "${kernel_config_path}" ./.config
ARCH=${arch_target} make oldconfig ${CROSS_BUILD_ARG}

View File

@@ -4,6 +4,7 @@ CONFIG_BLK_DEV_DM=y
CONFIG_DM_CRYPT=y
CONFIG_DM_VERITY=y
CONFIG_DM_INTEGRITY=y
CONFIG_DM_INIT=y
CONFIG_CRYPTO_AEAD=y
CONFIG_CRYPTO_AEAD2=y
CONFIG_CRYPTO_AKCIPHER2=y

View File

@@ -1 +0,0 @@
CONFIG_INITRAMFS_SOURCE="initramfs.cpio.gz"

View File

@@ -1 +1 @@
177
178

View File

@@ -231,14 +231,9 @@ function _upload_helm_chart_tarball()
RELEASE_VERSION="$(_release_version)"
# Package and upload kata-deploy chart
helm dependencies update ${repo_root_dir}/tools/packaging/kata-deploy/helm-chart/kata-deploy
helm package ${repo_root_dir}/tools/packaging/kata-deploy/helm-chart/kata-deploy
gh release upload "${RELEASE_VERSION}" "kata-deploy-${RELEASE_VERSION}.tgz"
# Package and upload kata-lifecycle-manager chart
helm package ${repo_root_dir}/tools/packaging/kata-deploy/helm-chart/kata-lifecycle-manager
gh release upload "${RELEASE_VERSION}" "kata-lifecycle-manager-${RELEASE_VERSION}.tgz"
}
function main()

View File

@@ -1,69 +0,0 @@
# Copyright (c) 2022 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
ARG cryptsetup_repo=${cryptsetup_repo}
ARG cryptsetup_version=${cryptsetup_version}
ARG lvm2_repo=${lvm2_repo}
ARG lvm2_version=${lvm2_version}
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV TZ=UTC
RUN apt-get update && apt-get upgrade -y && \
apt-get --no-install-recommends install -y \
apt-utils \
asciidoctor \
autoconf \
autopoint \
automake \
busybox-static \
ca-certificates \
curl \
gcc \
gettext \
git \
libaio-dev \
libblkid-dev \
libselinux1-dev \
libtool \
libpopt-dev \
libjson-c-dev \
libssl-dev \
make \
ninja-build \
pkg-config \
uuid-dev \
libseccomp-dev \
libseccomp2 \
zlib1g-dev &&\
apt-get clean && rm -rf /var/lib/apt/lists/ && \
build_root=$(mktemp -d) && \
pushd ${build_root} && \
echo "Build ${lvm2_repo} version: ${lvm2_version}" && \
git clone --depth 1 --branch "${lvm2_version}" "${lvm2_repo}" lvm2 && \
pushd lvm2 && \
./configure --enable-static_link --disable-selinux && \
make && make install && \
cp ./libdm/libdevmapper.pc /usr/lib/pkgconfig/devmapper.pc && \
popd && \
echo "Build ${cryptsetup_repo} version: ${cryptsetup_version}" && \
git clone --depth 1 --branch "${cryptsetup_version}" "${cryptsetup_repo}" cryptsetup && \
pushd cryptsetup && \
./autogen.sh && \
./configure --enable-static --enable-static-cryptsetup --disable-udev --disable-external-tokens --disable-ssh-token && \
make && make install && \
strip /usr/sbin/veritysetup.static && \
popd && \
echo "Build gen_init_cpio tool" && \
git clone --depth 1 --filter=blob:none --sparse https://github.com/torvalds/linux.git && \
pushd linux && \
git sparse-checkout add usr && cd usr && make gen_init_cpio && \
install gen_init_cpio /usr/sbin/ && \
popd && \
popd && \
rm -rf ${build_root}
COPY init.sh /usr/sbin/init.sh

View File

@@ -1,22 +0,0 @@
#!/bin/bash
#
# Copyright (c) 2022 Intel
#
# SPDX-License-Identifier: Apache-2.0
set -o errexit
set -o nounset
set -o pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/../../scripts/lib.sh"
install_dir="${1:-.}"
tmpfile="$(mktemp -t initramfs.XXXXXX.cpio)"
trap 'rm -f "$tmpfile"' EXIT
if ! gen_init_cpio "${script_dir}/initramfs.list" > "${tmpfile}"; then
echo "gen_init_cpio failed" >&2
exit 1
fi
gzip -9 -n -c "${tmpfile}" > "${install_dir}"/initramfs.cpio.gz

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env bash
#
# Copyright (c) 2022 Intel
#
# SPDX-License-Identifier: Apache-2.0
set -o errexit
set -o nounset
set -o pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root_dir="$(cd "${script_dir}/../../../.." && pwd)"
readonly initramfs_builder="${script_dir}/build-initramfs.sh"
readonly default_install_dir="$(cd "${script_dir}/../../kernel" && pwd)"
source "${script_dir}/../../scripts/lib.sh"
kata_version="${kata_version:-}"
cryptsetup_repo="${cryptsetup_repo:-}"
cryptsetup_version="${cryptsetup_version:-}"
lvm2_repo="${lvm2_repo:-}"
lvm2_version="${lvm2_version:-}"
package_output_dir="${package_output_dir:-}"
[ -n "${cryptsetup_repo}" ] || cryptsetup_repo=$(get_from_kata_deps ".externals.cryptsetup.url")
[ -n "${cryptsetup_version}" ] || cryptsetup_version=$(get_from_kata_deps ".externals.cryptsetup.version")
[ -n "${lvm2_repo}" ] || lvm2_repo=$(get_from_kata_deps ".externals.lvm2.url")
[ -n "${lvm2_version}" ] || lvm2_version=$(get_from_kata_deps ".externals.lvm2.version")
[ -n "${cryptsetup_repo}" ] || die "Failed to get cryptsetup repo"
[ -n "${cryptsetup_version}" ] || die "Failed to get cryptsetup version"
[ -n "${lvm2_repo}" ] || die "Failed to get lvm2 repo"
[ -n "${lvm2_version}" ] || die "Failed to get lvm2 version"
container_image="${BUILDER_REGISTRY}:initramfs-cryptsetup-${cryptsetup_version}-lvm2-${lvm2_version}-$(get_last_modification ${repo_root_dir} ${script_dir})-$(uname -m)"
docker pull ${container_image} || (docker build \
--build-arg cryptsetup_repo="${cryptsetup_repo}" \
--build-arg cryptsetup_version="${cryptsetup_version}" \
--build-arg lvm2_repo="${lvm2_repo}" \
--build-arg lvm2_version="${lvm2_version}" \
-t "${container_image}" "${script_dir}" && \
# No-op unless PUSH_TO_REGISTRY is exported as "yes"
push_to_registry "${container_image}")
docker run --rm -i -v "${repo_root_dir}:${repo_root_dir}" \
-w "${PWD}" \
"${container_image}" \
bash -c "${initramfs_builder} ${default_install_dir}"

View File

@@ -1,60 +0,0 @@
#!/bin/sh
#
# Copyright (c) 2022 Intel
#
# SPDX-License-Identifier: Apache-2.0
[ -d /dev ] || mkdir -m 0755 /dev
[ -d /root ] || mkdir -m 0700 /root
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /mnt ] || mkdir /mnt
[ -d /tmp ] || mkdir /tmp
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
echo "/sbin/mdev" > /proc/sys/kernel/hotplug
mdev -s
get_option() {
local value
value=" $(cat /proc/cmdline) "
value="${value##* ${1}=}"
value="${value%% *}"
[ "${value}" != "" ] && echo "${value}"
}
rootfs_verifier=$(get_option rootfs_verity.scheme)
rootfs_hash=$(get_option rootfs_verity.hash)
root_device=$(get_option root)
hash_device=${root_device%?}2
# The root device should exist to be either verified then mounted or
# just mounted when verification is disabled.
if [ ! -e "${root_device}" ]
then
echo "No root device ${root_device} found"
exit 1
fi
if [ "${rootfs_verifier}" = "dm-verity" ]
then
echo "Verify the root device with ${rootfs_verifier}"
if [ ! -e "${hash_device}" ]
then
echo "No hash device ${hash_device} found. Cannot verify the root device"
exit 1
fi
veritysetup open --panic-on-corruption "${root_device}" root "${hash_device}" "${rootfs_hash}"
mount /dev/mapper/root /mnt
else
echo "No LUKS device found"
mount "${root_device}" /mnt
fi
umount /proc
umount /sys
exec switch_root /mnt /sbin/init

View File

@@ -1,21 +0,0 @@
# Copyright (c) 2022 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
# initramfs to setup verified boot for rootfs
dir /dev 0755 0 0
dir /root 0700 0 0
dir /sbin 0755 0 0
dir /bin 0755 0 0
dir /run 0755 0 0
dir /mnt 0755 0 0
file /init /usr/sbin/init.sh 0755 0 0
file /sbin/busybox /usr/bin/busybox 0755 0 0
file /sbin/veritysetup /usr/sbin/veritysetup.static 0755 0 0
slink /bin/sh /sbin/busybox 0755 0 0
slink /sbin/mount /sbin/busybox 0755 0 0
slink /bin/mkdir /sbin/busybox 0755 0 0
slink /sbin/mdev /sbin/busybox 0755 0 0
slink /sbin/switch_root /sbin/busybox 0755 0 0
slink /sbin/umount /sbin/busybox 0755 0 0
slink /sbin/cat /sbin/busybox 0755 0 0

View File

@@ -18,7 +18,6 @@ repo_root_dir="${repo_root_dir:-}"
[[ -n "${repo_root_dir}" ]] || die "repo_root_dir is not set"
readonly kernel_builder="${repo_root_dir}/tools/packaging/kernel/build-kernel.sh"
readonly initramfs_builder="${repo_root_dir}/tools/packaging/static-build/initramfs/build.sh"
BUILDX=
PLATFORM=
@@ -32,10 +31,6 @@ kernel_builder_args="-a ${ARCH:-} $*"
KERNEL_DEBUG_ENABLED=${KERNEL_DEBUG_ENABLED:-"no"}
if [[ "${MEASURED_ROOTFS}" == "yes" ]]; then
info "build initramfs for cc kernel"
"${initramfs_builder}"
# Turn on the flag to build the kernel with support to
# measured rootfs.
kernel_builder_args+=" -m"
fi

View File

@@ -36,18 +36,31 @@ case "${RUNTIME_CHOICE}" in
esac
[ "${CROSS_BUILD}" == "true" ] && container_image_bk="${container_image}" && container_image="${container_image}-cross-build"
if [ "${MEASURED_ROOTFS}" == "yes" ]; then
info "Enable rootfs measurement config"
root_hash_file="${repo_root_dir}/tools/packaging/kata-deploy/local-build/build/root_hash.txt"
# Variants (targets) that build a measured rootfs as of now are:
# - rootfs-image-confidential
# - rootfs-image-nvidia-gpu
# - rootfs-image-nvidia-gpu-confidential
#
root_hash_dir="${repo_root_dir}/tools/packaging/kata-deploy/local-build/build"
verity_variants=(
"confidential:KERNELVERITYPARAMS"
"nvidia-gpu:KERNELVERITYPARAMS_NV"
"nvidia-gpu-confidential:KERNELVERITYPARAMS_CONFIDENTIAL_NV"
)
for entry in "${verity_variants[@]}"; do
variant="${entry%%:*}"
param_var="${entry#*:}"
root_hash_file="${root_hash_dir}/root_hash_${variant}.txt"
[[ -f "${root_hash_file}" ]] || continue
[ -f "$root_hash_file" ] || \
die "Root hash file for measured rootfs not found at ${root_hash_file}"
# root_hash_*.txt contains a single kernel_verity_params line.
IFS= read -r root_measure_config < "${root_hash_file}"
root_measure_config="${root_measure_config%$'\r'}"
[[ -n "${root_measure_config}" ]] || die "Empty kernel verity params in ${root_hash_file}"
root_hash=$(sed -e 's/Root hash:\s*//g;t;d' "${root_hash_file}")
root_measure_config="rootfs_verity.scheme=dm-verity rootfs_verity.hash=${root_hash}"
EXTRA_OPTS+=" ROOTMEASURECONFIG=\"${root_measure_config}\""
fi
EXTRA_OPTS+=" ${param_var}=${root_measure_config}"
done
docker pull ${container_image} || \
(docker ${BUILDX} build ${PLATFORM} \
@@ -76,7 +89,7 @@ case "${RUNTIME_CHOICE}" in
-w "${repo_root_dir}/src/runtime-rs" \
--user "$(id -u)":"$(id -g)" \
"${container_image}" \
bash -c "make clean-generated-files && make PREFIX=${PREFIX} QEMUCMD=qemu-system-${arch}"
bash -c "make clean-generated-files && make PREFIX=${PREFIX} QEMUCMD=qemu-system-${arch} ${EXTRA_OPTS}"
docker run --rm -i -v "${repo_root_dir}:${repo_root_dir}" \
--env CROSS_BUILD=${CROSS_BUILD} \
@@ -85,7 +98,7 @@ case "${RUNTIME_CHOICE}" in
-w "${repo_root_dir}/src/runtime-rs" \
--user "$(id -u)":"$(id -g)" \
"${container_image}" \
bash -c "make PREFIX="${PREFIX}" DESTDIR="${DESTDIR}" install"
bash -c "make PREFIX="${PREFIX}" DESTDIR="${DESTDIR}" ${EXTRA_OPTS} install"
;;
esac

View File

@@ -317,7 +317,7 @@ externals:
# version older than them.
version: "v1.7.25"
lts: "v1.7"
active: "v2.1"
active: "v2.2"
# keep the latest version to make the current PR ci work, once it was
# merged,we can remove the latest version.
latest: "v2.2"