Compare commits

..
Author SHA1 Message Date
panxiao81 787614667c 为 Pod Runner 提供 Ubuntu workflow 环境
test / python (pull_request) Successful in 11s
test / shell (pull_request) Successful in 17s
2026-09-16 17:21:41 +00:00
7 changed files with 28 additions and 92 deletions
-31
View File
@@ -1,31 +0,0 @@
name: VM kind smoke
on:
workflow_dispatch:
jobs:
kind:
runs-on: [self-hosted, vm]
steps:
- name: Verify Docker
run: docker info
- name: Install kind
shell: bash
run: |
set -euo pipefail
version=v0.33.0
curl --fail --location --silent --show-error \
--output /tmp/kind "https://kind.sigs.k8s.io/dl/${version}/kind-linux-amd64"
curl --fail --location --silent --show-error \
--output /tmp/kind.sha256sum "https://kind.sigs.k8s.io/dl/${version}/kind-linux-amd64.sha256sum"
printf '%s %s\n' "$(cut -d ' ' -f1 /tmp/kind.sha256sum)" /tmp/kind | sha256sum --check
chmod 0755 /tmp/kind
- name: Create and delete kind cluster
shell: bash
run: |
set -euo pipefail
trap '/tmp/kind delete cluster --name smoke' EXIT
/tmp/kind create cluster --name smoke --wait 180s
/tmp/kind get clusters | grep -Fx smoke
+1 -8
View File
@@ -20,7 +20,7 @@ runs-on: [self-hosted, vm]
- `controller`:接收 Gitea `workflow_job` webhook,将指定 label 的 queued job - `controller`:接收 Gitea `workflow_job` webhook,将指定 label 的 queued job
发布到 NATS JetStream。 发布到 NATS JetStream。
- `worker`:领取任务、限制并发,并通过 Pod 或 microVM backend 创建一次性环境。 - `worker`:领取任务、限制并发,并通过 Pod 或 microVM backend 创建一次性环境。
- `microvm-runner-launch`:为每个任务以 direct I/O 转换出 flat qcow2 root disk、创建 NoCloud seed 和 TAP,运行 - `microvm-runner-launch`:为每个任务创建 COW disk、NoCloud seed 和 TAP,运行
Cloud Hypervisor,退出后完整清理。 Cloud Hypervisor,退出后完整清理。
- `guest-runner`:在 guest 中领取一次性 runner registration token,注册 ephemeral - `guest-runner`:在 guest 中领取一次性 runner registration token,注册 ephemeral
runner,执行一个 job 后关机。 runner,执行一个 job 后关机。
@@ -51,16 +51,9 @@ pytest
- NATS 密码、webhook secret 和 Gitea registration token 只从文件读取。 - NATS 密码、webhook secret 和 Gitea registration token 只从文件读取。
- registration token 不写入 seed image;worker 通过单次 nonce endpoint 交给 guest。 - registration token 不写入 seed image;worker 通过单次 nonce endpoint 交给 guest。
- guest 启动时从仅监听 microVM bridge 的 worker endpoint 获取固定版本 Runner 和配置
资产;基础镜像无需为 Runner 发布而重做。
- `runner-vm-bootstrap.yaml` 暂时只验证 VM 调度和生命周期,不提供 SPIFFE
identity;VM agent attestation 完成前不得将它当作身份链路验证结果。
- guest runner 使用 `--ephemeral`,每台 VM 只执行一个 job。 - guest runner 使用 `--ephemeral`,每台 VM 只执行一个 job。
- launcher 只接受 UUID instance ID 和 URL-safe nonce,所有临时文件都位于独立目录。 - launcher 只接受 UUID instance ID 和 URL-safe nonce,所有临时文件都位于独立目录。
- base image 不得包含 runner identity、registration token、SSH 密码或 host key。 - base image 不得包含 runner identity、registration token、SSH 密码或 host key。
- LXC 内运行 Cloud Hypervisor 必须为 guest memory 启用 `shared=on`。默认的 private
memfd 映射会在 guest 写入后同时产生 shmem 与 anonymous CoW charge,使 LXC cgroup
对 guest RAM 接近双倍计费。
homelab 的 Kubernetes、OpenBao、LXC、bridge 和容量配置保留在 homelab 的 Kubernetes、OpenBao、LXC、bridge 和容量配置保留在
`panxiao81/homelab-infra`。 `panxiao81/homelab-infra`。
-11
View File
@@ -1,11 +0,0 @@
runner:
capacity: 1
timeout: 3h
shutdown_timeout: 1m
host:
workdir_parent: /workspace
container:
require_docker: false
valid_volumes: []
+15 -3
View File
@@ -1,8 +1,20 @@
FROM ghcr.io/spiffe/spire-agent:1.15.3@sha256:41b0dcd8b258a69db9e2768292a060766fb76fd866e4bc925849981ea1b825ff AS spire FROM ghcr.io/spiffe/spire-agent:1.15.3@sha256:41b0dcd8b258a69db9e2768292a060766fb76fd866e4bc925849981ea1b825ff AS spire
FROM docker.io/gitea/runner:3.5.0@sha256:66b7da94dc7dcadb2e076bec6928221336a9a637196399281c4b766fe1288242 FROM docker.io/gitea/runner:3.5.0@sha256:66b7da94dc7dcadb2e076bec6928221336a9a637196399281c4b766fe1288242 AS runner
# The runner daemon image is intentionally minimal and does not contain the
# Node.js runtime required by JavaScript actions such as actions/checkout.
# Run the daemon in Gitea's Ubuntu workflow image so host-mode jobs and their
# actions share a GitHub Actions-compatible userspace.
FROM docker.io/gitea/runner-images:ubuntu-latest@sha256:fd911d7417bfbf0f454530e447da95b58001e1df41bbc5e1a8dd35d432575aae
USER root USER root
COPY --from=runner /usr/local/bin/gitea-runner /usr/local/bin/gitea-runner
COPY --from=runner /usr/local/bin/run.sh /usr/local/bin/run.sh
COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent
COPY config/runner.yaml /etc/gitea-runner/config.yaml COPY config/runner.yaml /etc/gitea-runner/config.yaml
COPY scripts/gitea-job-started /usr/local/libexec/gitea-job-started COPY --chmod=0755 scripts/gitea-job-started /usr/local/libexec/gitea-job-started
RUN chmod 0755 /usr/local/libexec/gitea-job-started
VOLUME ["/data"]
WORKDIR /
ENTRYPOINT ["/usr/local/bin/run.sh"]
+1 -4
View File
@@ -6,8 +6,6 @@ instance=${2:?Gitea instance is required}
runner_name=${3:?runner name is required} runner_name=${3:?runner name is required}
runner_labels=${4:?runner labels are required} runner_labels=${4:?runner labels are required}
token_file=/run/gitea-runner-registration-token token_file=/run/gitea-runner-registration-token
config_file=${GITEA_RUNNER_CONFIG_FILE:-/etc/gitea-runner/config-vm-bootstrap.yaml}
export HOME=${HOME:-/root}
cleanup() { cleanup() {
rm -f -- "$token_file" rm -f -- "$token_file"
@@ -27,5 +25,4 @@ gitea-runner register \
--labels "$runner_labels" \ --labels "$runner_labels" \
--token-file "$token_file" --token-file "$token_file"
rm -f -- "$token_file" rm -f -- "$token_file"
gitea-runner daemon --config "$config_file" --once gitea-runner daemon
echo gitea-runner-job-complete >/dev/ttyS0
+11 -24
View File
@@ -29,13 +29,8 @@ tap="mvr${instance_id%%-*}"
overlay="$vm_dir/root.qcow2" overlay="$vm_dir/root.qcow2"
seed="$vm_dir/seed.img" seed="$vm_dir/seed.img"
serial="$vm_dir/serial.log" serial="$vm_dir/serial.log"
log_dir="$state_root/logs"
cleanup() { cleanup() {
if test -f "$serial"; then
install -d -m 0700 "$log_dir"
cp "$serial" "$log_dir/$instance_id.log"
fi
ip link delete "$tap" 2>/dev/null || true ip link delete "$tap" 2>/dev/null || true
rm -rf -- "$vm_dir" rm -rf -- "$vm_dir"
} }
@@ -44,10 +39,7 @@ trap cleanup EXIT INT TERM
test -r "$base_image" test -r "$base_image"
test -r "$firmware" test -r "$firmware"
install -d -m 0700 "$state_root/instances" "$vm_dir" install -d -m 0700 "$state_root/instances" "$vm_dir"
# Cloud Hypervisor cannot open qcow2 backing chains. Keep each disposable qemu-img create -q -f qcow2 -F qcow2 -b "$base_image" "$overlay"
# root disk flat and bypass the LXC page cache: caching both a 5 GiB copy and
# guest RAM can otherwise trigger the container memory limit.
qemu-img convert -q -T none -t none -f qcow2 -O qcow2 "$base_image" "$overlay"
cat >"$vm_dir/meta-data" <<EOF cat >"$vm_dir/meta-data" <<EOF
instance-id: $instance_id instance-id: $instance_id
@@ -56,8 +48,7 @@ EOF
cat >"$vm_dir/user-data" <<EOF cat >"$vm_dir/user-data" <<EOF
#cloud-config #cloud-config
runcmd: runcmd:
- [ sh, -c, "curl --fail --silent --show-error --retry 10 --retry-all-errors $token_url/assets/guest-assets.tar.gz | tar -xz -C /" ] - [ /usr/local/libexec/gitea-microvm-guest-runner, "$token_url/token/$nonce", "$gitea_instance", "gitea-${instance_id%%-*}", "$runner_labels" ]
- [ /usr/local/libexec/gitea-microvm-guest-runner, "$token_url/token/$nonce", "$gitea_instance", "gitea-vm-${instance_id%%-*}", "$runner_labels" ]
EOF EOF
cloud-localds "$seed" "$vm_dir/user-data" "$vm_dir/meta-data" cloud-localds "$seed" "$vm_dir/user-data" "$vm_dir/meta-data"
@@ -72,16 +63,12 @@ ip tuntap add dev "$tap" mode tap
ip link set "$tap" master "$bridge" ip link set "$tap" master "$bridge"
ip link set "$tap" up ip link set "$tap" up
if timeout --signal=TERM --kill-after=30s "$vm_timeout" "$cloud_hypervisor" \ timeout --signal=TERM --kill-after=30s "$vm_timeout" "$cloud_hypervisor" \
--firmware "$firmware" \ --firmware "$firmware" \
--cpus "boot=$cpus" \ --cpus "boot=$cpus" \
--memory "size=$memory,shared=on" \ --memory "size=$memory" \
--disk "path=$overlay,image_type=qcow2,direct=on,sparse=off" \ --disk "path=$overlay" \
--disk "path=$seed,readonly=on,image_type=raw,direct=on,sparse=off" \ --disk "path=$seed,readonly=on" \
--net "tap=$tap,mac=$mac" \ --net "tap=$tap,mac=$mac" \
--serial "file=$serial" \ --serial "file=$serial" \
--console off; then --console off
grep -Fq gitea-runner-job-complete "$serial"
else
exit $?
fi
-11
View File
@@ -31,9 +31,6 @@ REGISTRATION_TOKEN_FILE = Path(os.environ.get("REGISTRATION_TOKEN_FILE", "/etc/m
LAUNCHER = os.environ.get("LAUNCHER", "/usr/local/libexec/microvm-runner-launch") LAUNCHER = os.environ.get("LAUNCHER", "/usr/local/libexec/microvm-runner-launch")
TOKEN_LISTEN = os.environ.get("TOKEN_LISTEN", "172.30.0.1") TOKEN_LISTEN = os.environ.get("TOKEN_LISTEN", "172.30.0.1")
TOKEN_PORT = int(os.environ.get("TOKEN_PORT", "8787")) TOKEN_PORT = int(os.environ.get("TOKEN_PORT", "8787"))
GUEST_ASSETS = Path(
os.environ.get("GUEST_ASSETS", "/opt/gitea-dynamic-runner/guest-assets.tar.gz")
)
tokens: dict[str, bytes] = {} tokens: dict[str, bytes] = {}
token_lock = asyncio.Lock() token_lock = asyncio.Lock()
@@ -48,13 +45,6 @@ async def token(request: web.Request) -> web.Response:
return web.Response(body=value, headers={"Cache-Control": "no-store"}) return web.Response(body=value, headers={"Cache-Control": "no-store"})
async def guest_assets(_: web.Request) -> web.FileResponse:
return web.FileResponse(
GUEST_ASSETS,
headers={"Cache-Control": "public, immutable"},
)
async def heartbeat(message: object, stop: asyncio.Event) -> None: async def heartbeat(message: object, stop: asyncio.Event) -> None:
while True: while True:
try: try:
@@ -172,7 +162,6 @@ async def consume() -> None:
async def main() -> None: async def main() -> None:
app = web.Application() app = web.Application()
app.router.add_get("/token/{nonce}", token) app.router.add_get("/token/{nonce}", token)
app.router.add_get("/assets/guest-assets.tar.gz", guest_assets)
runner = web.AppRunner(app) runner = web.AppRunner(app)
await runner.setup() await runner.setup()
await web.TCPSite(runner, TOKEN_LISTEN, TOKEN_PORT).start() await web.TCPSite(runner, TOKEN_LISTEN, TOKEN_PORT).start()