add Flatcar Proxmox Gitea runner bootstrap
Kube-bench CIS scan / Scan ephemeral K3s cluster (push) Failing after 3m12s
Kube-bench CIS scan / Scan ephemeral K3s cluster (push) Failing after 3m12s
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
# Proxmox Flatcar Gitea runner
|
||||||
|
|
||||||
|
`create-flatcar-gitea-runner.sh` creates a Flatcar VM using the Stable Proxmox
|
||||||
|
image and provisions it with Ignition through Proxmox `cicustom` user-data.
|
||||||
|
The VM uses 4 CPU cores, 16 GiB memory, and a 100 GiB disk by default. It
|
||||||
|
enables Docker and starts the Gitea Actions runner in a persistent container.
|
||||||
|
|
||||||
|
The script follows the Flatcar Proxmox flow: import the image, attach a
|
||||||
|
Cloud-Init drive, place Ignition JSON in the Proxmox snippets directory, and
|
||||||
|
set `qm set --cicustom user=...`. Flatcar uses the same `user-data` path for
|
||||||
|
Ignition and regular cloud-init, so do not combine this snippet with regular
|
||||||
|
cloud-init settings.
|
||||||
|
|
||||||
|
Before running it:
|
||||||
|
|
||||||
|
1. Enable the `snippets` content type on the Proxmox storage used by
|
||||||
|
`SNIPPET_STORAGE`.
|
||||||
|
2. Obtain a repository, organization, or instance Gitea runner registration
|
||||||
|
token.
|
||||||
|
3. Confirm the VM ID is unused and the bridge and storage names are correct.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/proxmox/create-flatcar-gitea-runner.sh
|
||||||
|
|
||||||
|
VM_ID=9100 \
|
||||||
|
GITEA_INSTANCE_URL=https://git.example.com \
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN='replace-with-token' \
|
||||||
|
SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \
|
||||||
|
scripts/proxmox/create-flatcar-gitea-runner.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The registration token is written to a root-only file inside the VM. It is
|
||||||
|
also present in the generated Proxmox snippet, so protect the snippet storage
|
||||||
|
and remove it after the VM is provisioned if recovery from the snippet is not
|
||||||
|
required.
|
||||||
|
|
||||||
|
The runner mounts the Docker socket because Gitea Actions uses Docker to
|
||||||
|
execute workflow jobs. A Docker socket is equivalent to broad host control,
|
||||||
|
so this VM should be dedicated to trusted CI workloads and should not host
|
||||||
|
unrelated services.
|
||||||
|
|
||||||
|
The image URL and optional SHA256 value can be overridden through environment
|
||||||
|
variables. The default URL tracks the Flatcar Stable current amd64 image. Pin
|
||||||
|
`FLATCAR_IMAGE_URL` and set `FLATCAR_IMAGE_SHA256` for a reproducible change
|
||||||
|
controlled deployment.
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Create a Flatcar VM on Proxmox and bootstrap a Docker-based Gitea Actions runner.
|
||||||
|
# Run this on the Proxmox host as root.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
VM_ID=9100 \
|
||||||
|
GITEA_INSTANCE_URL=https://gitea.example.com \
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN='token' \
|
||||||
|
SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \
|
||||||
|
./create-flatcar-gitea-runner.sh
|
||||||
|
|
||||||
|
Required environment variables:
|
||||||
|
VM_ID Proxmox VM ID that does not already exist
|
||||||
|
GITEA_INSTANCE_URL Gitea URL, including scheme
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN Repository, organization, or instance token
|
||||||
|
SSH_PUBLIC_KEY SSH public key for the Flatcar core user
|
||||||
|
|
||||||
|
Optional environment variables:
|
||||||
|
VM_NAME VM name, default flatcar-gitea-runner
|
||||||
|
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
|
||||||
|
FLATCAR_IMAGE_URL Flatcar image URL, default Stable amd64 current image
|
||||||
|
FLATCAR_IMAGE_SHA256 Optional expected SHA256 for the downloaded image
|
||||||
|
IMAGE_CACHE_DIR Host image cache, default /var/lib/vz/template/cache
|
||||||
|
GITEA_RUNNER_NAME Runner name, default flatcar-gitea-runner-VM_ID
|
||||||
|
GITEA_RUNNER_LABELS Runner labels, default ubuntu-latest:docker://node:20-bookworm
|
||||||
|
GITEA_RUNNER_IMAGE Runner image, default docker.io/gitea/act_runner:latest
|
||||||
|
|
||||||
|
The script uses DHCP for the VM network. The generated VM receives an Ignition
|
||||||
|
configuration through Proxmox's user-data snippet mechanism. Do not reuse the
|
||||||
|
snippet file or VM disk without removing the registration token first.
|
||||||
|
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}"
|
||||||
|
: "${GITEA_INSTANCE_URL:?GITEA_INSTANCE_URL is required}"
|
||||||
|
: "${GITEA_RUNNER_REGISTRATION_TOKEN:?GITEA_RUNNER_REGISTRATION_TOKEN is required}"
|
||||||
|
: "${SSH_PUBLIC_KEY:?SSH_PUBLIC_KEY is required}"
|
||||||
|
|
||||||
|
VM_NAME="${VM_NAME:-flatcar-gitea-runner}"
|
||||||
|
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}"
|
||||||
|
FLATCAR_IMAGE_URL="${FLATCAR_IMAGE_URL:-https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_proxmoxve_image.img}"
|
||||||
|
IMAGE_CACHE_DIR="${IMAGE_CACHE_DIR:-/var/lib/vz/template/cache}"
|
||||||
|
GITEA_RUNNER_NAME="${GITEA_RUNNER_NAME:-flatcar-gitea-runner-${VM_ID}}"
|
||||||
|
GITEA_RUNNER_LABELS="${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:20-bookworm}"
|
||||||
|
GITEA_RUNNER_IMAGE="${GITEA_RUNNER_IMAGE:-docker.io/gitea/act_runner:latest}"
|
||||||
|
|
||||||
|
[[ "$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"
|
||||||
|
[[ "$GITEA_INSTANCE_URL" =~ ^https?:// ]] || die "GITEA_INSTANCE_URL must begin with http:// or https://"
|
||||||
|
|
||||||
|
if qm status "$VM_ID" >/dev/null 2>&1; then
|
||||||
|
die "VM $VM_ID already exists; choose another VM_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
storage_config="$(pvesm config "$SNIPPET_STORAGE" 2>/dev/null || true)"
|
||||||
|
grep -Eq '(^|,)snippets(,|$)' <<<"$storage_config" || die "storage $SNIPPET_STORAGE is not configured for snippets. Enable it with: pvesm set $SNIPPET_STORAGE --content snippets"
|
||||||
|
|
||||||
|
mkdir -p "$SNIPPET_DIR" "$IMAGE_CACHE_DIR"
|
||||||
|
chmod 0750 "$SNIPPET_DIR"
|
||||||
|
|
||||||
|
work_dir="$(mktemp -d /tmp/flatcar-runner.XXXXXX)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$work_dir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
image_path="$IMAGE_CACHE_DIR/$(basename "$FLATCAR_IMAGE_URL")"
|
||||||
|
snippet_name="flatcar-gitea-runner-${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
|
||||||
|
echo "Checking Flatcar image SHA256"
|
||||||
|
printf '%s %s\n' "$FLATCAR_IMAGE_SHA256" "$image_path" | sha256sum --check --strict -
|
||||||
|
fi
|
||||||
|
|
||||||
|
runner_env="$work_dir/runner.env"
|
||||||
|
cat >"$runner_env" <<EOF
|
||||||
|
GITEA_INSTANCE_URL=$GITEA_INSTANCE_URL
|
||||||
|
GITEA_RUNNER_REGISTRATION_TOKEN=$GITEA_RUNNER_REGISTRATION_TOKEN
|
||||||
|
GITEA_RUNNER_NAME=$GITEA_RUNNER_NAME
|
||||||
|
GITEA_RUNNER_LABELS=$GITEA_RUNNER_LABELS
|
||||||
|
EOF
|
||||||
|
|
||||||
|
runner_unit="$work_dir/gitea-runner.service"
|
||||||
|
cat >"$runner_unit" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea Actions runner container
|
||||||
|
After=docker.service
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=/var/lib/gitea-runner/runner.env
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force gitea-runner
|
||||||
|
ExecStart=/usr/bin/docker run --name gitea-runner --rm \\
|
||||||
|
--env GITEA_INSTANCE_URL \\
|
||||||
|
--env GITEA_RUNNER_REGISTRATION_TOKEN \\
|
||||||
|
--env GITEA_RUNNER_NAME \\
|
||||||
|
--env GITEA_RUNNER_LABELS \\
|
||||||
|
--env-file /var/lib/gitea-runner/runner.env \\
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \\
|
||||||
|
--volume /var/lib/gitea-runner:/data \\
|
||||||
|
$GITEA_RUNNER_IMAGE
|
||||||
|
ExecStop=/usr/bin/docker stop --time 30 gitea-runner
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg ssh_key "$SSH_PUBLIC_KEY" \
|
||||||
|
--arg hostname "$VM_NAME" \
|
||||||
|
--rawfile runner_env "$runner_env" \
|
||||||
|
--rawfile runner_unit "$runner_unit" \
|
||||||
|
'{
|
||||||
|
ignition: {version: "3.3.0"},
|
||||||
|
storage: {
|
||||||
|
directories: [
|
||||||
|
{path: "/var/lib/gitea-runner", mode: 448}
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
{path: "/etc/hostname", mode: 420, contents: {source: ("data:text/plain;charset=utf-8," + ($hostname | @uri))}},
|
||||||
|
{path: "/var/lib/gitea-runner/runner.env", mode: 384, user: {name: "root"}, group: {name: "root"}, contents: {source: ("data:text/plain;charset=utf-8," + ($runner_env | @uri))}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
systemd: {
|
||||||
|
units: [
|
||||||
|
{name: "docker.service", enabled: true},
|
||||||
|
{name: "gitea-runner.service", enabled: true, contents: $runner_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}"
|
||||||
|
|
||||||
|
echo "Starting VM $VM_ID"
|
||||||
|
qm start "$VM_ID"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Flatcar Gitea runner VM created.
|
||||||
|
|
||||||
|
VM ID: $VM_ID
|
||||||
|
VM name: $VM_NAME
|
||||||
|
Runner name: $GITEA_RUNNER_NAME
|
||||||
|
Image: $FLATCAR_IMAGE_URL
|
||||||
|
Snippet: $snippet_path
|
||||||
|
|
||||||
|
Check the VM console or DHCP lease, then verify:
|
||||||
|
ssh core@<vm-ip> 'systemctl status docker gitea-runner --no-pager'
|
||||||
|
ssh core@<vm-ip> 'docker ps'
|
||||||
|
|
||||||
|
Security note: the runner mounts /var/run/docker.sock so workflow jobs can
|
||||||
|
launch Docker containers. Treat this VM as a dedicated CI trust boundary.
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user