|
| 1 | +--- |
| 2 | +name: Fetch guest VM image |
| 3 | +description: Pull the guest qcow2 and its metadata from the ORAS artifact. |
| 4 | + |
| 5 | +inputs: |
| 6 | + repo: |
| 7 | + description: Artifact repository |
| 8 | + required: false |
| 9 | + default: docker.io/sbates130272/batesste-ci-images-ubuntu-qcow2-gen-ernic-rocjitsu |
| 10 | + tag: |
| 11 | + description: Artifact tag; pin an immutable date-prefixed one in CI |
| 12 | + required: true |
| 13 | + dest: |
| 14 | + description: Directory to unpack into |
| 15 | + required: false |
| 16 | + default: /guest-image |
| 17 | + info_artifact_type: |
| 18 | + description: artifactType of the referrer carrying vm-info.json and the keys |
| 19 | + required: false |
| 20 | + default: application/vnd.batesste.vm-info.v1 |
| 21 | + |
| 22 | +outputs: |
| 23 | + output_dir: |
| 24 | + description: Directory holding the qcow2, keys and vm-info.json |
| 25 | + value: ${{ steps.fetch.outputs.output_dir }} |
| 26 | + |
| 27 | +runs: |
| 28 | + using: composite |
| 29 | + steps: |
| 30 | + - id: fetch |
| 31 | + shell: bash |
| 32 | + env: |
| 33 | + REPO: ${{ inputs.repo }} |
| 34 | + TAG: ${{ inputs.tag }} |
| 35 | + DEST: ${{ inputs.dest }} |
| 36 | + INFO_TYPE: ${{ inputs.info_artifact_type }} |
| 37 | + run: | |
| 38 | + set -euo pipefail |
| 39 | + REF="${REPO}:${TAG}" |
| 40 | +
|
| 41 | + retry() { |
| 42 | + local rc=0 |
| 43 | + for i in 1 2 3; do |
| 44 | + if "$@"; then |
| 45 | + return 0 |
| 46 | + else |
| 47 | + rc=$? |
| 48 | + fi |
| 49 | + echo "Command failed (attempt $i/3): $*" >&2 |
| 50 | + sleep 5 |
| 51 | + done |
| 52 | + return "$rc" |
| 53 | + } |
| 54 | +
|
| 55 | + # oras and zstd ship in the builder image; nothing to install, so |
| 56 | + # no apt-get sits on the path to a guest. |
| 57 | + mkdir -p "${DEST}/output" |
| 58 | + cd "${DEST}/output" |
| 59 | +
|
| 60 | + echo "=== Fetching metadata for ${REF} ===" |
| 61 | + # Referrer first: it is kilobytes, so a bad reference fails in |
| 62 | + # seconds rather than after a 3.7 GB download. |
| 63 | + DIGEST=$(retry oras discover --format json "$REF" \ |
| 64 | + | jq -r --arg t "$INFO_TYPE" \ |
| 65 | + '[.referrers[] | select(.artifactType==$t)] |
| 66 | + | sort_by(.annotations["org.opencontainers.image.created"]) |
| 67 | + | last | .digest') |
| 68 | + if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then |
| 69 | + echo "✗ ${REF} has no ${INFO_TYPE} referrer" |
| 70 | + exit 1 |
| 71 | + fi |
| 72 | + retry oras pull "${REPO}@${DIGEST}" |
| 73 | +
|
| 74 | + # oras does not preserve modes; OpenSSH refuses a group-readable key. |
| 75 | + # Take the key name from the metadata for the same reason the disk |
| 76 | + # name is globbed below: a rename upstream should not abort the job |
| 77 | + # on a hardcoded filename under set -e. |
| 78 | + KEY=$(jq -r '.ssh_keys.private_key_path // ""' vm-info.json) |
| 79 | + KEY=$(basename -- "${KEY:-id_rsa}") |
| 80 | + if [ ! -f "$KEY" ]; then |
| 81 | + echo "✗ referrer carries no private key named ${KEY}" |
| 82 | + ls -la |
| 83 | + exit 1 |
| 84 | + fi |
| 85 | + chmod 600 "$KEY" |
| 86 | +
|
| 87 | + echo "=== Fetching disk for ${REF} ===" |
| 88 | + retry oras pull "$REF" |
| 89 | +
|
| 90 | + # Take the disk name from what actually landed rather than pinning |
| 91 | + # it, so a renamed guest fails here instead of in Read VM info. |
| 92 | + shopt -s nullglob |
| 93 | + ZST=(*.qcow2.zst) |
| 94 | + if [ ${#ZST[@]} -ne 1 ]; then |
| 95 | + echo "✗ expected exactly one *.qcow2.zst, got: ${ZST[*]:-none}" |
| 96 | + ls -la |
| 97 | + exit 1 |
| 98 | + fi |
| 99 | +
|
| 100 | + # --long=27 matches the compressor's window, which zstd otherwise |
| 101 | + # refuses as needing too much memory to decode. --rm releases the |
| 102 | + # 3.7 GB .zst as the 5.75 GB disk lands: the runner does not |
| 103 | + # comfortably hold both alongside a build tree. |
| 104 | + zstd -d --long=27 --rm -q "${ZST[0]}" |
| 105 | +
|
| 106 | + ls -la "${DEST}/output" |
| 107 | + echo "output_dir=${DEST}/output" >> "$GITHUB_OUTPUT" |
0 commit comments