diff --git a/.github/workflows/build-podvm-cohere.yaml b/.github/workflows/build-podvm-cohere.yaml index 4c92d98124..9a03b066e9 100644 --- a/.github/workflows/build-podvm-cohere.yaml +++ b/.github/workflows/build-podvm-cohere.yaml @@ -12,6 +12,14 @@ on: type: choice options: [ubuntu, fedora] default: ubuntu + tee_platform: + description: >- + AA variant. tdx-nvidia = live TDX fleet (baselines enforced). + cc-nvidia = universal image for the new AMD GPUs (baselines deferred). + required: false + type: choice + options: [tdx-nvidia, cc-nvidia] + default: tdx-nvidia ssh_public_key: description: "SSH public key for debug images (optional)" required: false @@ -21,6 +29,11 @@ on: required: false type: boolean default: true + deploy_azure: + description: "Publish to Azure Compute Gallery (needs Azure CC infra)." + required: false + type: boolean + default: false permissions: actions: read # download smoke measurements artifact in the GCP smoke workflow @@ -30,10 +43,9 @@ permissions: packages: write # push OCI artifact to GHCR env: - # tdx-nvidia selects the AA OCI tag suffix (-tdx-nvidia_x86_64) that - # bundles libnvat.so* alongside the binary. The fork's publish-artifacts - # workflow produces this variant; upstream guest-components does not. - TEE_PLATFORM: tdx-nvidia + # AA OCI tag suffix (-_x86_64) pulled by `make binaries`. + # Push/tag builds default to the live tdx-nvidia; cc-nvidia is dispatch-only. + TEE_PLATFORM: ${{ inputs.tee_platform || 'tdx-nvidia' }} OCI_IMAGE: ghcr.io/${{ github.repository }}/podvm VERSIONS_YAML: src/cloud-api-adaptor/versions.yaml SMOKE_MACHINE_TYPE: a3-highgpu-1g @@ -163,6 +175,8 @@ jobs: oras version - name: Install cvm-measure + # TDX-only: cc-nvidia defers baselines (Phase 2). + if: env.TEE_PLATFORM == 'tdx-nvidia' env: CVM_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -232,6 +246,7 @@ jobs: - name: Mint GitHub App token for baselines id: app-token + if: env.TEE_PLATFORM == 'tdx-nvidia' uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ secrets.GH_APP_CLIENT_ID }} @@ -241,6 +256,7 @@ jobs: - name: Extract UKI and predict TDX measurements id: measure + if: env.TEE_PLATFORM == 'tdx-nvidia' env: GH_TOKEN: ${{ steps.app-token.outputs.token }} IMAGE_NAME: ${{ matrix.profile == 'release' && needs.meta.outputs.image_name_release || needs.meta.outputs.image_name_debug }} @@ -344,6 +360,54 @@ jobs: cat /tmp/measurements.json + - name: Write metadata-only measurements (no attestation baseline) + # cc-nvidia: emit deploy metadata only, no rtmr2/mrtd (baselines = Phase 2). + if: env.TEE_PLATFORM != 'tdx-nvidia' + env: + IMAGE_NAME: ${{ matrix.profile == 'release' && needs.meta.outputs.image_name_release || needs.meta.outputs.image_name_debug }} + IMAGE_FAMILY: ${{ matrix.profile == 'release' && needs.meta.outputs.family_release || needs.meta.outputs.family_debug }} + IMAGE_CLASS: ${{ needs.meta.outputs.class }} + IMAGE_TTL: ${{ needs.meta.outputs.ttl }} + CAA_COMMIT: ${{ github.sha }} + CAA_REF: ${{ github.ref_name }} + DISTRO: ${{ needs.meta.outputs.distro }} + PROFILE: ${{ matrix.profile }} + run: | + # Surface the pinned nvidia driver (best-effort; omitted if absent). + DISTRO_CONF="src/cloud-api-adaptor/podvm-mkosi/mkosi.images/system/mkosi.conf.d/${DISTRO}.conf" + NVIDIA_DRIVER_PKG="" + if [ -f "$DISTRO_CONF" ]; then + NVIDIA_DRIVER_PKG=$(awk -F= ' + /^[[:space:]]*nvidia-driver-[0-9]+(-open)?[[:space:]]*=/ { + gsub(/[[:space:]]/,"",$1); gsub(/[[:space:]]/,"",$2); + print $1 "=" $2; exit + }' "$DISTRO_CONF") + fi + NVIDIA_DRIVER_VERSION="${NVIDIA_DRIVER_PKG#*=}" + + jq -n \ + --arg image_name "$IMAGE_NAME" \ + --arg image_family "$IMAGE_FAMILY" \ + --arg image_class "$IMAGE_CLASS" \ + --arg image_ttl "$IMAGE_TTL" \ + --arg nvidia_driver_pkg "$NVIDIA_DRIVER_PKG" \ + --arg nvidia_driver_version "$NVIDIA_DRIVER_VERSION" \ + --arg caa_commit "$CAA_COMMIT" \ + --arg caa_ref "$CAA_REF" \ + --arg caa_version "$CAA_REF" \ + --arg distro "$DISTRO" \ + --arg profile "$PROFILE" \ + --arg tee_platform "$TEE_PLATFORM" \ + --arg build_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + '$ARGS.named + | if .nvidia_driver_version == "" then + del(.nvidia_driver_pkg, .nvidia_driver_version) + else + . + end' > /tmp/measurements.json + + cat /tmp/measurements.json + - name: Prepare artifacts run: | tar -czf /tmp/disk.tar.gz -C /tmp disk.raw @@ -351,7 +415,7 @@ jobs: echo "Compressed size: $(du -sh /tmp/disk.tar.gz | cut -f1)" - name: Upload smoke measurements - if: matrix.profile == 'debug' + if: matrix.profile == 'debug' && env.TEE_PLATFORM == 'tdx-nvidia' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: smoke-measurements-${{ needs.meta.outputs.image_name_debug }} @@ -376,7 +440,8 @@ jobs: CAA_COMMIT: ${{ github.sha }} run: | OCI_REF="${OCI_IMAGE}:${OCI_TAG}" - RTMR2=$(jq -r '.rtmr2_sha384' /tmp/measurements.json) + # rtmr2 present only for tdx-nvidia; cc-nvidia omits it. + RTMR2=$(jq -r '.rtmr2_sha384 // ""' /tmp/measurements.json) NVIDIA_DRIVER_VERSION=$(jq -r '.nvidia_driver_version // ""' /tmp/measurements.json) cd /tmp @@ -388,8 +453,11 @@ jobs: --annotation "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" --annotation "com.cohere.caa.commit=${CAA_COMMIT}" --annotation "com.cohere.caa.version=${GITHUB_REF_NAME}" - --annotation "com.cohere.rtmr2=${RTMR2}" + --annotation "com.cohere.tee_platform=${TEE_PLATFORM}" ) + if [ -n "$RTMR2" ]; then + ANNOTATIONS+=(--annotation "com.cohere.rtmr2=${RTMR2}") + fi if [ -n "$NVIDIA_DRIVER_VERSION" ]; then ANNOTATIONS+=(--annotation "com.cohere.nvidia.driver_version=${NVIDIA_DRIVER_VERSION}") fi @@ -441,10 +509,11 @@ jobs: smoke-gcp-tdx: name: Smoke TDX measurements (GCP debug image) needs: [meta, deploy-gcp] + # TDX-only: cc-nvidia has no predicted measurements to reproduce yet. if: | needs.deploy-gcp.result == 'success' && (github.event_name == 'push' || - (github.event_name == 'workflow_dispatch' && inputs.deploy_gcp)) + (github.event_name == 'workflow_dispatch' && inputs.deploy_gcp && inputs.tee_platform == 'tdx-nvidia')) uses: ./.github/workflows/smoke-gcp-tdx.yaml with: image_tag: ${{ needs.meta.outputs.image_tag_debug }} @@ -455,3 +524,22 @@ jobs: GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} GCP_SERVICE_ACCOUNT: ${{ secrets.GCP_SERVICE_ACCOUNT }} DEBUG_SSH_PRIVATE_KEY: ${{ secrets.DEBUG_SSH_PRIVATE_KEY }} + + # Deploy to Azure Compute Gallery — opt-in; needs the Azure CC infra. + deploy-azure: + name: Deploy ${{ matrix.profile }} to Azure + needs: [meta, build] + if: | + needs.build.result == 'success' && + github.event_name == 'workflow_dispatch' && inputs.deploy_azure + strategy: + fail-fast: false + matrix: + profile: [release, debug] + uses: ./.github/workflows/deploy-azure-cohere.yaml + with: + image_tag: ${{ matrix.profile == 'release' && needs.meta.outputs.image_tag_release || needs.meta.outputs.image_tag_debug }} + secrets: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} diff --git a/.github/workflows/deploy-azure-cohere.yaml b/.github/workflows/deploy-azure-cohere.yaml new file mode 100644 index 0000000000..7fee7e1220 --- /dev/null +++ b/.github/workflows/deploy-azure-cohere.yaml @@ -0,0 +1,233 @@ +name: Deploy PodVM to Azure (Cohere) + +# Publishes a podvm artifact to an Azure Compute Gallery image version flagged +# ConfidentialVmSupported (x86_64/Gen2) for SEV-SNP VMs (e.g. NCCadsH100v5). +# Requires (infra-azure): +# secrets: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID (OIDC app +# with Contributor + Storage Blob Data Contributor) +# vars: AZURE_RESOURCE_GROUP, AZURE_PODVM_GALLERY_NAME, +# AZURE_PODVM_STORAGE_ACCOUNT, AZURE_PODVM_BLOB_CONTAINER, +# AZURE_LOCATION + +on: + workflow_call: + inputs: + image_tag: + description: "OCI artifact tag in GHCR to deploy" + required: true + type: string + secrets: + AZURE_CLIENT_ID: + required: true + AZURE_TENANT_ID: + required: true + AZURE_SUBSCRIPTION_ID: + required: true + + workflow_dispatch: + inputs: + image_tag: + description: "OCI artifact tag in GHCR to deploy" + required: true + type: string + +permissions: + id-token: write # Azure login via OIDC federated credentials + contents: read # actions/checkout + packages: read # pull OCI artifact from GHCR + attestations: read # gh attestation verify + +env: + OCI_IMAGE: ghcr.io/${{ github.repository }}/podvm + VERSIONS_YAML: src/cloud-api-adaptor/versions.yaml + +jobs: + deploy: + name: Azure + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Install ORAS + run: | + command -v yq || sudo snap install yq + ORAS_VERSION=$(yq '.tools.oras' "$VERSIONS_YAML") + curl -fsSLO "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz" + tar -xzf "oras_${ORAS_VERSION}_linux_amd64.tar.gz" oras + sudo mv oras /usr/local/bin/ + oras version + + - name: Login to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify attestation + id: verify + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + IMAGE_DIGEST=$(oras resolve "${OCI_IMAGE}:${IMAGE_TAG}") + IMAGE_REF="${OCI_IMAGE}@${IMAGE_DIGEST}" + gh attestation verify \ + "oci://${IMAGE_REF}" \ + --repo "${GITHUB_REPOSITORY}" + echo "image_ref=$IMAGE_REF" >> "$GITHUB_OUTPUT" + echo "Attestation verified for ${IMAGE_REF}" + + - name: Pull artifact and read measurements + id: pull + env: + IMAGE_REF: ${{ steps.verify.outputs.image_ref }} + run: | + mkdir -p /tmp/podvm + cd /tmp/podvm + oras pull "${IMAGE_REF}" + ls -lh + + if [ ! -f measurements.json ]; then + echo "ERROR: measurements.json not found in artifact — build is broken" + exit 1 + fi + cat measurements.json + + IMAGE_NAME=$(jq -r '.image_name' measurements.json) + IMAGE_FAMILY=$(jq -r '.image_family // ""' measurements.json) + if [ -z "$IMAGE_NAME" ] || [ "$IMAGE_NAME" = "null" ] || [ -z "$IMAGE_FAMILY" ]; then + echo "ERROR: image_name/image_family missing in measurements.json" >&2 + exit 1 + fi + + # def names: [A-Za-z0-9._-], <=80 chars. + IMAGE_DEF=$(echo "$IMAGE_FAMILY" | sed 's/[^A-Za-z0-9._-]/-/g' | cut -c1-80) + + # Versions need numeric Major.Minor.Patch, no leading zeros. + MAJOR=$(date -u +%Y) + MINOR=$(date -u +%m%d | sed 's/^0*//'); MINOR=${MINOR:-0} + PATCH=$(date -u +%H%M | sed 's/^0*//'); PATCH=${PATCH:-0} + IMAGE_VERSION="${MAJOR}.${MINOR}.${PATCH}" + + { + echo "image_name=$IMAGE_NAME" + echo "image_def=$IMAGE_DEF" + echo "image_version=$IMAGE_VERSION" + echo "tee_platform=$(jq -r '.tee_platform // "tdx"' measurements.json)" + echo "distro=$(jq -r '.distro' measurements.json)" + echo "profile=$(jq -r '.profile' measurements.json)" + echo "caa_commit=$(jq -r '.caa_commit' measurements.json)" + } >> "$GITHUB_OUTPUT" + + - name: Extract raw disk and convert to fixed VHD + id: vhd + run: | + cd /tmp/podvm + tar -xzf disk.tar.gz + # Azure needs a fixed-format VHD with an exact (1 MiB-aligned) size. + qemu-img convert -f raw -O vpc -o subformat=fixed,force_size \ + disk.raw disk.vhd + ls -lh disk.vhd + + - name: Azure login (OIDC) + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Publish confidential gallery image version + env: + RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} + GALLERY: ${{ vars.AZURE_PODVM_GALLERY_NAME }} + STORAGE_ACCOUNT: ${{ vars.AZURE_PODVM_STORAGE_ACCOUNT }} + BLOB_CONTAINER: ${{ vars.AZURE_PODVM_BLOB_CONTAINER }} + LOCATION: ${{ vars.AZURE_LOCATION }} + IMAGE_NAME: ${{ steps.pull.outputs.image_name }} + IMAGE_DEF: ${{ steps.pull.outputs.image_def }} + IMAGE_VERSION: ${{ steps.pull.outputs.image_version }} + TEE_PLATFORM: ${{ steps.pull.outputs.tee_platform }} + run: | + set -euo pipefail + for v in RESOURCE_GROUP GALLERY STORAGE_ACCOUNT BLOB_CONTAINER LOCATION; do + if [ -z "${!v}" ]; then + echo "ERROR: required Azure var '$v' is empty — configure the infra-azure prerequisites" >&2 + exit 1 + fi + done + + BLOB_NAME="${IMAGE_NAME}.vhd" + BLOB_URI="https://${STORAGE_ACCOUNT}.blob.core.windows.net/${BLOB_CONTAINER}/${BLOB_NAME}" + + echo "Uploading staging VHD page blob: $BLOB_URI" + az storage blob upload \ + --account-name "$STORAGE_ACCOUNT" \ + --container-name "$BLOB_CONTAINER" \ + --name "$BLOB_NAME" \ + --file /tmp/podvm/disk.vhd \ + --type page \ + --auth-mode login \ + --overwrite true + + # Create the ConfidentialVmSupported def on first use. + if ! az sig image-definition show \ + --resource-group "$RESOURCE_GROUP" \ + --gallery-name "$GALLERY" \ + --gallery-image-definition "$IMAGE_DEF" >/dev/null 2>&1; then + echo "Creating image definition $IMAGE_DEF (ConfidentialVmSupported)" + az sig image-definition create \ + --resource-group "$RESOURCE_GROUP" \ + --gallery-name "$GALLERY" \ + --gallery-image-definition "$IMAGE_DEF" \ + --publisher cohere \ + --offer podvm \ + --sku "$IMAGE_DEF" \ + --os-type Linux \ + --os-state Generalized \ + --hyper-v-generation V2 \ + --architecture x64 \ + --features SecurityType=ConfidentialVmSupported + fi + + if az sig image-version show \ + --resource-group "$RESOURCE_GROUP" \ + --gallery-name "$GALLERY" \ + --gallery-image-definition "$IMAGE_DEF" \ + --gallery-image-version "$IMAGE_VERSION" >/dev/null 2>&1; then + echo "ERROR: image version $IMAGE_DEF:$IMAGE_VERSION already exists" >&2 + echo "Gallery versions are immutable; re-run to bump the time-based patch." >&2 + exit 1 + fi + + echo "Creating gallery image version $IMAGE_DEF:$IMAGE_VERSION" + az sig image-version create \ + --resource-group "$RESOURCE_GROUP" \ + --gallery-name "$GALLERY" \ + --gallery-image-definition "$IMAGE_DEF" \ + --gallery-image-version "$IMAGE_VERSION" \ + --location "$LOCATION" \ + --os-vhd-storage-account "$STORAGE_ACCOUNT" \ + --os-vhd-uri "$BLOB_URI" \ + --tags "tee=${TEE_PLATFORM}" "image_name=${IMAGE_NAME}" "caa_commit=${{ steps.pull.outputs.caa_commit }}" + + echo "Cleaning up staging VHD blob" + az storage blob delete \ + --account-name "$STORAGE_ACCOUNT" \ + --container-name "$BLOB_CONTAINER" \ + --name "$BLOB_NAME" \ + --auth-mode login || true + + { + echo "### PodVM Azure gallery image published" + echo "" + echo "| Field | Value |" + echo "| --- | --- |" + echo "| Gallery | \`${GALLERY}\` |" + echo "| Definition | \`${IMAGE_DEF}\` |" + echo "| Version | \`${IMAGE_VERSION}\` |" + echo "| Security type | ConfidentialVmSupported |" + echo "| TEE | ${TEE_PLATFORM} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/deploy-gcp-cohere.yaml b/.github/workflows/deploy-gcp-cohere.yaml index 243734f522..3aa4ae38ae 100644 --- a/.github/workflows/deploy-gcp-cohere.yaml +++ b/.github/workflows/deploy-gcp-cohere.yaml @@ -27,6 +27,7 @@ permissions: attestations: read # gh attestation verify env: + # Fallback; overridden per-image from measurements.json in the deploy job. TEE_PLATFORM: tdx OCI_IMAGE: ghcr.io/${{ github.repository }}/podvm PODVM_GCP_IMAGE_PROJECT: cohere-artifacts @@ -121,6 +122,7 @@ jobs: echo "rtmr2=$(jq -r '.rtmr2_sha384 // ""' measurements.json)" echo "kernel=$(jq -r '.kernel // ""' measurements.json)" echo "build_date=$(jq -r '.build_date // ""' measurements.json)" + echo "tee_platform=$(jq -r '.tee_platform // "tdx"' measurements.json)" } >> "$GITHUB_OUTPUT" - name: Authenticate to GCP @@ -162,6 +164,8 @@ jobs: GCS_URI: ${{ steps.upload.outputs.gcs_uri }} GCP_PROJECT: ${{ env.PODVM_GCP_IMAGE_PROJECT }} IMAGE_TAG: ${{ inputs.image_tag }} + # From the image's measurements.json; drives guest-os-features below. + TEE_PLATFORM: ${{ steps.pull.outputs.tee_platform }} run: | # Names are content-addressed (class + version|date+sha, plus run # attempt on reruns) so collisions mean the workflow logic regressed. @@ -186,6 +190,19 @@ jobs: --project="$GCP_PROJECT" \ --description="podvm import (intermediate): distro=${DISTRO} tee=${TEE_PLATFORM} caa=${CAA_VERSION} commit=${CAA_COMMIT}" + # guest-os-features must match the image's attesters; cc-* gets both. + CC_FEATURES="" + case "$TEE_PLATFORM" in + *cc*) CC_FEATURES="TDX_CAPABLE,SEV_SNP_CAPABLE" ;; + *snp*) CC_FEATURES="SEV_SNP_CAPABLE" ;; + *tdx*) CC_FEATURES="TDX_CAPABLE" ;; + *) + echo "ERROR: cannot derive guest-os-features from tee_platform='$TEE_PLATFORM'" >&2 + exit 1 + ;; + esac + GUEST_OS_FEATURES="${CC_FEATURES},UEFI_COMPATIBLE,GVNIC" + sanitize_label() { echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g' | cut -c1-63; } LABELS="class=${IMAGE_CLASS}" LABELS="${LABELS},ttl=${IMAGE_TTL}" @@ -204,7 +221,7 @@ jobs: --family="$IMAGE_FAMILY" \ --description="podvm: distro=${DISTRO} tee=${TEE_PLATFORM} profile=${PROFILE} class=${IMAGE_CLASS} caa=${CAA_VERSION} commit=${CAA_COMMIT} rtmr2=${RTMR2} oci=${OCI_IMAGE}:${IMAGE_TAG}" \ --labels="$LABELS" \ - --guest-os-features=TDX_CAPABLE,UEFI_COMPATIBLE,GVNIC + --guest-os-features="$GUEST_OS_FEATURES" gcloud compute images delete "$RAW_IMAGE" \ --project="$GCP_PROJECT" --quiet diff --git a/src/cloud-api-adaptor/versions.yaml b/src/cloud-api-adaptor/versions.yaml index 74dc67b774..6d84fde6bf 100644 --- a/src/cloud-api-adaptor/versions.yaml +++ b/src/cloud-api-adaptor/versions.yaml @@ -66,13 +66,17 @@ oci: registry: ghcr.io/kata-containers/cached-artefacts reference: 8dccf4cf37aeea4b6c2caacf3e61510d6eef2f71 # v3.29.0 # Cohere fork of guest-components. The fork's publish-artifacts workflow - # publishes attestation-agent (incl. a tdx-nvidia variant that bundles - # libnvat.so), confidential-data-hub, and api-server-rest to GHCR. - # Bump `reference` to the 40-char SHA whose publish-artifacts run has - # completed successfully (visible on the GHCR Packages page). + # publishes attestation-agent (tdx-nvidia and the universal cc-nvidia + # variant, both bundling libnvat.so), confidential-data-hub, and + # api-server-rest to GHCR. Bump `reference` to the 40-char SHA whose + # publish-artifacts run has completed successfully (GHCR Packages page). + # + # NOTE: this branch pin (66aa03f) is ahead of the live cohere pin and is + # scoped to the cc-nvidia podvm work. It also rebuilds the tdx-nvidia AA, so + # do NOT merge to cohere until the TDX baselines are regenerated (Phase 2). guest-components: registry: ghcr.io/cohere-ai/guest-components - reference: f475b74f94978952804c2ce5205b3aad16076117 + reference: 66aa03f828c1278056fff7f02b9f548f993fe491 kata-agent-ctl: registry: ghcr.io/kata-containers/cached-artefacts reference: 3.29.0