diff --git a/.github/workflows/build-podvm-cohere.yaml b/.github/workflows/build-podvm-cohere.yaml index 4c92d98124..e8759495b1 100644 --- a/.github/workflows/build-podvm-cohere.yaml +++ b/.github/workflows/build-podvm-cohere.yaml @@ -21,6 +21,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 +35,11 @@ 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 + # tdx-snp-nvidia selects the AA OCI tag suffix + # (-tdx-snp-nvidia_x86_64). + # The Cohere fork's variant bundles TDX + SNP + Azure SNP-vTPM + NVIDIA GPU + # attesters and includes libnvat.so* alongside the binary. + TEE_PLATFORM: tdx-snp-nvidia OCI_IMAGE: ghcr.io/${{ github.repository }}/podvm VERSIONS_YAML: src/cloud-api-adaptor/versions.yaml SMOKE_MACHINE_TYPE: a3-highgpu-1g @@ -455,3 +461,25 @@ 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..3f709d70b3 --- /dev/null +++ b/.github/workflows/deploy-azure-cohere.yaml @@ -0,0 +1,254 @@ +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: {} + +env: + OCI_IMAGE: ghcr.io/${{ github.repository }}/podvm + VERSIONS_YAML: src/cloud-api-adaptor/versions.yaml + +jobs: + deploy: + name: Azure + runs-on: ubuntu-latest + # Serialize publishes per image tag: gallery versions are immutable and the + # patch is time-based, so overlapping runs could collide on the same version. + concurrency: + group: deploy-azure-cohere-${{ inputs.image_tag }} + cancel-in-progress: false + 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 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends qemu-utils + 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 + qemu-img --version + + - name: Login to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + 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 + + # Definition 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 whose virtual size is a whole number + # of MiB. mkosi's raw image isn't necessarily 1 MiB-aligned, so round + # it up to the next whole MiB first; qemu-img's force_size then keeps + # that exact aligned size in the fixed VHD footer. Without this, the + # gallery image-version create fails with "unsupported virtual size". + MB=$((1024 * 1024)) + size=$(qemu-img info -f raw --output json disk.raw | jq '.["virtual-size"]') + rounded=$(( ((size + MB - 1) / MB) * MB )) + if [ "$rounded" -ne "$size" ]; then + echo "Rounding raw image $size -> $rounded bytes ($((rounded / MB)) MiB) for Azure alignment" + qemu-img resize -f raw disk.raw "$rounded" + fi + 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 }} + CAA_COMMIT: ${{ steps.pull.outputs.caa_commit }} + 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 definition 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=${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..b849d8fabf 100644 --- a/.github/workflows/deploy-gcp-cohere.yaml +++ b/.github/workflows/deploy-gcp-cohere.yaml @@ -4,7 +4,7 @@ on: workflow_call: inputs: image_tag: - description: "OCI artifact tag in GHCR (e.g. podvm-ubuntu-tdx-nvidia-release-v1-0-0)" + description: "OCI artifact tag in GHCR (e.g. podvm-ubuntu-tdx-snp-nvidia-release-v1-0-0)" required: true type: string secrets: @@ -27,7 +27,6 @@ permissions: attestations: read # gh attestation verify env: - TEE_PLATFORM: tdx OCI_IMAGE: ghcr.io/${{ github.repository }}/podvm PODVM_GCP_IMAGE_PROJECT: cohere-artifacts VERSIONS_YAML: src/cloud-api-adaptor/versions.yaml @@ -118,6 +117,7 @@ jobs: echo "caa_commit_short=$(jq -r '.caa_commit[:7]' measurements.json)" echo "caa_version=$(jq -r '.caa_version' measurements.json)" echo "caa_ref=$(jq -r '.caa_ref' measurements.json)" + echo "tee_platform=$(jq -r '.tee_platform' measurements.json)" echo "rtmr2=$(jq -r '.rtmr2_sha384 // ""' measurements.json)" echo "kernel=$(jq -r '.kernel // ""' measurements.json)" echo "build_date=$(jq -r '.build_date // ""' measurements.json)" @@ -156,6 +156,7 @@ jobs: CAA_COMMIT_SHORT: ${{ steps.pull.outputs.caa_commit_short }} CAA_REF: ${{ steps.pull.outputs.caa_ref }} PROFILE: ${{ steps.pull.outputs.profile }} + TEE_PLATFORM: ${{ steps.pull.outputs.tee_platform }} RTMR2: ${{ steps.pull.outputs.rtmr2 }} KERNEL: ${{ steps.pull.outputs.kernel }} BUILD_DATE: ${{ steps.pull.outputs.build_date }} @@ -174,6 +175,30 @@ jobs: exit 1 fi + if [ -z "$TEE_PLATFORM" ] || [ "$TEE_PLATFORM" = "null" ]; then + echo "ERROR: tee_platform is missing from measurements.json" >&2 + exit 1 + fi + + case "$TEE_PLATFORM" in + tdx-snp-nvidia) + GUEST_OS_FEATURES="TDX_CAPABLE,SEV_CAPABLE,SEV_SNP_CAPABLE,UEFI_COMPATIBLE,GVNIC" + ;; + tdx) + GUEST_OS_FEATURES="TDX_CAPABLE,UEFI_COMPATIBLE,GVNIC" + ;; + snp|amd) + GUEST_OS_FEATURES="SEV_SNP_CAPABLE,UEFI_COMPATIBLE,GVNIC" + ;; + sev) + GUEST_OS_FEATURES="SEV_CAPABLE,UEFI_COMPATIBLE,GVNIC" + ;; + *) + echo "ERROR: unsupported tee_platform '$TEE_PLATFORM'" >&2 + exit 1 + ;; + esac + RAW_IMAGE="${IMAGE_NAME}-import" if gcloud compute images describe "$RAW_IMAGE" --project="$GCP_PROJECT" &>/dev/null; then echo "Deleting stale intermediate image $RAW_IMAGE" @@ -204,7 +229,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/Dockerfile b/src/cloud-api-adaptor/Dockerfile index 6867aa5f04..46441062dc 100644 --- a/src/cloud-api-adaptor/Dockerfile +++ b/src/cloud-api-adaptor/Dockerfile @@ -1,7 +1,7 @@ ARG BUILD_TYPE=dev -# golang:1.25.10 (based on Debian 13 trixie) -ARG BUILDER_BASE=golang@sha256:3965b9511a9bed199f0b4eb146f99c31971a5c0adb3240d9f25e233cbd9e01c8 -# debian:trixie-slim (matches golang:1.25.10 base for library compatibility) +# golang:1.25.11 (based on Debian 13 trixie) +ARG BUILDER_BASE=golang@sha256:f188e8c16ea47a8b22d2bdcf6d9bcd07b63ea7876c199749c07bf31e0ab33bad +# debian:trixie-slim (matches golang:1.25.11 base for library compatibility) # Multi-arch manifest list digest supporting amd64, arm64, ppc64le,, s390x ARG BASE=debian@sha256:109e2c65005bf160609e4ba6acf7783752f8502ad218e298253428690b9eaa4b diff --git a/src/cloud-api-adaptor/docs/addnewprovider.md b/src/cloud-api-adaptor/docs/addnewprovider.md index 6e367b8267..f3934a314d 100644 --- a/src/cloud-api-adaptor/docs/addnewprovider.md +++ b/src/cloud-api-adaptor/docs/addnewprovider.md @@ -219,8 +219,8 @@ go mod tidy ```bash cat > Dockerfile <