Merge pull request 'Make runner installer safe to resume' (#44) from fix/runner-installer-resume into main
CI / tests-and-images (push) Has been cancelled
Runner smoke test / runner-smoke (push) Has been cancelled

Reviewed-on: #44
Reviewed-by: agent <agent@vakanaut.ru>
This commit was merged in pull request #44.
This commit is contained in:
admin committed 2026-10-11 23:22:16 +00:00
commit e6b099cf31
10 files changed
+707 -78

No files matched your search

+5 -2
View File
@@ -32,6 +32,7 @@ jobs:
- name: Check runner config and isolation guards
run: |
bash infra/runner/test-runner-config.sh
bash infra/runner/test-install-runner-idempotency.sh
bash infra/runner/test-install-runner-guard.sh
- name: Check infrastructure shell syntax
run: |
@@ -40,8 +41,10 @@ jobs:
infra/deploy/install-helper.sh infra/deploy/compose-helper.sh infra/deploy/test-compose-helper.sh \
infra/deploy/prepare-production-config.sh infra/deploy/test-production-config-guard.sh \
infra/bootstrap/test-audit-host.sh infra/bootstrap/test-verify-helper.sh \
infra/runner/install-runner.sh infra/runner/act-runner-healthcheck.sh \
infra/runner/test-runner-config.sh infra/runner/test-install-runner-guard.sh
infra/runner/install-runner.sh infra/runner/install-runner-lib.sh \
infra/runner/act-runner-healthcheck.sh infra/runner/test-runner-config.sh \
infra/runner/test-install-runner-idempotency.sh infra/runner/test-install-runner-guard.sh
python3 -c "import ast,pathlib; ast.parse(pathlib.Path('infra/runner/secure-runner-image.py').read_text())"
- name: Run rollback helper tests
run: python3 -m unittest discover -s infra/deploy -p 'test_*.py' -v
- name: Validate production Compose and Caddyfile
+1
View File
@@ -2,6 +2,7 @@
## Не выпущено
- Runner installer защищает image и mountpoint root-owned home-каталогом, оставляет регистрацию и workspace в runner-owned каталоге, безопасно форматирует образ по открытому FD и проверяет `nosuid,nodev`.
- Backend `0.11.3`: ключ подписи поисковых снимков читается из Docker secret-файла, а не из аргументов процесса. Добавлена базовая инфраструктура VPS, Caddy HTTPS proxy с разделёнными Compose-сетями и CI-проверка конфигурации; публикация образов и production-деплой остаются выключены до проверки runner, registry, backup/restore и ограниченного deploy-helper.
- Frontend `0.1.2`: на планшетной ширине экран поиска переходит в одну колонку, чтобы фильтры, результаты и документ помещались в окно.
- Backend `0.11.2`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе.
+50 -15
View File
@@ -16,7 +16,11 @@ before placing it on the host.
CI jobs run in `docker.gitea.com/runner-images:ubuntu-24.04` containers with
capacity 1, per-job limits of 2 CPU, 3 GiB RAM and 512 processes, and a 15 GiB
dedicated ext4 loop-backed Docker data filesystem. The rootless Docker daemon
dedicated ext4 loop-backed Docker data filesystem. The runner account's home
directory is root-owned mode `0711`, so CI cannot rename the image or mountpoint
entries. Docker config remains under `.config`; registration state and the
runner workspace live in `.config/act-runner`, which is runner-owned. The
rootless Docker daemon
and runner user are separate from production Docker; `codex` is not added to
sudo or Docker groups. Jobs do not receive the rootless Docker socket, the
production Docker socket, production environment/secrets, or arbitrary host
@@ -49,16 +53,36 @@ for runner images/workspaces; and configures systemd services. It starts only
the separate rootless Docker daemon; the runner and health timer stay disabled
until repository registration. It does not change
SSH/UFW, production Docker configuration, application data, or production
secrets. It refuses to overwrite existing runner paths and requires at least
25 GiB free on `/` before reserving storage.
secrets. It can be rerun after partial setup: it validates and reuses an
existing runner account, subordinate ID mappings, image, and mount unit. An
existing image must be exactly 15 GiB and contain ext4; a mismatch stops the
installer without resizing or formatting it. The runner account must retain
a non-root system UID below `UID_MIN`, its expected home, primary group, and
sole supplementary group. After validating the account, the installer locks
its home directory to root ownership and mode `0711`; it refuses to rerun while
the runner service is active, so it cannot interrupt a job. The mount
is started only when it is not already active, and an active mount must be the
expected ext4 loop device backed by the runner image with both `nosuid` and
`nodev` active. Runner-home directories and user configuration files are
created in runner-owned subdirectories. Root creates only the first `.config`
entry beneath the already locked home; subsequent directory and config writes
run as `akyldash-runner`. The installer rejects static symlinks and validates
ownership and mode after setup. This also normalizes the `0755` intermediate
directories left by the first installer version. Conflicting files or state
stop the installer for manual inspection. A new image requires at least 25 GiB
free on `/`. Image creation opens each parent component without following
symlinks, creates the image exclusively relative to the opened directory, and
passes the open file descriptor to `mkfs.ext4`; validation checks the same inode
and rejects path replacement. Systemd mounts the image under the root-owned home
entry, which the runner cannot replace after validation.
If the reviewed checkout is not present at the inventoried path, first verify
that the target is absent and clone the reviewed PR branch there. This path is
currently absent per the read-only audit; do not overwrite it if that changes.
that the target is absent and clone the reviewed `main` there. Do not overwrite
an existing checkout.
```bash
sudo test ! -e /opt/akyldash/current
sudo git clone --branch feature/infrastructure-deployment-phase-2 \
sudo git clone --branch main \
--single-branch https://git.vakanaut.ru/admin/akyldash.git \
/opt/akyldash/current
```
@@ -75,7 +99,8 @@ not send the token in chat or save it in the checkout. Register interactively
in a terminal so it is not placed in shell history or process arguments:
```bash
sudo -u akyldash-runner -H /usr/local/bin/act_runner \
sudo -u akyldash-runner env HOME=/var/lib/akyldash-runner/.config/act-runner \
/usr/local/bin/act_runner \
--config /etc/akyldash-runner/config.yaml register
```
@@ -83,7 +108,7 @@ Enter `https://git.vakanaut.ru/`, the token, runner name `akyldash-ci-vps-01`,
and label `akyldash-ci:docker://docker.gitea.com/runner-images:ubuntu-24.04`
when prompted. The config sets the label and capacity; verify them in the
repository runner settings. The token is saved only in the mode-0700 runner
state directory, outside Git. Start/enable the runner after registration:
runtime directory, outside Git. Start/enable the runner after registration:
```bash
sudo systemctl enable --now akyldash-act-runner.service
@@ -114,16 +139,23 @@ that token; do not put it in command-line arguments or environment variables.
Update `runner_version` and its expected official SHA-256 together in
`infra/runner/install-runner.sh`, review upstream release compatibility with
the installed Gitea, and open a PR. From the reviewed checkout, re-run the
installer only after resolving its existing-path guards. Do not replace the
binary while a job is active. Stop the runner, install/verify the new binary,
then start it and confirm Online plus a smoke run. Preserve `/var/lib/akyldash-runner/.runner`.
the installed Gitea, and open a PR. Do not replace the binary while a job is
active. Stop the runner, install/verify the new binary, then start it and
confirm Online plus a smoke run. Preserve
`/var/lib/akyldash-runner/.config/act-runner/.runner`.
## Recovery and rollback
The runner is disposable: production data and secrets are not on its filesystem.
For a runner process failure, inspect `systemctl status` and
`journalctl -u akyldash-act-runner.service`; restart the fixed runner service.
If installation stops after creating or enabling the mount unit, inspect the
image and mount with `stat`, `blkid`, `findmnt`, and `losetup`, then rerun the
reviewed installer only when the image is 15 GiB ext4 and the active mount is
backed by it. The installer recognizes that state and does not format or
restart the mounted filesystem. If size, filesystem, unit contents, or backing
device differ, stop and resolve the state manually; never format the existing
image as a retry.
For lost/corrupt registration state, stop the service and obtain a fresh
repository-level token from Settings → Actions → Runners. Preserve the 15 GiB
data image and state for diagnosis; re-register only after the repository
@@ -133,6 +165,9 @@ automatic recovery step.
## Status
The configuration and scripts are prepared in Git. They have not been
installed on the VPS, a repository-level token is not available to this agent,
and no runner Online state or workflow execution is claimed yet.
The root installer and registration are incomplete. The owner reports that the
15 GiB ext4 loop-backed mount is active on `/dev/loop0` after manually running
`systemctl daemon-reload`, `enable`, and `start` when the first installer run
stopped at mount activation. This PR has not changed the VPS and does not claim
that remaining runner components are installed, a repository token is
available, the runner is Online, or a workflow has run.
+9 -2
View File
@@ -86,8 +86,15 @@ UFW rule details, and protected file metadata remain unverified.
3 GiB RAM, 512 processes), account slice limits (2.5 CPU, 4 GiB RAM, 2048
tasks), rotated file logs, and a systemd health timer. Jobs use the
`akyldash-ci` label and do not receive the rootless or production Docker
socket. This is repository code only: no runner package, account, store,
service, token, or registration is installed on the VPS.
socket. The owner reports a partial runner installation: the 15 GiB ext4
loop-backed store is active on `/dev/loop0` after manually enabling and
starting its mount unit when the installer stopped. Remaining components,
repository registration, and Online state are unverified; this PR makes no
VPS changes. The reviewed installer locks the runner home to root ownership
(`0711`) so CI cannot replace the image or mountpoint, while writable config
and runtime state remain in runner-owned subdirectories. It reuses legacy
`0755` config directories, creates the image through a pinned file descriptor,
rejects path replacement, and requires mount options `nosuid,nodev`.
- Gitea repository runner inventory could not be read by the current API
principal (403). The owner has not supplied runner state, and no runner
Online status or runner-hosted workflow is claimed. The manual
+1 -1
View File
@@ -1,7 +1,7 @@
log:
level: info
runner:
file: /var/lib/akyldash-runner/.runner
file: /var/lib/akyldash-runner/.config/act-runner/.runner
capacity: 1
labels:
- "akyldash-ci:docker://docker.gitea.com/runner-images:ubuntu-24.04"
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
runner_die() { printf 'ERROR: %s\n' "$*" >&2; return 1; }
runner_ensure_account() {
local user=$1 home=$2 entry uid passwd_home shell groups uid_min
if ! entry=$(getent passwd "$user"); then
[[ ! -e $home && ! -L $home ]] || { runner_die "runner home already exists without its account: $home"; return 1; }
useradd --system --create-home --home-dir "$home" --shell /usr/sbin/nologin "$user" || return 1
return
fi
IFS=: read -r _ _ uid _ _ passwd_home shell <<<"$entry"
uid_min=$(awk '$1 == "UID_MIN" && $2 ~ /^[0-9]+$/ {print $2; exit}' /etc/login.defs)
[[ $uid =~ ^[0-9]+$ && $uid -gt 0 && $uid_min =~ ^[0-9]+$ && $uid -lt $uid_min && $passwd_home == "$home" && $shell == /usr/sbin/nologin ]] || { runner_die "existing $user account has an unexpected UID, home, or shell"; return 1; }
[[ $(id -gn "$user") == "$user" ]] || { runner_die "existing $user account has an unexpected primary group"; return 1; }
groups=$(id -nG "$user")
[[ $groups == "$user" ]] || { runner_die "existing $user account has unexpected supplementary groups: $groups"; return 1; }
[[ -d $home && ! -L $home ]] || { runner_die "existing $user home is missing or has unexpected type"; return 1; }
case $(stat -c %U "$home") in
"$user") ;;
root) [[ $(stat -c '%G:%a' "$home") == root:711 ]] || { runner_die "root-owned $user home must be mode 0711"; return 1; } ;;
*) runner_die "existing $user home has unexpected ownership"; return 1 ;;
esac
}
runner_lock_home() {
local home=$1 parent_mode
[[ -d $home && ! -L $home && $(stat -c %U /var/lib) == root ]] || { runner_die "runner home or its parent is unsafe: $home"; return 1; }
parent_mode=$(stat -c %a /var/lib)
(( (8#$parent_mode & 022) == 0 )) || { runner_die '/var/lib must not be group/world-writable'; return 1; }
chown root:root "$home" && chmod 0711 "$home" || return 1
[[ $(stat -c '%U:%G:%a' "$home") == root:root:711 ]] || { runner_die 'runner home could not be locked from runner writes'; return 1; }
}
runner_ensure_image() {
local image=$1 bytes=$2 parent_uid=$3 parent_mode=$4
python3 "$(dirname "${BASH_SOURCE[0]}")/secure-runner-image.py" "$image" "$bytes" "$parent_uid" "$parent_mode" || { runner_die "runner image validation or initialization failed: $image"; return 1; }
}
runner_install_file() {
local source=$1 destination=$2 owner=$3 group=$4 mode=${5#0}
if [[ -e $destination || -L $destination ]]; then
[[ -f $destination && ! -L $destination ]] && cmp -s "$source" "$destination" || { runner_die "existing runner file differs from reviewed configuration: $destination"; return 1; }
[[ $(stat -c '%U:%G:%a' "$destination") == "$owner:$group:$mode" ]] || { runner_die "existing runner file has unexpected ownership or permissions: $destination"; return 1; }
return
fi
install -o "$owner" -g "$group" -m "0$mode" "$source" "$destination"
}
runner_install_content() {
local destination=$1 owner=$2 group=$3 mode=$4 temporary
temporary=$(mktemp)
cat >"$temporary"
if ! runner_install_file "$temporary" "$destination" "$owner" "$group" "$mode"; then
rm -f "$temporary"
return 1
fi
rm -f "$temporary"
}
runner_ensure_private_directory() {
local directory=$1 owner=$2 group=$3 current= component index
local -a components
[[ $directory == /* ]] || { runner_die "runner directory path is not absolute: $directory"; return 1; }
IFS=/ read -r -a components <<<"${directory#/}"
for index in "${!components[@]}"; do
component=${components[index]}
[[ -n $component && $component != . && $component != .. ]] || { runner_die "runner directory path has an unsafe component: $directory"; return 1; }
current+="/$component"
[[ ! -L $current ]] || { runner_die "runner directory path contains a symbolic link: $current"; return 1; }
if [[ ! -e $current ]]; then
((index == ${#components[@]} - 1)) || { runner_die "runner directory parent is missing: $current"; return 1; }
break
fi
[[ -d $current ]] || { runner_die "runner directory path component is not a directory: $current"; return 1; }
if [[ $current == /var/lib/akyldash-runner ]]; then
[[ $(stat -c '%U:%G:%a' "$current") == root:root:711 ]] || { runner_die 'runner home must be root-owned mode 0711'; return 1; }
elif [[ $current == /var/lib/akyldash-runner/* ]]; then
[[ $(stat -c '%U:%G' "$current") == "$owner:$group" ]] || { runner_die "runner directory ancestor has unexpected ownership: $current"; return 1; }
fi
done
# These paths are runner-writable; make changes as that user so a path swap
# cannot turn a root operation into a write outside its home.
if [[ $directory == /var/lib/akyldash-runner/.config ]]; then
install -d -o "$owner" -g "$group" -m 0700 "$directory" || return 1
else
runuser -u "$owner" -- install -d -m 0700 "$directory" || return 1
fi
[[ -d $directory && ! -L $directory && $(stat -c '%U:%G:%a' "$directory") == "$owner:$group:700" ]] || { runner_die "runner directory failed validation: $directory"; return 1; }
}
runner_install_user_content() {
local destination=$1 owner=$2 group=$3 mode=${4#0} temporary
temporary=$(mktemp)
cat >"$temporary"
if [[ -e $destination || -L $destination ]]; then
[[ -f $destination && ! -L $destination ]] && cmp -s "$temporary" "$destination" || { rm -f "$temporary"; runner_die "existing runner file differs from reviewed configuration: $destination"; return 1; }
[[ $(stat -c '%U:%G:%a' "$destination") == "$owner:$group:$mode" ]] || { rm -f "$temporary"; runner_die "existing runner file has unexpected ownership or permissions: $destination"; return 1; }
rm -f "$temporary"
return
fi
chown "$owner:$group" "$temporary" && chmod "0$mode" "$temporary" || { rm -f "$temporary"; return 1; }
local result=0
runuser -u "$owner" -- install -m "0$mode" "$temporary" "$destination" || result=$?
rm -f "$temporary"
((result == 0)) || return 1
[[ -f $destination && ! -L $destination && $(stat -c '%U:%G:%a' "$destination") == "$owner:$group:$mode" ]] || { runner_die "runner file failed validation: $destination"; return 1; }
}
runner_validate_mount() {
local image=$1 store=$2 target filesystem source options backing mount_info
mount_info=$(findmnt --noheadings --output TARGET,FSTYPE,SOURCE,OPTIONS --mountpoint "$store") || { runner_die 'runner store is not mounted'; return 1; }
read -r target filesystem source options <<<"$mount_info" || { runner_die 'runner store mount information is empty'; return 1; }
[[ $target == "$store" && $filesystem == ext4 && $source == /dev/loop* ]] || { runner_die 'runner store is mounted from an unexpected target, filesystem, or device'; return 1; }
case ",$options," in *,nosuid,*nodev,*|*,nodev,*nosuid,*) ;; *) runner_die 'runner store mount is missing nosuid or nodev'; return 1 ;; esac
backing=$(losetup --noheadings --output BACK-FILE "$source") || { runner_die 'cannot identify the runner loop backing file'; return 1; }
[[ $(realpath -e "$backing") == "$(realpath -e "$image")" ]] || { runner_die 'runner store is backed by a different image'; return 1; }
}
runner_ensure_mount() {
local image=$1 store=$2 unit_name=$3 unit_file=$4 unit_tmp mounted=0
[[ -d $store && ! -L $store ]] || { runner_die "runner mount point is missing or is not a directory: $store"; return 1; }
if mountpoint -q "$store"; then
runner_validate_mount "$image" "$store" || return 1
mounted=1
fi
unit_tmp=$(mktemp)
if ! cat >"$unit_tmp" <<EOF
[Unit]
Description=Bounded storage for the AkylDash CI runner
Before=local-fs.target
[Mount]
What=$image
Where=$store
Type=ext4
Options=loop,nosuid,nodev
[Install]
WantedBy=local-fs.target
EOF
then
rm -f "$unit_tmp"
runner_die 'failed to prepare runner mount unit'
return 1
fi
if [[ -e $unit_file || -L $unit_file ]]; then
if [[ ! -f $unit_file || -L $unit_file ]] || ! cmp -s "$unit_tmp" "$unit_file"; then
rm -f "$unit_tmp"
runner_die "existing runner mount unit differs from the reviewed configuration: $unit_file"
return 1
fi
[[ $(stat -c '%u:%g:%a' "$unit_file") == "$(id -u):$(id -g):644" ]] || { rm -f "$unit_tmp"; runner_die 'existing runner mount unit has unexpected ownership or permissions'; return 1; }
else
install -m 0644 "$unit_tmp" "$unit_file" || { rm -f "$unit_tmp"; return 1; }
fi
rm -f "$unit_tmp"
systemctl daemon-reload || return 1
if ((mounted)); then
systemctl enable "$unit_name" || return 1
else
systemctl enable "$unit_name" || return 1
systemctl start "$unit_name" || return 1
runner_validate_mount "$image" "$store" || return 1
fi
}
+102 -56
View File
@@ -3,9 +3,12 @@ set -Eeuo pipefail
readonly runner_user=akyldash-runner
readonly runner_home=/var/lib/akyldash-runner
readonly runner_runtime_home="$runner_home/.config/act-runner"
readonly runner_store="$runner_home/docker-data"
readonly runner_image="$runner_home/docker-store.img"
readonly runner_mount_unit="/etc/systemd/system/$(systemd-escape --path --suffix=mount /var/lib/akyldash-runner/docker-data)"
readonly runner_mount_unit_name="$(systemd-escape --path --suffix=mount /var/lib/akyldash-runner/docker-data)"
readonly runner_mount_unit="/etc/systemd/system/$runner_mount_unit_name"
readonly runner_image_bytes=16106127360
readonly runner_version=0.2.13
readonly runner_sha256=3acac8b506ac8cadc88a55155b5d6378f0fab0b8f62d1e0c0450f4ccd69733e2
readonly runner_url="https://dl.gitea.com/gitea-runner/$runner_version/act_runner-$runner_version-linux-amd64"
@@ -28,70 +31,108 @@ done
unsafe=$(find "$checkout" -xdev \( ! -user root -o -perm /022 \) -print -quit)
[[ -z $unsafe ]] || die 'the repository checkout contains files writable by non-root users'
git -C "$checkout" diff --quiet HEAD -- infra/runner || die 'runner files differ from the reviewed Git revision'
source "$checkout/infra/runner/install-runner-lib.sh"
if systemctl is-active --quiet akyldash-act-runner.service; then
die 'stop akyldash-act-runner.service after all jobs finish before rerunning this installer'
fi
[[ -r /etc/subuid && -r /etc/subgid ]] || die 'subordinate ID allocation files are unavailable'
subid_range=$(awk -f "$(dirname "$0")/find-free-subid-range.awk" /etc/subuid /etc/subgid) || die 'could not find a free 65536-ID range shared by /etc/subuid and /etc/subgid'
if getent passwd "$runner_user" >/dev/null; then
runner_ensure_account "$runner_user" "$runner_home" || die 'runner account validation failed'
runner_lock_home "$runner_home" || die 'runner home protection failed'
else
[[ ! -e $runner_home && ! -L $runner_home ]] || die 'runner home exists without its account; inspect it before continuing'
fi
if [[ -e $runner_image || -L $runner_image ]]; then
runner_ensure_image "$runner_image" "$runner_image_bytes" 0 711 || die 'runner image validation failed'
else
[[ $(df -Pk / | awk 'NR==2 {print $4}') -ge 25000000 ]] || die 'need at least 25 GiB free before reserving 15 GiB for runner data'
fi
for path in "$runner_home" "$runner_image" "$runner_store" /etc/akyldash-runner; do
[[ ! -e $path ]] || die "$path already exists; inspect it and follow the documented recovery procedure"
for file in /etc/subuid /etc/subgid; do
awk -F: '!/^[[:space:]]*#/ && !/^[[:space:]]*$/ && (NF != 3 || $2 !~ /^[0-9]+$/ || $3 !~ /^[0-9]+$/ || $3 < 1) { bad=1 } END { exit bad }' "$file" || die "$file contains an invalid subordinate ID entry"
done
subuid_pair=$(awk -F: -v user="$runner_user" '$1 == user { count++; pair=$2 ":" $3 } END { if (count > 1) exit 2; if (count == 1) print pair }' /etc/subuid) || die 'runner has multiple subuid allocations; inspect them before continuing'
subgid_pair=$(awk -F: -v user="$runner_user" '$1 == user { count++; pair=$2 ":" $3 } END { if (count > 1) exit 2; if (count == 1) print pair }' /etc/subgid) || die 'runner has multiple subgid allocations; inspect them before continuing'
for pair in "$subuid_pair" "$subgid_pair"; do
[[ -z $pair || $pair =~ ^[0-9]+:65536$ ]] || die 'existing runner subordinate ID allocation is not a single 65536-ID range'
done
if [[ -n $subuid_pair && -n $subgid_pair && $subuid_pair != "$subgid_pair" ]]; then
die 'runner subuid and subgid allocations differ; inspect them before continuing'
fi
existing_pair=${subuid_pair:-$subgid_pair}
if [[ -n $existing_pair ]]; then
subid_start=${existing_pair%%:*}
[[ $subid_start =~ ^[1-9][0-9]*$ && $subid_start -ge 100000 && $subid_start -le 4294901760 ]] || die 'existing runner subordinate ID range is outside the supported range'
subid_end=$((subid_start + 65535))
subid_range="$subid_start-$subid_end"
else
subid_range=$(awk -f "$(dirname "$0")/find-free-subid-range.awk" /etc/subuid /etc/subgid) || die 'could not find a free 65536-ID range shared by /etc/subuid and /etc/subgid'
subid_start=${subid_range%-*}
subid_end=${subid_range#*-}
fi
for file in /etc/subuid /etc/subgid; do
awk -F: -v user="$runner_user" -v start="$subid_start" -v end="$subid_end" '$1 != user && $2 <= end && $2 + $3 - 1 >= start { conflict=1 } END { exit conflict }' "$file" || die "runner subordinate ID range conflicts with another entry in $file"
done
! id "$runner_user" >/dev/null 2>&1 || die "user $runner_user already exists; inspect it before continuing"
[[ ! -e /usr/local/bin/act_runner ]] || die '/usr/local/bin/act_runner already exists'
[[ ! -e /etc/systemd/system/akyldash-act-runner.service ]] || die 'runner service already exists'
[[ ! -e /etc/systemd/system/akyldash-runner-healthcheck.service ]] || die 'runner health monitor already exists'
[[ ! -e /etc/logrotate.d/akyldash-runner ]] || die 'runner logrotate policy already exists'
[[ ! -e $runner_mount_unit ]] || die 'runner data mount unit already exists'
[[ $(df -Pk / | awk 'NR==2 {print $4}') -ge 25000000 ]] || die 'need at least 25 GiB free before reserving 15 GiB for runner data'
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y uidmap dbus-user-session curl logrotate docker-ce-rootless-extras
useradd --system --create-home --home-dir "$runner_home" --shell /usr/sbin/nologin "$runner_user"
usermod --add-subuids "$subid_range" --add-subgids "$subid_range" "$runner_user"
runner_ensure_account "$runner_user" "$runner_home" || die 'runner account creation failed'
runner_lock_home "$runner_home" || die 'runner home protection failed'
subid_args=()
[[ -n $subuid_pair ]] || subid_args+=(--add-subuids "$subid_range")
[[ -n $subgid_pair ]] || subid_args+=(--add-subgids "$subid_range")
if ((${#subid_args[@]})); then
usermod "${subid_args[@]}" "$runner_user"
fi
runner_uid=$(id -u "$runner_user")
install -d -o "$runner_user" -g "$runner_user" -m 0700 "$runner_home" "$runner_store" "$runner_home/.config/docker" "$runner_home/.config/systemd/user/docker.service.d"
runner_ensure_private_directory "$runner_home/.config" "$runner_user" "$runner_user" || die 'runner config directory failed validation'
runner_ensure_private_directory "$runner_home/.config/docker" "$runner_user" "$runner_user" || die 'runner Docker config directory failed validation'
runner_ensure_private_directory "$runner_home/.config/systemd" "$runner_user" "$runner_user" || die 'runner systemd config directory failed validation'
runner_ensure_private_directory "$runner_home/.config/systemd/user" "$runner_user" "$runner_user" || die 'runner user systemd directory failed validation'
runner_ensure_private_directory "$runner_home/.config/systemd/user/docker.service.d" "$runner_user" "$runner_user" || die 'runner Docker override directory failed validation'
runner_ensure_private_directory "$runner_runtime_home" "$runner_user" "$runner_user" || die 'runner runtime home failed directory validation'
if [[ -e $runner_home/.runner || -L $runner_home/.runner ]]; then
[[ -f $runner_home/.runner && ! -L $runner_home/.runner ]] || die 'legacy runner registration state is not a regular file'
if [[ ! -e $runner_runtime_home/.runner && ! -L $runner_runtime_home/.runner ]]; then
runner_install_user_content "$runner_runtime_home/.runner" "$runner_user" "$runner_user" 0600 <"$runner_home/.runner" || die 'legacy runner registration state could not be preserved'
fi
fi
if [[ ! -e $runner_store && ! -L $runner_store ]]; then
install -d -o root -g root -m 0755 "$runner_store"
fi
[[ -d $runner_store && ! -L $runner_store ]] || die 'runner mount path is not a directory'
install -d -o root -g "$runner_user" -m 0750 /etc/akyldash-runner
install -d -o "$runner_user" -g "$runner_user" -m 0750 /var/log/akyldash-runner
fallocate -l 15G "$runner_image"
mkfs.ext4 -F -q "$runner_image"
cat >"$runner_mount_unit" <<EOF
[Unit]
Description=Bounded storage for the AkylDash CI runner
Before=local-fs.target
[Mount]
What=$runner_image
Where=$runner_store
Type=ext4
Options=loop,nosuid,nodev
[Install]
WantedBy=local-fs.target
EOF
systemctl daemon-reload
systemctl enable --now "$(basename "$runner_mount_unit" .mount)"
runner_ensure_image "$runner_image" "$runner_image_bytes" 0 711 || die 'runner image initialization failed'
runner_ensure_mount "$runner_image" "$runner_store" "$runner_mount_unit_name" "$runner_mount_unit" || die 'runner mount activation failed'
chown "$runner_user:$runner_user" "$runner_store"
chmod 0700 "$runner_store"
install -o root -g root -m 0755 "$(dirname "$0")/act-runner-healthcheck.sh" /usr/local/sbin/akyldash-runner-healthcheck
install -o root -g "$runner_user" -m 0640 "$(dirname "$0")/config.yaml" /etc/akyldash-runner/config.yaml
tmp_binary=$(mktemp)
trap 'rm -f "$tmp_binary"' EXIT
curl --fail --location --silent --show-error "$runner_url" -o "$tmp_binary"
printf '%s %s\n' "$runner_sha256" "$tmp_binary" | sha256sum --check --status || die 'official act_runner checksum mismatch'
install -o root -g root -m 0755 "$tmp_binary" /usr/local/bin/act_runner
runner_install_file "$(dirname "$0")/act-runner-healthcheck.sh" /usr/local/sbin/akyldash-runner-healthcheck root root 0755 || die 'healthcheck command differs from reviewed configuration'
runner_install_file "$(dirname "$0")/config.yaml" /etc/akyldash-runner/config.yaml root "$runner_user" 0640 || die 'runner config differs from reviewed configuration'
if [[ -e /usr/local/bin/act_runner || -L /usr/local/bin/act_runner ]]; then
[[ -f /usr/local/bin/act_runner && ! -L /usr/local/bin/act_runner ]] || die 'existing act_runner path is not a regular file'
printf '%s %s\n' "$runner_sha256" /usr/local/bin/act_runner | sha256sum --check --status || die 'existing act_runner binary does not match the pinned release'
else
tmp_binary=$(mktemp)
trap 'rm -f "$tmp_binary"' EXIT
curl --fail --location --silent --show-error "$runner_url" -o "$tmp_binary"
printf '%s %s\n' "$runner_sha256" "$tmp_binary" | sha256sum --check --status || die 'official act_runner checksum mismatch'
install -o root -g root -m 0755 "$tmp_binary" /usr/local/bin/act_runner
rm -f "$tmp_binary"
trap - EXIT
fi
cat >"$runner_home/.config/docker/daemon.json" <<EOF
runner_install_user_content "$runner_home/.config/docker/daemon.json" "$runner_user" "$runner_user" 0600 <<EOF
{
"data-root": "$runner_store",
"log-driver": "local",
"log-opts": {"max-size": "10m", "max-file": "2"}
}
EOF
chown "$runner_user:$runner_user" "$runner_home/.config/docker/daemon.json"
chmod 0600 "$runner_home/.config/docker/daemon.json"
cat >"$runner_home/.config/systemd/user/docker.service.d/limits.conf" <<'EOF'
runner_install_user_content "$runner_home/.config/systemd/user/docker.service.d/limits.conf" "$runner_user" "$runner_user" 0644 <<'EOF'
[Service]
Delegate=yes
CPUQuota=200%
@@ -100,9 +141,7 @@ TasksMax=1024
Restart=always
RestartSec=5
EOF
chown -R "$runner_user:$runner_user" "$runner_home/.config"
install -o root -g root -m 0644 /dev/stdin /etc/systemd/system/akyldash-act-runner.service <<EOF
runner_install_content /etc/systemd/system/akyldash-act-runner.service root root 0644 <<EOF
[Unit]
Description=AkylDash repository Gitea Actions runner
After=network-online.target local-fs.target
@@ -114,7 +153,8 @@ Type=simple
Slice=user-$runner_uid.slice
User=$runner_user
Group=$runner_user
Environment=HOME=$runner_home
Environment=HOME=$runner_runtime_home
WorkingDirectory=$runner_runtime_home
Environment=XDG_RUNTIME_DIR=/run/user/$runner_uid
Environment=DOCKER_HOST=unix:///run/user/$runner_uid/docker.sock
ExecStartPre=/bin/sh -c 'for i in \$(seq 1 60); do /usr/bin/docker info >/dev/null 2>&1 && exit 0; sleep 2; done; exit 1'
@@ -132,7 +172,7 @@ ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
RestrictSUIDSGID=yes
ReadWritePaths=$runner_home /var/log/akyldash-runner /run/user/$runner_uid
ReadWritePaths=$runner_runtime_home /var/log/akyldash-runner /run/user/$runner_uid
StandardOutput=append:/var/log/akyldash-runner/act_runner.log
StandardError=append:/var/log/akyldash-runner/act_runner.log
@@ -140,14 +180,14 @@ StandardError=append:/var/log/akyldash-runner/act_runner.log
WantedBy=multi-user.target
EOF
mkdir -p "/etc/systemd/system/user-$runner_uid.slice.d"
cat >"/etc/systemd/system/user-$runner_uid.slice.d/limits.conf" <<EOF
install -d -o root -g root -m 0755 "/etc/systemd/system/user-$runner_uid.slice.d"
runner_install_content "/etc/systemd/system/user-$runner_uid.slice.d/limits.conf" root root 0644 <<EOF
[Slice]
CPUQuota=250%
MemoryMax=4G
TasksMax=2048
EOF
cat >/etc/logrotate.d/akyldash-runner <<'EOF'
runner_install_content /etc/logrotate.d/akyldash-runner root root 0644 <<'EOF'
/var/log/akyldash-runner/act_runner.log {
daily
rotate 7
@@ -158,14 +198,20 @@ cat >/etc/logrotate.d/akyldash-runner <<'EOF'
copytruncate
}
EOF
install -o root -g root -m 0644 "$(dirname "$0")/akyldash-runner-healthcheck.service" /etc/systemd/system/akyldash-runner-healthcheck.service
install -o root -g root -m 0644 "$(dirname "$0")/akyldash-runner-healthcheck.timer" /etc/systemd/system/akyldash-runner-healthcheck.timer
install -o root -g root -m 0644 "$(dirname "$0")/akyldash-runner-restart.service" /etc/systemd/system/akyldash-runner-restart.service
runner_install_file "$(dirname "$0")/akyldash-runner-healthcheck.service" /etc/systemd/system/akyldash-runner-healthcheck.service root root 0644
runner_install_file "$(dirname "$0")/akyldash-runner-healthcheck.timer" /etc/systemd/system/akyldash-runner-healthcheck.timer root root 0644
runner_install_file "$(dirname "$0")/akyldash-runner-restart.service" /etc/systemd/system/akyldash-runner-restart.service root root 0644
loginctl enable-linger "$runner_user"
systemctl daemon-reload
systemctl start "user@$runner_uid.service"
runuser -u "$runner_user" -- env HOME="$runner_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus" dockerd-rootless-setuptool.sh install --force
rootless_docker_unit="$runner_home/.config/systemd/user/docker.service"
if [[ -e $rootless_docker_unit || -L $rootless_docker_unit ]]; then
[[ -f $rootless_docker_unit && ! -L $rootless_docker_unit && $(stat -c %U "$rootless_docker_unit") == "$runner_user" ]] || die 'existing rootless Docker unit has an unexpected type or owner'
grep -Fq 'dockerd-rootless.sh' "$rootless_docker_unit" || die 'existing rootless Docker unit differs from the expected service'
else
runuser -u "$runner_user" -- env HOME="$runner_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus" dockerd-rootless-setuptool.sh install
fi
runuser -u "$runner_user" -- env HOME="$runner_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus" systemctl --user daemon-reload
runuser -u "$runner_user" -- env HOME="$runner_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus" systemctl --user enable --now docker.service
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Create or validate the runner image without following replaceable paths."""
import os
import stat
import subprocess
import sys
def open_directory(path: str) -> int:
current = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
try:
for component in path.strip("/").split("/"):
if not component:
continue
next_fd = os.open(
component,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=current,
)
os.close(current)
current = next_fd
return current
except BaseException:
os.close(current)
raise
def filesystem_type(fd: int) -> str:
result = subprocess.run(
["blkid", "-p", "-s", "TYPE", "-o", "value", f"/proc/self/fd/{fd}"],
check=False,
capture_output=True,
text=True,
pass_fds=(fd,),
)
return result.stdout.strip() if result.returncode == 0 else ""
def main() -> int:
image, bytes_text, parent_uid_text, parent_mode_text = sys.argv[1:]
image = os.path.abspath(image)
parent_path, name = os.path.split(image)
if not name or name in {".", ".."}:
raise ValueError("invalid image filename")
parent_fd = open_directory(parent_path)
image_fd = -1
try:
parent_stat = os.fstat(parent_fd)
if parent_stat.st_uid != int(parent_uid_text) or stat.S_IMODE(parent_stat.st_mode) != int(parent_mode_text, 8):
raise RuntimeError("image parent has unexpected owner or permissions")
created = False
try:
image_fd = os.open(
name,
os.O_RDWR | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=parent_fd,
)
except FileNotFoundError:
image_fd = os.open(
name,
os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC,
0o600,
dir_fd=parent_fd,
)
created = True
image_stat = os.fstat(image_fd)
if not stat.S_ISREG(image_stat.st_mode) or image_stat.st_uid != os.geteuid() or image_stat.st_nlink != 1:
raise RuntimeError("image must be a singly-linked regular file owned by the installer")
if not created and image_stat.st_mode & 0o022:
raise RuntimeError("existing image is writable by group or other users")
if created:
os.posix_fallocate(image_fd, 0, int(bytes_text))
subprocess.run(
["mkfs.ext4", "-F", "-q", f"/proc/self/fd/{image_fd}"],
check=True,
pass_fds=(image_fd,),
)
image_stat = os.fstat(image_fd)
if image_stat.st_size != int(bytes_text):
raise RuntimeError(f"image size is {image_stat.st_size} bytes, expected {bytes_text}; refusing to resize or format")
if filesystem_type(image_fd) != "ext4":
raise RuntimeError("image does not contain ext4; refusing to format")
path_stat = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
if not stat.S_ISREG(path_stat.st_mode) or (path_stat.st_dev, path_stat.st_ino) != (image_stat.st_dev, image_stat.st_ino):
raise RuntimeError("image path changed during validation")
return 0
finally:
if image_fd >= 0:
os.close(image_fd)
os.close(parent_fd)
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, RuntimeError, ValueError, subprocess.CalledProcessError) as error:
print(f"ERROR: {error}", file=sys.stderr)
raise SystemExit(1)
@@ -0,0 +1,251 @@
#!/usr/bin/env bash
set -Eeuo pipefail
runner_dir=$(cd "$(dirname "$0")" && pwd)
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin" "$tmp/store" "$tmp/partial-store"
log=$tmp/commands.log
export RUNNER_TEST_LOG=$log
cat >"$tmp/bin/mkfs.ext4" <<'EOF'
#!/usr/bin/env bash
printf 'mkfs\n' >>"$RUNNER_TEST_LOG"
if [[ -n ${RUNNER_TEST_REPLACE_IMAGE:-} ]]; then
rm -f "$RUNNER_TEST_IMAGE"
ln -s "$RUNNER_TEST_REPLACE_IMAGE" "$RUNNER_TEST_IMAGE"
fi
EOF
cat >"$tmp/bin/blkid" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "${RUNNER_TEST_FS:-ext4}"
EOF
cat >"$tmp/bin/systemctl" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$RUNNER_TEST_LOG"
if [[ $1 == start ]]; then
[[ ${RUNNER_TEST_FAIL_START:-0} == 0 ]] || exit 1
: >"$RUNNER_TEST_MOUNTED"
fi
EOF
cat >"$tmp/bin/mountpoint" <<'EOF'
#!/usr/bin/env bash
[[ -e $RUNNER_TEST_MOUNTED ]]
EOF
cat >"$tmp/bin/findmnt" <<'EOF'
#!/usr/bin/env bash
printf '%s ext4 /dev/loop0 %s\n' "$RUNNER_TEST_STORE" "${RUNNER_TEST_OPTIONS:-rw,nosuid,nodev,relatime}"
EOF
cat >"$tmp/bin/losetup" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$RUNNER_TEST_BACKING"
EOF
chmod +x "$tmp/bin/"*
PATH="$tmp/bin:$PATH"
export PATH
source "$runner_dir/install-runner-lib.sh"
runuser() {
shift 2
[[ $1 == -- ]] || return 2
shift
"$@"
}
getent() {
[[ $1 == passwd ]] || return 2
if [[ -n ${RUNNER_TEST_PASSWD:-} ]]; then
printf '%s\n' "$RUNNER_TEST_PASSWD"
elif [[ -r ${RUNNER_TEST_ACCOUNT_STATE:-} ]]; then
cat "$RUNNER_TEST_ACCOUNT_STATE"
else
return 2
fi
}
id() {
case $1 in
-gn) printf '%s\n' "${RUNNER_TEST_PRIMARY_GROUP:-akyldash-runner}" ;;
-nG) [[ ${TEST_EXTRA_GROUP:-0} == 1 ]] && printf 'akyldash-runner docker\n' || printf 'akyldash-runner\n' ;;
*) command id "$@" ;;
esac
}
stat() {
if [[ $1 == -c && $2 == %U && $3 == ${RUNNER_TEST_HOME:-} ]]; then
printf '%s\n' "${RUNNER_TEST_HOME_OWNER:-akyldash-runner}"
elif [[ $1 == -c && $2 == %G:%a && $3 == ${RUNNER_TEST_HOME:-} ]]; then
printf '%s:%s\n' "${RUNNER_TEST_HOME_GROUP:-root}" "${RUNNER_TEST_HOME_MODE:-711}"
else
command stat "$@"
fi
}
useradd() {
printf 'useradd\n' >>"$RUNNER_TEST_LOG"
local user=${@: -1} home=
while (($#)); do
if [[ $1 == --home-dir ]]; then home=$2; shift 2; else shift; fi
done
mkdir -p "$home"
printf '%s:x:990:990::%s:/usr/sbin/nologin\n' "$user" "$home" >"$RUNNER_TEST_ACCOUNT_STATE"
}
export RUNNER_TEST_HOME="$tmp/account-home"
export RUNNER_TEST_ACCOUNT_STATE="$tmp/account.passwd"
mkdir -p "$RUNNER_TEST_HOME"
export RUNNER_TEST_PASSWD="akyldash-runner:x:990:990::${RUNNER_TEST_HOME}:/usr/sbin/nologin"
runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME"
RUNNER_TEST_HOME_OWNER=root runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME"
if RUNNER_TEST_HOME_OWNER=root RUNNER_TEST_HOME_MODE=755 runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME" 2>/dev/null; then
echo 'Writable root-owned runner home was accepted' >&2
exit 1
fi
TEST_EXTRA_GROUP=1
if runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME" 2>/dev/null; then
echo 'Runner account with extra group membership was accepted' >&2
exit 1
fi
unset TEST_EXTRA_GROUP
if RUNNER_TEST_PASSWD="akyldash-runner:x:990:990::${tmp}/wrong-home:/usr/sbin/nologin" runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME" 2>/dev/null; then
echo 'Runner account with a different home was accepted' >&2
exit 1
fi
if RUNNER_TEST_PASSWD="akyldash-runner:x:0:990::${RUNNER_TEST_HOME}:/usr/sbin/nologin" runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME" 2>/dev/null; then
echo 'Runner account with UID 0 was accepted' >&2
exit 1
fi
if RUNNER_TEST_PASSWD="akyldash-runner:x:1000:990::${RUNNER_TEST_HOME}:/usr/sbin/nologin" runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME" 2>/dev/null; then
echo 'Runner account with a regular-user UID was accepted' >&2
exit 1
fi
RUNNER_TEST_PRIMARY_GROUP=unexpected
if runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME" 2>/dev/null; then
echo 'Runner account with an unexpected primary group was accepted' >&2
exit 1
fi
unset RUNNER_TEST_PRIMARY_GROUP
unset RUNNER_TEST_PASSWD
export RUNNER_TEST_HOME="$tmp/new-account-home"
runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME"
runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME"
[[ $(grep -c '^useradd$' "$log") == 1 ]]
private_root=$tmp/private
mkdir -m 0700 "$private_root"
runner_owner=$(command id -un)
runner_group=$(command id -gn)
runner_ensure_private_directory "$private_root/config" "$runner_owner" "$runner_group"
runner_ensure_private_directory "$private_root/config" "$runner_owner" "$runner_group"
mkdir -m 0700 "$tmp/outside"
ln -s "$tmp/outside" "$private_root/linked"
if runner_ensure_private_directory "$private_root/linked/docker" "$runner_owner" "$runner_group" 2>/dev/null; then
echo 'Runner config symlink was accepted' >&2
exit 1
fi
[[ ! -e $tmp/outside/docker ]]
legacy_root=$tmp/legacy
mkdir -p "$legacy_root/.config/systemd/user/docker.service.d"
chmod 0755 "$legacy_root" "$legacy_root/.config" "$legacy_root/.config/systemd" "$legacy_root/.config/systemd/user" "$legacy_root/.config/systemd/user/docker.service.d"
for legacy_dir in "$legacy_root" "$legacy_root/.config" "$legacy_root/.config/systemd" "$legacy_root/.config/systemd/user" "$legacy_root/.config/systemd/user/docker.service.d"; do
runner_ensure_private_directory "$legacy_dir" "$runner_owner" "$runner_group"
[[ $(stat -c %a "$legacy_dir") == 700 ]]
done
user_config=$private_root/daemon.json
runner_install_user_content "$user_config" "$runner_owner" "$runner_group" 0600 <<'EOF'
{"data-root":"/private/runner"}
EOF
runner_install_user_content "$user_config" "$runner_owner" "$runner_group" 0600 <<'EOF'
{"data-root":"/private/runner"}
EOF
if runner_install_user_content "$user_config" "$runner_owner" "$runner_group" 0600 <<'EOF' 2>/dev/null
{"data-root":"/unexpected"}
EOF
then
echo 'Conflicting runner user config was accepted' >&2
exit 1
fi
image=$tmp/docker-store.img
runner_ensure_image "$image" 1048576 "$(command id -u)" 700
runner_ensure_image "$image" 1048576 "$(command id -u)" 700
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
truncate -s 1048575 "$image"
if runner_ensure_image "$image" 1048576 "$(command id -u)" 700 2>/dev/null; then
echo 'Mismatched image size was accepted' >&2
exit 1
fi
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
truncate -s 1048576 "$image"
if RUNNER_TEST_FS=ext3 runner_ensure_image "$image" 1048576 "$(command id -u)" 700 2>/dev/null; then
echo 'Existing non-ext4 image was accepted' >&2
exit 1
fi
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
external=$tmp/external-file
printf 'preserve this file\n' >"$external"
race_image=$tmp/race.img
ln -s "$external" "$race_image"
if runner_ensure_image "$race_image" 1048576 "$(command id -u)" 700 2>/dev/null; then
echo 'Symlink runner image was accepted' >&2
exit 1
fi
[[ $(cat "$external") == 'preserve this file' ]]
rm -f "$race_image"
race_image=$tmp/race-during-format.img
export RUNNER_TEST_IMAGE="$race_image" RUNNER_TEST_REPLACE_IMAGE="$external"
if runner_ensure_image "$race_image" 1048576 "$(command id -u)" 700 2>/dev/null; then
echo 'Image path replacement during formatting was accepted' >&2
exit 1
fi
[[ $(cat "$external") == 'preserve this file' ]]
rm -f "$race_image"
unset RUNNER_TEST_IMAGE RUNNER_TEST_REPLACE_IMAGE
[[ $(grep -c '^mkfs$' "$log") == 2 ]]
unit_name='var-lib-akyldash\x2drunner-docker\x2ddata.mount'
unit_file=$tmp/systemd/$unit_name
mkdir -p "$(dirname "$unit_file")"
export RUNNER_TEST_STORE="$tmp/store" RUNNER_TEST_BACKING="$image"
export RUNNER_TEST_MOUNTED="$tmp/mounted"
runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$unit_file"
[[ $(grep -c '^start ' "$log") == 1 ]]
grep -Fq "enable $unit_name" "$log"
grep -Fq "start $unit_name" "$log"
# A second run with the image mounted must reuse it and avoid another start.
runner_ensure_image "$image" 1048576 "$(command id -u)" 700
runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$unit_file"
[[ $(grep -c '^mkfs$' "$log") == 2 ]]
[[ $(grep -c '^start ' "$log") == 1 ]]
chmod 0664 "$unit_file"
if runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$unit_file" 2>/dev/null; then
echo 'Writable systemd mount unit was accepted' >&2
exit 1
fi
chmod 0644 "$unit_file"
export RUNNER_TEST_BACKING="$tmp/unrelated.img"
touch "$RUNNER_TEST_BACKING"
if runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$unit_file" 2>/dev/null; then
echo 'Active mount backed by a different image was accepted' >&2
exit 1
fi
[[ $(grep -c '^start ' "$log") == 1 ]]
export RUNNER_TEST_BACKING="$image"
# Simulate interruption after unit installation/enable, before mount activation.
export RUNNER_TEST_STORE="$tmp/partial-store" RUNNER_TEST_MOUNTED="$tmp/partial-mounted"
export RUNNER_TEST_FAIL_START=1
if runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$tmp/systemd/partial.mount" 2>/dev/null; then
echo 'Injected mount interruption was not observed' >&2
exit 1
fi
export RUNNER_TEST_FAIL_START=0
runner_ensure_image "$image" 1048576 "$(command id -u)" 700
runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$tmp/systemd/partial.mount"
[[ $(grep -c '^mkfs$' "$log") == 2 ]]
[[ $(grep -c '^start ' "$log") == 3 ]]
export RUNNER_TEST_STORE="$tmp/store" RUNNER_TEST_MOUNTED="$tmp/mounted" RUNNER_TEST_OPTIONS=rw,relatime
if runner_validate_mount "$image" "$RUNNER_TEST_STORE" 2>/dev/null; then
echo 'Runner mount missing nosuid/nodev was accepted' >&2
exit 1
fi
unset RUNNER_TEST_OPTIONS
echo 'runner installer idempotency checks passed'
+17 -2
View File
@@ -17,10 +17,25 @@ grep -Eq '^ valid_volumes: \[\]$' "$config"
grep -Fq -- '--cpus=2 --memory=3g --pids-limit=512' "$config"
assert_absent 'var/run/docker.sock|metrics:|health_check:|production.env|review_secret' "$config"
grep -Fq 'docker-ce-rootless-extras' "$installer"
grep -Fq 'fallocate -l 15G' "$installer"
grep -Fq 'runner_ensure_image "$runner_image" "$runner_image_bytes" 0 711' "$installer"
grep -Fq 'runner_lock_home "$runner_home"' "$installer"
grep -Fq 'die '\''stop akyldash-act-runner.service after all jobs finish before rerunning this installer' "$installer"
grep -Fq 'ReadWritePaths=$runner_runtime_home /var/log/akyldash-runner /run/user/$runner_uid' "$installer"
grep -Fq 'os.O_NOFOLLOW | os.O_CLOEXEC' infra/runner/secure-runner-image.py
grep -Fq 'os.posix_fallocate(image_fd, 0, int(bytes_text))' infra/runner/secure-runner-image.py
grep -Fq 'pass_fds=(image_fd,)' infra/runner/secure-runner-image.py
grep -Fq 'systemctl enable "$unit_name"' infra/runner/install-runner-lib.sh
grep -Fq 'systemctl start "$unit_name"' infra/runner/install-runner-lib.sh
grep -Fq 'runuser -u "$owner" -- install -d -m 0700 "$directory"' infra/runner/install-runner-lib.sh
grep -Fq 'runner_install_user_content "$runner_home/.config/docker/daemon.json"' "$installer"
assert_absent 'basename.*runner_mount_unit|enable --now.*runner_mount_unit' "$installer"
grep -Fq 'groups=$(id -nG "$user")' infra/runner/install-runner-lib.sh
grep -Fq 'id -gn "$user"' infra/runner/install-runner-lib.sh
assert_absent 'dockerd-rootless-setuptool.sh install --force' "$installer"
grep -Fq 'runner_sha256=3acac8b506ac8cadc88a55155b5d6378f0fab0b8f62d1e0c0450f4ccd69733e2' "$installer"
grep -Fq 'find-free-subid-range.awk' "$installer"
grep -Fq 'usermod --add-subuids "$subid_range" --add-subgids "$subid_range"' "$installer"
grep -Fq 'usermod "${subid_args[@]}" "$runner_user"' "$installer"
grep -Fq 'subid_args+=(--add-subuids "$subid_range")' "$installer"
assert_absent 'usermod.*(sudo|docker)|/var/run/docker.sock|/etc/sudoers' "$installer"
grep -Fq 'runs-on: akyldash-ci' .gitea/workflows/runner-smoke.yaml
grep -Fq 'test ! -S /var/run/docker.sock' .gitea/workflows/runner-smoke.yaml