#!/usr/bin/env bash # Create a Flatcar VM for disposable Linux k3d security testing. # Run this on the Proxmox host as root. set -Eeuo pipefail usage() { cat <<'EOF' Usage: VM_ID=9101 \ SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ ./create-flatcar-k3d-test-host.sh Required environment variables: VM_ID Proxmox VM ID that does not already exist SSH_PUBLIC_KEY SSH public key for the Flatcar core user Optional environment variables: VM_NAME VM name, default flatcar-k3d-test CORES CPU cores, default 4 MEMORY_MB Memory in MiB, default 16384 DISK_SIZE Final disk size, default 100G BRIDGE Proxmox bridge, default vmbr0 DISK_STORAGE Storage for the VM disk, default local-lvm CLOUDINIT_STORAGE Storage for the Cloud-Init drive, default local-lvm SNIPPET_STORAGE Proxmox storage containing snippets, default local SNIPPET_DIR Host snippet directory, default /var/lib/vz/snippets IMAGE_CACHE_DIR Host image cache, default /var/lib/vz/template/cache FLATCAR_IMAGE_URL Flatcar Stable amd64 Proxmox image URL FLATCAR_IMAGE_SHA256 Optional expected SHA256 for the image K3D_VERSION k3d version installed on first boot, default v5.9.0 KUBECTL_VERSION kubectl version installed on first boot, default v1.35.5 The VM uses DHCP and is intended to be a dedicated disposable Linux test host. It is not a Gitea runner and does not receive a registration token. EOF } die() { echo "ERROR: $*" >&2 exit 1 } require_command() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" } [[ "${1:-}" != "--help" && "${1:-}" != "-h" ]] || { usage; exit 0; } [[ "$(id -u)" -eq 0 ]] || die "run this script as root on the Proxmox host" for command_name in qm pvesm curl sha256sum jq; do require_command "$command_name" done : "${VM_ID:?VM_ID is required}" : "${SSH_PUBLIC_KEY:?SSH_PUBLIC_KEY is required}" VM_NAME="${VM_NAME:-flatcar-k3d-test}" CORES="${CORES:-4}" MEMORY_MB="${MEMORY_MB:-16384}" DISK_SIZE="${DISK_SIZE:-100G}" BRIDGE="${BRIDGE:-vmbr0}" DISK_STORAGE="${DISK_STORAGE:-local-lvm}" CLOUDINIT_STORAGE="${CLOUDINIT_STORAGE:-local-lvm}" SNIPPET_STORAGE="${SNIPPET_STORAGE:-local}" SNIPPET_DIR="${SNIPPET_DIR:-/var/lib/vz/snippets}" IMAGE_CACHE_DIR="${IMAGE_CACHE_DIR:-/var/lib/vz/template/cache}" FLATCAR_IMAGE_URL="${FLATCAR_IMAGE_URL:-https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_proxmoxve_image.img}" K3D_VERSION="${K3D_VERSION:-v5.9.0}" KUBECTL_VERSION="${KUBECTL_VERSION:-v1.35.5}" [[ "$VM_ID" =~ ^[0-9]+$ ]] || die "VM_ID must be numeric" [[ "$CORES" =~ ^[0-9]+$ ]] || die "CORES must be numeric" [[ "$MEMORY_MB" =~ ^[0-9]+$ ]] || die "MEMORY_MB must be numeric" if qm status "$VM_ID" >/dev/null 2>&1; then die "VM $VM_ID already exists; choose another VM_ID" fi snippet_storages="$(pvesm status --content snippets 2>/dev/null || true)" awk -v storage="$SNIPPET_STORAGE" '$1 == storage {found = 1} END {exit !found}' <<<"$snippet_storages" || die "storage $SNIPPET_STORAGE is not configured for snippets" mkdir -p "$SNIPPET_DIR" "$IMAGE_CACHE_DIR" chmod 0750 "$SNIPPET_DIR" work_dir="$(mktemp -d /tmp/flatcar-k3d.XXXXXX)" cleanup() { rm -rf "$work_dir"; } trap cleanup EXIT image_path="$IMAGE_CACHE_DIR/$(basename "$FLATCAR_IMAGE_URL")" snippet_name="flatcar-k3d-test-${VM_ID}-user-data" snippet_path="$SNIPPET_DIR/$snippet_name" if [[ -s "$image_path" ]]; then echo "Using cached Flatcar image: $image_path" else echo "Downloading Flatcar image: $FLATCAR_IMAGE_URL" curl --fail --location --retry 3 --output "$image_path.partial" "$FLATCAR_IMAGE_URL" mv "$image_path.partial" "$image_path" fi if [[ -n "${FLATCAR_IMAGE_SHA256:-}" ]]; then printf '%s %s\n' "$FLATCAR_IMAGE_SHA256" "$image_path" | sha256sum --check --strict - fi bootstrap="$work_dir/bootstrap-k3d-test-host.sh" cat >"$bootstrap" </home/core/.profile <<'PROFILE' export PATH="/home/core/bin:/opt/k3d-test/bin:$PATH" PROFILE chown -R core:core /home/core/bin /home/core/.profile touch /opt/k3d-test/.tools-installed EOF bootstrap_unit="$work_dir/k3d-test-tools.service" cat >"$bootstrap_unit" <<'EOF' [Unit] Description=Install k3d test host tooling After=network-online.target docker.service Wants=network-online.target Requires=docker.service ConditionPathExists=!/opt/k3d-test/.tools-installed [Service] Type=oneshot ExecStart=/opt/k3d-test/bootstrap-k3d-test-host.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF jq -n \ --arg ssh_key "$SSH_PUBLIC_KEY" \ --rawfile bootstrap "$bootstrap" \ --rawfile bootstrap_unit "$bootstrap_unit" \ '{ ignition: {version: "3.3.0"}, storage: { directories: [ {path: "/opt/k3d-test", mode: 493}, {path: "/opt/k3d-test/bin", mode: 493}, {path: "/opt/k3d-test/workspace", mode: 493} ], files: [ {path: "/opt/k3d-test/bootstrap-k3d-test-host.sh", mode: 365, contents: {source: ("data:text/plain;charset=utf-8," + ($bootstrap | @uri))}} ] }, systemd: { units: [ {name: "docker.service", enabled: true}, {name: "k3d-test-tools.service", enabled: true, contents: $bootstrap_unit} ] }, passwd: {users: [{name: "core", sshAuthorizedKeys: [$ssh_key]}]} }' >"$snippet_path" chmod 0600 "$snippet_path" echo "Creating Proxmox VM $VM_ID" qm create "$VM_ID" \ --name "$VM_NAME" \ --ostype l26 \ --cores "$CORES" \ --memory "$MEMORY_MB" \ --net0 "virtio,bridge=$BRIDGE" \ --ipconfig0 ip=dhcp \ --agent enabled=1 \ --onboot 1 \ --scsihw virtio-scsi-single qm disk import "$VM_ID" "$image_path" "$DISK_STORAGE" qm set "$VM_ID" --scsi0 "$DISK_STORAGE:vm-${VM_ID}-disk-0" qm resize "$VM_ID" scsi0 "$DISK_SIZE" qm set "$VM_ID" --boot order=scsi0 qm set "$VM_ID" --ide2 "$CLOUDINIT_STORAGE:cloudinit" qm set "$VM_ID" --cicustom "user=${SNIPPET_STORAGE}:snippets/${snippet_name}" qm start "$VM_ID" cat < 'systemctl status docker k3d-test-tools --no-pager' ssh core@ 'k3d version && kubectl version --client' The test host has no Gitea token and is separate from the CI runner. EOF