diff --git a/.github/workflows/cleanup-images.yml b/.github/workflows/cleanup-images.yml index 2510329..2aaf3c7 100644 --- a/.github/workflows/cleanup-images.yml +++ b/.github/workflows/cleanup-images.yml @@ -29,6 +29,18 @@ on: description: "🗑️ Clean up AWS AMIs (deregister + delete snapshots)" default: true type: boolean + cleanup_gce: + description: "🗑️ Clean up GCE images and GCS tarballs" + default: false + type: boolean + gcp_project: + description: "GCP project ID for GCE cleanup" + type: string + default: "pelagic-logic-394811" + gcs_bucket: + description: "GCS bucket for GCE tarball cleanup" + type: string + default: "ubicloud-gce-images" aws_ami_regions: description: "AWS regions to clean up AMIs from (comma-separated)" type: string @@ -287,6 +299,63 @@ jobs: fi done + - name: Authenticate to GCP + if: ${{ inputs.cleanup_gce }} + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Set up Cloud SDK + if: ${{ inputs.cleanup_gce }} + uses: google-github-actions/setup-gcloud@v2 + + - name: Cleanup GCE images + if: ${{ inputs.cleanup_gce }} + run: | + project="${{ inputs.gcp_project }}" + bucket="${{ inputs.gcs_bucket }}" + + echo "### GCE Cleanup" >> $GITHUB_STEP_SUMMARY + + cleanup_gce_image() { + local image_name=$1 + local arch=$2 + + if gcloud compute images describe "${image_name}" --project="${project}" &>/dev/null; then + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "[DRY RUN] Would delete GCE image: ${image_name}" + echo "- [DRY RUN] Would delete ${arch} image: ${image_name}" >> $GITHUB_STEP_SUMMARY + else + gcloud compute images delete "${image_name}" --project="${project}" --quiet + echo "Deleted GCE image: ${image_name}" + echo "- Deleted ${arch} image: ${image_name}" >> $GITHUB_STEP_SUMMARY + fi + else + echo "GCE image not found: ${image_name}" + echo "- ${arch} image not found: ${image_name}" >> $GITHUB_STEP_SUMMARY + fi + + local tar_file="${image_name}.tar.gz" + if gcloud storage ls "gs://${bucket}/${tar_file}" &>/dev/null; then + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "[DRY RUN] Would delete tarball: gs://${bucket}/${tar_file}" + echo "- [DRY RUN] Would delete ${arch} tarball: ${tar_file}" >> $GITHUB_STEP_SUMMARY + else + gcloud storage rm "gs://${bucket}/${tar_file}" + echo "Deleted tarball: gs://${bucket}/${tar_file}" + echo "- Deleted ${arch} tarball: ${tar_file}" >> $GITHUB_STEP_SUMMARY + fi + fi + } + + if [ "${{ inputs.architecture }}" = "x64" ] || [ "${{ inputs.architecture }}" = "both" ]; then + cleanup_gce_image "${{ steps.set_image_names.outputs.x64_image_name }}" "x64" + fi + + if [ "${{ inputs.architecture }}" = "arm64" ] || [ "${{ inputs.architecture }}" = "both" ]; then + cleanup_gce_image "${{ steps.set_image_names.outputs.arm64_image_name }}" "arm64" + fi + - name: Summary run: | echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/postgres-vm-image.yml b/.github/workflows/postgres-vm-image.yml index 7295f15..2cfc7a7 100644 --- a/.github/workflows/postgres-vm-image.yml +++ b/.github/workflows/postgres-vm-image.yml @@ -37,6 +37,10 @@ on: description: "📤 Upload to R2 (ignored if build_only)" default: false type: boolean + upload_gce: + description: "📤 Create GCE image" + default: false + type: boolean upload_aws_ami: description: "📤 Create AWS AMI" default: false @@ -72,6 +76,8 @@ jobs: sha256: ${{ steps.compute_sha.outputs.sha256 }} all_ami_ids: ${{ steps.copy_ami.outputs.all_ami_ids }} source_ami_id: ${{ steps.register_ami.outputs.ami_id }} + gce_image_name: ${{ steps.create_gce_image.outputs.image_name }} + gce_image_project: ${{ steps.create_gce_image.outputs.image_project }} steps: - name: Print inputs run: | @@ -492,6 +498,114 @@ jobs: echo "Cleaning up S3..." aws s3 rm s3://${{ steps.s3_upload.outputs.s3_bucket }}/${{ steps.set_image_name.outputs.S3_BUCKET_IMAGE_PREFIX }}/${{ steps.s3_upload.outputs.image_filename }} + # === GCE Image Steps === + - name: GCE post-processing + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + image_filename=${{ steps.set_image_name.outputs.MINIO_IMAGE_NAME }}.raw + cp "${image_filename}" postgres-x64-gce-work.raw + sudo ./gce-postprocess.sh postgres-x64-gce-work.raw + rm -f postgres-x64-gce-work.raw + + - name: Set GCE image name + if: ${{ inputs.upload_gce && !inputs.build_only }} + id: set_gce_image_name + run: | + # GCE image names allow only [a-z0-9-]; image_suffix may contain + # dots (e.g. "20260428.1.0"), so swap them for hyphens. + suffix="${{ inputs.image_suffix }}" + gce_image_name="postgres-ubuntu-2204-x64-${suffix//./-}" + echo "gce_image_name=${gce_image_name}" >> $GITHUB_OUTPUT + echo "GCE Image name: ${gce_image_name}" + + - name: Rename GCE tar.gz and compute SHA256 + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + mv postgres-x64-gce-image.tar.gz "${gce_image_name}.tar.gz" + sha256sum "${gce_image_name}.tar.gz" > "${gce_image_name}.tar.gz.sha256" + + echo "### GCE Image (x64)" >> $GITHUB_STEP_SUMMARY + du -h "${gce_image_name}.tar.gz" >> $GITHUB_STEP_SUMMARY + echo "### GCE SHA256" >> $GITHUB_STEP_SUMMARY + cat "${gce_image_name}.tar.gz.sha256" >> $GITHUB_STEP_SUMMARY + + - name: Authenticate to GCP + if: ${{ inputs.upload_gce && !inputs.build_only }} + id: gcp_auth + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Set up Cloud SDK + if: ${{ inputs.upload_gce && !inputs.build_only }} + uses: google-github-actions/setup-gcloud@v2 + + - name: Upload to GCS + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + tar_file="${gce_image_name}.tar.gz" + bucket="${{ secrets.GCS_BUCKET }}" + + echo "Uploading ${tar_file} to gs://${bucket}/..." + gcloud storage cp "${tar_file}" "gs://${bucket}/${tar_file}" + + echo "### GCS Upload (x64)" >> $GITHUB_STEP_SUMMARY + echo "Uploaded to gs://${bucket}/${tar_file}" >> $GITHUB_STEP_SUMMARY + + - name: Create GCE image + if: ${{ inputs.upload_gce && !inputs.build_only }} + id: create_gce_image + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + tar_file="${gce_image_name}.tar.gz" + bucket="${{ secrets.GCS_BUCKET }}" + project="${{ steps.gcp_auth.outputs.project_id }}" + commit_sha="${{ github.sha }}" + + echo "Creating GCE image: ${gce_image_name}" + gcloud compute images create "${gce_image_name}" \ + --project="${project}" \ + --source-uri="gs://${bucket}/${tar_file}" \ + --guest-os-features=VIRTIO_SCSI_MULTIQUEUE,GVNIC \ + --labels="source=postgres-vm-images,commit=${commit_sha:0:8},arch=x64" + + echo "image_name=${gce_image_name}" >> $GITHUB_OUTPUT + echo "image_project=${project}" >> $GITHUB_OUTPUT + + echo "### GCE Image Created (x64)" >> $GITHUB_STEP_SUMMARY + echo "- Name: ${gce_image_name}" >> $GITHUB_STEP_SUMMARY + echo "- Project: ${project}" >> $GITHUB_STEP_SUMMARY + echo "- Commit: ${commit_sha:0:8}" >> $GITHUB_STEP_SUMMARY + + - name: Make GCE image public + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + project="${{ steps.gcp_auth.outputs.project_id }}" + gcloud compute images add-iam-policy-binding "${gce_image_name}" \ + --project="${project}" \ + --member="allAuthenticatedUsers" \ + --role="roles/compute.imageUser" + + - name: Verify GCE image + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + project="${{ steps.gcp_auth.outputs.project_id }}" + gcloud compute images describe "${gce_image_name}" \ + --project="${project}" \ + --format="table(name,family,status,diskSizeGb,creationTimestamp)" + + - name: Clean up GCS tar.gz + if: ${{ inputs.upload_gce && !inputs.build_only }} + continue-on-error: true + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + bucket="${{ secrets.GCS_BUCKET }}" + gcloud storage rm "gs://${bucket}/${gce_image_name}.tar.gz" + # arm64 build build-arm64: name: Build postgres-ubuntu-2204-arm64-${{ inputs.image_suffix }} @@ -501,6 +615,8 @@ jobs: sha256: ${{ steps.compute_sha.outputs.sha256 }} all_ami_ids: ${{ steps.copy_ami.outputs.all_ami_ids }} source_ami_id: ${{ steps.register_ami.outputs.ami_id }} + gce_image_name: ${{ steps.create_gce_image.outputs.image_name }} + gce_image_project: ${{ steps.create_gce_image.outputs.image_project }} steps: - name: Print inputs run: | @@ -872,6 +988,115 @@ jobs: echo "Cleaning up S3..." aws s3 rm s3://${{ steps.s3_upload.outputs.s3_bucket }}/${{ steps.set_image_name.outputs.S3_BUCKET_IMAGE_PREFIX }}/${{ steps.s3_upload.outputs.image_filename }} + # === GCE Image Steps === + - name: GCE post-processing + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + image_filename=${{ steps.set_image_name.outputs.IMAGE_NAME }}.raw + cp "${image_filename}" postgres-arm64-gce-work.raw + sudo ./gce-postprocess.sh postgres-arm64-gce-work.raw + rm -f postgres-arm64-gce-work.raw + + - name: Set GCE image name + if: ${{ inputs.upload_gce && !inputs.build_only }} + id: set_gce_image_name + run: | + # GCE image names allow only [a-z0-9-]; image_suffix may contain + # dots (e.g. "20260428.1.0"), so swap them for hyphens. + suffix="${{ inputs.image_suffix }}" + gce_image_name="postgres-ubuntu-2204-arm64-${suffix//./-}" + echo "gce_image_name=${gce_image_name}" >> $GITHUB_OUTPUT + echo "GCE Image name: ${gce_image_name}" + + - name: Rename GCE tar.gz and compute SHA256 + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + mv postgres-arm64-gce-image.tar.gz "${gce_image_name}.tar.gz" + sha256sum "${gce_image_name}.tar.gz" > "${gce_image_name}.tar.gz.sha256" + + echo "### GCE Image (arm64)" >> $GITHUB_STEP_SUMMARY + du -h "${gce_image_name}.tar.gz" >> $GITHUB_STEP_SUMMARY + echo "### GCE SHA256" >> $GITHUB_STEP_SUMMARY + cat "${gce_image_name}.tar.gz.sha256" >> $GITHUB_STEP_SUMMARY + + - name: Authenticate to GCP + if: ${{ inputs.upload_gce && !inputs.build_only }} + id: gcp_auth + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Set up Cloud SDK + if: ${{ inputs.upload_gce && !inputs.build_only }} + uses: google-github-actions/setup-gcloud@v2 + + - name: Upload to GCS + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + tar_file="${gce_image_name}.tar.gz" + bucket="${{ secrets.GCS_BUCKET }}" + + echo "Uploading ${tar_file} to gs://${bucket}/..." + gcloud storage cp "${tar_file}" "gs://${bucket}/${tar_file}" + + echo "### GCS Upload (arm64)" >> $GITHUB_STEP_SUMMARY + echo "Uploaded to gs://${bucket}/${tar_file}" >> $GITHUB_STEP_SUMMARY + + - name: Create GCE image + if: ${{ inputs.upload_gce && !inputs.build_only }} + id: create_gce_image + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + tar_file="${gce_image_name}.tar.gz" + bucket="${{ secrets.GCS_BUCKET }}" + project="${{ steps.gcp_auth.outputs.project_id }}" + commit_sha="${{ github.sha }}" + + echo "Creating GCE image: ${gce_image_name}" + gcloud compute images create "${gce_image_name}" \ + --project="${project}" \ + --source-uri="gs://${bucket}/${tar_file}" \ + --guest-os-features=GVNIC,UEFI_COMPATIBLE \ + --architecture=ARM64 \ + --labels="source=postgres-vm-images,commit=${commit_sha:0:8},arch=arm64" + + echo "image_name=${gce_image_name}" >> $GITHUB_OUTPUT + echo "image_project=${project}" >> $GITHUB_OUTPUT + + echo "### GCE Image Created (arm64)" >> $GITHUB_STEP_SUMMARY + echo "- Name: ${gce_image_name}" >> $GITHUB_STEP_SUMMARY + echo "- Project: ${project}" >> $GITHUB_STEP_SUMMARY + echo "- Commit: ${commit_sha:0:8}" >> $GITHUB_STEP_SUMMARY + + - name: Make GCE image public + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + project="${{ steps.gcp_auth.outputs.project_id }}" + gcloud compute images add-iam-policy-binding "${gce_image_name}" \ + --project="${project}" \ + --member="allAuthenticatedUsers" \ + --role="roles/compute.imageUser" + + - name: Verify GCE image + if: ${{ inputs.upload_gce && !inputs.build_only }} + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + project="${{ steps.gcp_auth.outputs.project_id }}" + gcloud compute images describe "${gce_image_name}" \ + --project="${project}" \ + --format="table(name,family,status,diskSizeGb,creationTimestamp)" + + - name: Clean up GCS tar.gz + if: ${{ inputs.upload_gce && !inputs.build_only }} + continue-on-error: true + run: | + gce_image_name="${{ steps.set_gce_image_name.outputs.gce_image_name }}" + bucket="${{ secrets.GCS_BUCKET }}" + gcloud storage rm "gs://${bucket}/${gce_image_name}.tar.gz" + # Create PR to ubicloud/ubicloud with updated image versions create-ubicloud-pr: name: Create PR to ubicloud/ubicloud @@ -913,6 +1138,11 @@ jobs: arm64_amis="us-west-2:${{ needs.build-arm64.outputs.source_ami_id }}" fi echo "arm64_ami_ids=${arm64_amis}" >> $GITHUB_OUTPUT + + # GCE image names + echo "x64_gce_image_name=${{ needs.build-x64.outputs.gce_image_name }}" >> $GITHUB_OUTPUT + echo "arm64_gce_image_name=${{ needs.build-arm64.outputs.gce_image_name }}" >> $GITHUB_OUTPUT + echo "gce_image_project=${{ needs.build-x64.outputs.gce_image_project }}" >> $GITHUB_OUTPUT fi - name: Clone ubicloud/ubicloud @@ -1037,6 +1267,56 @@ jobs: echo "Created migration file: ${migration_file}" cat "${migration_file}" + - name: Create GCE migration file + if: ${{ steps.collect.outputs.x64_gce_image_name != '' || steps.collect.outputs.arm64_gce_image_name != '' }} + run: | + cd ubicloud + timestamp=$(date +%Y%m%d) + migration_file="migrate/${timestamp}_update_pg_gce_images.rb" + + x64_gce_image="${{ steps.collect.outputs.x64_gce_image_name }}" + arm64_gce_image="${{ steps.collect.outputs.arm64_gce_image_name }}" + project="${{ steps.collect.outputs.gce_image_project }}" + + cat > "${migration_file}" << MIGRATION + # frozen_string_literal: true + + Sequel.migration do + up do + MIGRATION + sed -i 's/^ //' "${migration_file}" + + if [ -n "$x64_gce_image" ]; then + cat >> "${migration_file}" << MIGRATION + from(:pg_gce_image) + .where(gcp_project_id: "${project}", arch: "x64") + .update(gce_image_name: "${x64_gce_image}") + MIGRATION + sed -i 's/^ //' "${migration_file}" + fi + + if [ -n "$arm64_gce_image" ]; then + cat >> "${migration_file}" << MIGRATION + from(:pg_gce_image) + .where(gcp_project_id: "${project}", arch: "arm64") + .update(gce_image_name: "${arm64_gce_image}") + MIGRATION + sed -i 's/^ //' "${migration_file}" + fi + + cat >> "${migration_file}" << MIGRATION + end + + down do + raise Sequel::Error, "irreversible: previous GCE image names unknown" + end + end + MIGRATION + sed -i 's/^ //' "${migration_file}" + + echo "Created GCE migration file: ${migration_file}" + cat "${migration_file}" + - name: Create Pull Request env: GH_TOKEN: ${{ secrets.UBICLOUD_REPO_PAT }} @@ -1080,6 +1360,7 @@ jobs: --body "## Summary - Updates boot image version and SHA256 hashes in \`prog/download_boot_image.rb\` - Adds migration to update AWS AMI IDs in \`pg_aws_ami\` table + - Adds migration to update GCE image names in \`pg_gce_image\` table (if GCE images built) ## Image Version \`${{ inputs.image_suffix }}\` @@ -1087,6 +1368,9 @@ jobs: ## Changes - x64 SHA256: \`${{ steps.collect.outputs.x64_sha256 }}\` - arm64 SHA256: \`${{ steps.collect.outputs.arm64_sha256 }}\` + - GCE x64: \`${{ steps.collect.outputs.x64_gce_image_name }}\` + - GCE arm64: \`${{ steps.collect.outputs.arm64_gce_image_name }}\` + - GCE project: \`${{ steps.collect.outputs.gce_image_project }}\` 🤖 Generated by [postgres-vm-images](https://github.com/ubicloud/postgres-vm-images) workflow" diff --git a/build-gce.sh b/build-gce.sh new file mode 100755 index 0000000..2aef40f --- /dev/null +++ b/build-gce.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -uexo pipefail + +# Usage: ./build-gce.sh [size_gb] +# Builds a GCE-compatible PostgreSQL VM image. +# Runs the standard build (build.sh), then applies GCE-specific +# post-processing (gce-postprocess.sh): +# 1. GRUB reinstall for BIOS boot (virt-resize can corrupt it) +# 2. Google guest agent for metadata/SSH/startup-script support +# 3. Tar.gz packaging for gcloud compute images create + +TARGET_SIZE_GB="${1:-8}" + +HOST_ARCH=$(uname -m) +case $HOST_ARCH in + x86_64) IMAGE_ARCH="x64" ;; + aarch64) IMAGE_ARCH="arm64" ;; + *) echo "Unsupported architecture: $HOST_ARCH"; exit 1 ;; +esac + +# Step 1: Run the standard build +./build.sh "$TARGET_SIZE_GB" + +# Step 2: Apply GCE-specific post-processing +./gce-postprocess.sh "postgres-${IMAGE_ARCH}-image.raw" + +echo "=== GCE build complete ===" +echo "Upload and create GCE image with:" +echo " gcloud storage cp postgres-${IMAGE_ARCH}-gce-image.tar.gz gs://BUCKET/postgres-${IMAGE_ARCH}-gce-image.tar.gz" +if [ "$IMAGE_ARCH" = "arm64" ]; then + echo " gcloud compute images create postgres-ubuntu-2204-${IMAGE_ARCH}-YYYYMMDD \\" + echo " --source-uri=gs://BUCKET/postgres-${IMAGE_ARCH}-gce-image.tar.gz \\" + echo " --family=postgres-ubuntu-2204 \\" + echo " --guest-os-features=GVNIC,UEFI_COMPATIBLE" +else + echo " gcloud compute images create postgres-ubuntu-2204-${IMAGE_ARCH}-YYYYMMDD \\" + echo " --source-uri=gs://BUCKET/postgres-${IMAGE_ARCH}-gce-image.tar.gz \\" + echo " --family=postgres-ubuntu-2204 \\" + echo " --guest-os-features=VIRTIO_SCSI_MULTIQUEUE,GVNIC" +fi diff --git a/common/setup_base.sh b/common/setup_base.sh index 354bcba..d01acea 100644 --- a/common/setup_base.sh +++ b/common/setup_base.sh @@ -12,7 +12,7 @@ apt-get -qq -y satisfy 'openssh-server (>= 1:8.9p1-3ubuntu0.10)' echo "=== [setup_base.sh] Updating kernel ===" # Update to kernel 6.8.0-90-generic (Ubuntu 22.04's latest HWE kernel) -apt-get install -y linux-image-6.8.0-90-generic linux-headers-6.8.0-90-generic linux-tools-6.8.0-90-generic +apt-get install -y linux-image-6.8.0-90-generic linux-headers-6.8.0-90-generic linux-tools-6.8.0-90-generic linux-modules-extra-6.8.0-90-generic echo "=== [setup_base.sh] Installing ruby-bundler ===" apt-get install -y ruby-bundler diff --git a/common/setup_packages.sh b/common/setup_packages.sh index 52790d0..5fb679d 100644 --- a/common/setup_packages.sh +++ b/common/setup_packages.sh @@ -2,6 +2,8 @@ set -uexo pipefail export DEBIAN_FRONTEND=noninteractive +export HOME=/root +export GOPATH=/root/go # Read architecture from build_arch.env source /tmp/build_arch.env diff --git a/gce-postprocess.sh b/gce-postprocess.sh new file mode 100755 index 0000000..5d30040 --- /dev/null +++ b/gce-postprocess.sh @@ -0,0 +1,130 @@ +#!/bin/bash +set -uexo pipefail + +# Usage: ./gce-postprocess.sh +# Applies GCE-specific post-processing to a raw disk image: +# 1. GRUB reinstall for BIOS boot (virt-resize can corrupt it) +# 2. Google guest agent for metadata/SSH/startup-script support +# 3. Tar.gz packaging for gcloud compute images create +# +# The raw image is modified in place. +# Outputs: postgres-{arch}-gce-image.tar.gz in the current directory. + +IMAGE_FILE="${1:?Usage: gce-postprocess.sh }" + +HOST_ARCH=$(uname -m) +case $HOST_ARCH in + x86_64) IMAGE_ARCH="x64" ;; + aarch64) IMAGE_ARCH="arm64" ;; + *) echo "Unsupported architecture: $HOST_ARCH"; exit 1 ;; +esac + +echo "=== GCE post-processing: ${IMAGE_FILE} ===" + +# Step 1: Mount image and apply GCE-specific fixes +LOOP_DEV=$(losetup --find --show "${IMAGE_FILE}") +kpartx -av "${LOOP_DEV}" +sleep 2 + +LOOP_BASE=$(basename "${LOOP_DEV}") +ROOT_PART="" +for part in /dev/mapper/${LOOP_BASE}p*; do + if [ -b "$part" ]; then + FS_TYPE=$(blkid -o value -s TYPE "$part" 2>/dev/null || echo "") + if [ "$FS_TYPE" = "ext4" ]; then + ROOT_PART="$part" + break + fi + fi +done + +if [ -z "$ROOT_PART" ]; then + echo "Error: Could not find ext4 root partition" + exit 1 +fi + +MOUNT_POINT="/mnt/image" +mkdir -p "${MOUNT_POINT}" +mount "${ROOT_PART}" "${MOUNT_POINT}" + +# Set up DNS +mkdir -p "${MOUNT_POINT}/run/systemd/resolve" +cat /etc/resolv.conf > "${MOUNT_POINT}/etc/resolv.conf" || \ + echo "nameserver 8.8.8.8" > "${MOUNT_POINT}/etc/resolv.conf" + +mount --bind /dev "${MOUNT_POINT}/dev" +mount --bind /dev/pts "${MOUNT_POINT}/dev/pts" +mount --bind /proc "${MOUNT_POINT}/proc" +mount --bind /sys "${MOUNT_POINT}/sys" + +# Step 2: Reinstall GRUB (architecture-specific) +if [ "$HOST_ARCH" = "x86_64" ]; then + echo "=== GCE: Reinstalling GRUB for BIOS boot (x86_64) ===" + chroot "${MOUNT_POINT}" /bin/bash -c " + grub-install --target=i386-pc ${LOOP_DEV} + update-grub + " +else + echo "=== GCE: Updating GRUB for EFI boot (arm64) ===" + # ARM64 uses UEFI boot - the EFI partition from the Ubuntu cloud image + # is already correct. Just update grub config. + # Mount the EFI partition if present + EFI_PART="" + for part in /dev/mapper/${LOOP_BASE}p*; do + if [ -b "$part" ]; then + FS_TYPE=$(blkid -o value -s TYPE "$part" 2>/dev/null || echo "") + if [ "$FS_TYPE" = "vfat" ]; then + EFI_PART="$part" + break + fi + fi + done + if [ -n "$EFI_PART" ]; then + mkdir -p "${MOUNT_POINT}/boot/efi" + mount "$EFI_PART" "${MOUNT_POINT}/boot/efi" + chroot "${MOUNT_POINT}" /bin/bash -c "update-grub" + umount "${MOUNT_POINT}/boot/efi" + else + chroot "${MOUNT_POINT}" /bin/bash -c "update-grub" + fi +fi + +# Step 3: Install Google guest agent for metadata processing +echo "=== GCE: Installing Google guest agent ===" +chroot "${MOUNT_POINT}" /bin/bash -c " + set -uexo pipefail + export DEBIAN_FRONTEND=noninteractive + apt-add-repository -y universe + apt-get update -qq + apt-get install -y -qq google-guest-agent google-compute-engine + apt-get clean + rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* +" + +# Step 4: Cleanup +echo "=== GCE: Final cleanup ===" +chroot "${MOUNT_POINT}" /bin/bash -c " + rm -rf /var/lib/cloud + cloud-init clean --logs || true +" +truncate -s 0 "${MOUNT_POINT}/etc/machine-id" + +# Unmount +umount "${MOUNT_POINT}/sys" || true +umount "${MOUNT_POINT}/proc" || true +umount "${MOUNT_POINT}/dev/pts" || true +umount "${MOUNT_POINT}/dev" || true +umount "${MOUNT_POINT}" +kpartx -dv "${LOOP_DEV}" +losetup -d "${LOOP_DEV}" + +# Step 5: Package as tar.gz for GCE +echo "=== GCE: Creating tar.gz for image import ===" +cp "${IMAGE_FILE}" disk.raw +tar -czf "postgres-${IMAGE_ARCH}-gce-image.tar.gz" disk.raw +rm disk.raw + +echo "Final GCE image:" +ls -lh "postgres-${IMAGE_ARCH}-gce-image.tar.gz" + +echo "=== GCE post-processing complete ==="