fix(runner): block unmanaged non110 runner containers

This commit is contained in:
Your Name
2026-06-28 20:52:41 +08:00
parent 00a0f1ffc3
commit 8c79d3b7a1
5 changed files with 95 additions and 49 deletions

View File

@@ -48956,17 +48956,19 @@ production browser smoke:
**下一個 P0**
- 使用既有 Gitea `workflow_dispatch` / controlled CD lane 對含 `6a933e3f0` source 變更的最新 `main` 觸發 CD 與 code-reviewCD 成功後再做 production GET/POST readback 與 `/zh-TW/iwooos` desktop / mobile smoke。
## 2026-06-28 — 20:43 non-110 user runner 註冊前置實作與 188 prepare-only readback
## 2026-06-28 — 20:57 non-110 safe registration helper 與 unmanaged runner cleanup
**完成內容**
- 接續 `5f20d654d` non-110 user runner installer補上安全 registration helperinstaller 的 `REGISTER_NOW=1` 也改成只從互動 TTY 隱藏讀 token透過 stdin 餵給 `act_runner register`,不使用 `--token` argv、不列印 token、不讀 `.runner` 內容
- 188 已套用 user-level Docker runner prepare-only`/home/ollama/awoooi-non110-runner/data/config.yaml`、user systemd service、rollback service 與 `gitea/act_runner:latest` image 都可讀回service 維持 `loaded/inactive/disabled`
- `/home/ollama/act-runner-awoooi` 裸 binary scaffold 確認沒有 `.runner` 後移到 disabled quarantine避免 verifier 同時看見兩套 runner source
- 接續 host-binary user service apply gate補上 `ops/runner/register-awoooi-non110-runner.sh`helper 預設對準 `/home/ollama/act-runner-awoooi/act_runner` 與同目錄 `config.yaml` / `.runner`
- safe registration helper 只允許互動 TTY 隱藏讀 token透過 stdin 餵給 `act_runner register`,不使用 `--token` argv、不列印 token、不讀 `.runner` 內容Codex 不執行 token entry path
- `ops/runner/check-awoooi-non110-runner-readiness.sh` 補上 unmanaged runner container guardrandom-name `gitea/act_runner` 會被 blocker 擋下,明確命名的既有 StockPlatform runner 不列入 AWOOI non-110 blocker
- 188 已停掉兩個 random-name runner container未刪匿名 volume、未讀內容。退休 Docker runner dir `/home/ollama/awoooi-non110-runner` 確認沒有 `.runner` 後移到 disabled quarantine。
**驗證結果**
- 本地:`bash -n`、runner focused pytest `9 passed``guard-gitea-runner-pressure.py``git diff --check`
- 188 verifier`READY_CONFIG_COUNT=1``READY_BINARY_COUNT=1``READY_SERVICE_COUNT=1``READY_REGISTRATION_COUNT=0``READY_ACTIVE_SERVICE_COUNT=0``BLOCKER_COUNT=3`safe next step 指向 safe registration helper。
- 110 readbackrunner units 維持 masked/inactive`ACTIVE_JOB_CONTAINERS=0`failclosed enforcer timer active/enabled
- 本地:`bash -n`、runner focused pytest `11 passed`、Delivery Workbench focused test `1 passed``guard-gitea-runner-pressure.py``git diff --check`
- 188 final verifier`READY_CONFIG_COUNT=1``READY_BINARY_COUNT=1``READY_SERVICE_COUNT=1``READY_REGISTRATION_COUNT=0``READY_ACTIVE_SERVICE_COUNT=0``UNMANAGED_RUNNER_CONTAINERS=none``BLOCKER_COUNT=3`safe next step 指向 safe registration helper。
- 188 只剩明確屬於 StockPlatform 的命名 act_runner container`ACTIVE_ACTION_CONTAINERS=0`
- 110 readbackrunner units 維持 masked/inactive`ACTIVE_JOB_CONTAINERS=0`
**仍維持**
- 沒有讀、複製、貼上、外送 runner token / secret / `.env` / raw sessions / SQLite / auth。

View File

@@ -14,6 +14,7 @@ RUNNER_BINARY_PATHS="${RUNNER_BINARY_PATHS:-${RUNNER_HOME}/awoooi-non110-runner/
RUNNER_DOCKER_IMAGES="${RUNNER_DOCKER_IMAGES:-gitea/act_runner:latest}"
RUNNER_REGISTRATION_PATHS="${RUNNER_REGISTRATION_PATHS:-${RUNNER_HOME}/awoooi-non110-runner/data/.runner ${RUNNER_HOME}/awoooi-non110-runner/.runner ${RUNNER_HOME}/act-runner-awoooi/.runner /home/wooo/act-runner-awoooi/.runner /home/wooo/awoooi-act-runner/.runner /home/wooo/awoooi-non110-runner/.runner /home/wooo/act-runner/.runner}"
RUNNER_SERVICE_NAMES="${RUNNER_SERVICE_NAMES:-awoooi-non110-runner.service gitea-act-runner-awoooi.service gitea-act-runner-host.service}"
ALLOWED_RUNNER_CONTAINER_NAMES="${ALLOWED_RUNNER_CONTAINER_NAMES:-awoooi-non110-runner stockplatform-ubuntu-runner}"
ALLOWED_LABEL_NAMES="${ALLOWED_LABEL_NAMES:-awoooi-non110-host awoooi-non110-ubuntu awoooi-host awoooi-ubuntu}"
FORBIDDEN_LABEL_RE="${FORBIDDEN_LABEL_RE:-^(ubuntu-latest|ubuntu-[0-9].*|self-hosted|stockplatform.*|stock-platform.*|headless.*|playwright.*)$}"
MAX_CAPACITY="${MAX_CAPACITY:-1}"
@@ -155,6 +156,31 @@ active_action_container_count() {
docker ps --format '{{.Names}}' 2>/dev/null | grep -Ec '^GITEA-ACTIONS-TASK-' || true
}
allowed_runner_container_name() {
local name="$1"
local allowed
for allowed in $ALLOWED_RUNNER_CONTAINER_NAMES; do
[ "$name" = "$allowed" ] && return 0
done
return 1
}
unmanaged_runner_containers() {
if ! command_exists docker; then
return 0
fi
docker ps --format '{{.Names}}|{{.Image}}' 2>/dev/null | while IFS='|' read -r name image; do
[ -n "$name" ] || continue
case "$image" in
gitea/act_runner:*|*/gitea/act_runner:*)
if ! allowed_runner_container_name "$name"; then
printf '%s\n' "$name"
fi
;;
esac
done
}
heavy_process_count() {
local count=0
count="$(
@@ -401,12 +427,15 @@ check_rollback() {
check_pressure() {
section "pressure readback"
local containers heavy
local containers heavy unmanaged
containers="$(active_action_container_count)"
heavy="$(heavy_process_count)"
unmanaged="$(unmanaged_runner_containers | paste -sd, -)"
printf 'ACTIVE_ACTION_CONTAINERS=%s\n' "$containers"
printf 'UNMANAGED_RUNNER_CONTAINERS=%s\n' "${unmanaged:-none}"
printf 'HEAVY_PROCESS_COUNT=%s max=%s\n' "$heavy" "$MAX_HEAVY_PROCESS_COUNT"
[ "$containers" = "0" ] || blocker "active_action_containers_present:${containers}"
[ -z "$unmanaged" ] || blocker "unmanaged_runner_containers_present:${unmanaged}"
[ "$heavy" -le "$MAX_HEAVY_PROCESS_COUNT" ] || blocker "heavy_processes_present:${heavy}"
load_per_core_ok || blocker "host_load_per_core_too_high"
}

View File

@@ -9,11 +9,10 @@ set -euo pipefail
GITEA_INSTANCE="${GITEA_INSTANCE:-http://192.168.0.110:3001}"
RUNNER_NAME="${RUNNER_NAME:-awoooi-non110-188}"
RUNNER_LABELS="${RUNNER_LABELS:-awoooi-non110-host:host,awoooi-non110-ubuntu:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04}"
RUNNER_DIR="${RUNNER_DIR:-${HOME}/awoooi-non110-runner}"
DATA_DIR="${DATA_DIR:-${RUNNER_DIR}/data}"
ACT_RUNNER_IMAGE="${ACT_RUNNER_IMAGE:-gitea/act_runner:latest}"
RUNNER_CONFIG="${RUNNER_CONFIG:-${DATA_DIR}/config.yaml}"
RUNNER_REGISTRATION="${RUNNER_REGISTRATION:-${DATA_DIR}/.runner}"
RUNNER_DIR="${RUNNER_DIR:-${HOME}/act-runner-awoooi}"
RUNNER_BINARY="${RUNNER_BINARY:-${RUNNER_DIR}/act_runner}"
RUNNER_CONFIG="${RUNNER_CONFIG:-${RUNNER_DIR}/config.yaml}"
RUNNER_REGISTRATION="${RUNNER_REGISTRATION:-${RUNNER_DIR}/.runner}"
usage() {
cat <<'EOF'
@@ -22,8 +21,8 @@ Usage:
ops/runner/register-awoooi-non110-runner.sh
Environment overrides:
GITEA_INSTANCE, RUNNER_NAME, RUNNER_LABELS, RUNNER_DIR, DATA_DIR,
ACT_RUNNER_IMAGE, RUNNER_CONFIG, RUNNER_REGISTRATION
GITEA_INSTANCE, RUNNER_NAME, RUNNER_LABELS, RUNNER_DIR, RUNNER_BINARY,
RUNNER_CONFIG, RUNNER_REGISTRATION
Safety contract:
- requires an interactive TTY for token entry
@@ -52,18 +51,15 @@ metadata() {
log "gitea_instance=${GITEA_INSTANCE}"
log "runner_name=${RUNNER_NAME}"
log "runner_dir=${RUNNER_DIR}"
log "data_dir=${DATA_DIR}"
log "act_runner_image=${ACT_RUNNER_IMAGE}"
log "runner_binary=${RUNNER_BINARY}"
log "runner_config=${RUNNER_CONFIG}"
log "runner_registration=${RUNNER_REGISTRATION}"
}
preflight() {
command -v docker >/dev/null 2>&1 || die "docker_missing"
[ -d "$RUNNER_DIR" ] || die "runner_dir_missing:${RUNNER_DIR}"
[ -d "$DATA_DIR" ] || die "data_dir_missing:${DATA_DIR}"
[ -x "$RUNNER_BINARY" ] || die "runner_binary_missing_or_not_executable:${RUNNER_BINARY}"
[ -r "$RUNNER_CONFIG" ] || die "runner_config_missing_or_not_readable:${RUNNER_CONFIG}"
docker image inspect "$ACT_RUNNER_IMAGE" >/dev/null 2>&1 || die "act_runner_image_missing:${ACT_RUNNER_IMAGE}"
if [ -s "$RUNNER_REGISTRATION" ] && [ "${ALLOW_REREGISTER:-0}" != "1" ]; then
log "runner_registration_present=1"
@@ -97,14 +93,11 @@ read_token_from_tty() {
register_runner() {
cd "$RUNNER_DIR"
set +x
printf '%s\n' "$RUNNER_TOKEN" | docker run --rm -i \
-v "${DATA_DIR}:/data" \
-e CONFIG_FILE=/data/config.yaml \
"$ACT_RUNNER_IMAGE" register \
printf '%s\n' "$RUNNER_TOKEN" | "$RUNNER_BINARY" register \
--instance "$GITEA_INSTANCE" \
--name "$RUNNER_NAME" \
--labels "$RUNNER_LABELS" \
--config /data/config.yaml
--config "$RUNNER_CONFIG"
unset RUNNER_TOKEN
}

View File

@@ -61,6 +61,7 @@ def _run_verifier(
*,
active_service: bool = True,
unit_target_matches: bool = True,
unmanaged_runner_container: bool = False,
) -> subprocess.CompletedProcess[str]:
fake_bin = tmp_path / "bin"
unit_dir = tmp_path / "units"
@@ -97,11 +98,22 @@ esac
exit 1
""",
)
unmanaged_runner_output = (
"printf 'condescending_wilbur|gitea/act_runner:latest\\n'"
if unmanaged_runner_container
else "exit 0"
)
_write_fake_bin(
fake_bin,
"docker",
"""#!/usr/bin/env bash
if [ "${1:-}" = "ps" ]; then exit 0; fi
f"""#!/usr/bin/env bash
if [ "${{1:-}}" = "ps" ]; then
if printf '%s\\n' "$*" | grep -q 'Image'; then
{unmanaged_runner_output}
exit 0
fi
exit 0
fi
exit 0
""",
)
@@ -202,3 +214,20 @@ def test_non110_readiness_blocks_service_target_mismatch(tmp_path: Path) -> None
assert "BLOCKER runner_service_target_mismatch:awoooi-non110-runner.service" in result.stdout
assert "RUNNER_SERVICE_TARGETS unit=awoooi-non110-runner.service" in result.stdout
assert "AWOOOI_NON110_RUNNER_READY=0" in result.stdout
def test_non110_readiness_blocks_unmanaged_act_runner_container(tmp_path: Path) -> None:
registration_path = tmp_path / ".runner"
registration_path.write_text("secret-token-like-content-not-printed\n", encoding="utf-8")
result = _run_verifier(
tmp_path,
registration_path,
unmanaged_runner_container=True,
)
assert result.returncode == 1
assert "UNMANAGED_RUNNER_CONTAINERS=condescending_wilbur" in result.stdout
assert (
"BLOCKER unmanaged_runner_containers_present:condescending_wilbur"
in result.stdout
)
assert "secret-token-like-content" not in result.stdout

View File

@@ -8,7 +8,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
REGISTER = ROOT / "ops/runner/register-awoooi-non110-runner.sh"
INSTALLER = ROOT / "ops/runner/install-awoooi-non110-user-runner.sh"
COMPAT_WRAPPER = ROOT / "ops/runner/install-awoooi-non110-user-runner.sh"
USER_SERVICE_INSTALLER = ROOT / "ops/runner/install-awoooi-non110-runner-user-service.sh"
def test_register_helper_has_no_token_argv_path() -> None:
@@ -20,28 +21,21 @@ def test_register_helper_has_no_token_argv_path() -> None:
def test_user_installer_has_no_token_argv_path() -> None:
text = INSTALLER.read_text(encoding="utf-8")
assert "--token" not in text
assert "printf '%s\\n' \"$RUNNER_TOKEN\" | docker run --rm -i" in text
assert "runner_registration_requires_external_credentialed_channel" not in text
wrapper = COMPAT_WRAPPER.read_text(encoding="utf-8")
installer = USER_SERVICE_INSTALLER.read_text(encoding="utf-8")
assert "--token" not in wrapper
assert "--token" not in installer
assert "retired_wrapper_does_not_register_or_start_runner" in wrapper
assert "never\n# registers a runner" in installer
def test_register_helper_dry_run_requires_no_token(tmp_path: Path) -> None:
runner_dir = tmp_path / "runner"
data_dir = runner_dir / "data"
data_dir.mkdir(parents=True)
fake_bin = tmp_path / "bin"
fake_bin.mkdir()
docker = fake_bin / "docker"
docker.write_text(
"""#!/usr/bin/env bash
if [ "${1:-}" = "image" ] && [ "${2:-}" = "inspect" ]; then exit 0; fi
exit 99
""",
encoding="utf-8",
)
docker.chmod(0o755)
config = data_dir / "config.yaml"
runner_dir.mkdir(parents=True)
binary = runner_dir / "act_runner"
binary.write_text("#!/usr/bin/env bash\nexit 99\n", encoding="utf-8")
binary.chmod(0o755)
config = runner_dir / "config.yaml"
config.write_text("runner:\n capacity: 1\n", encoding="utf-8")
result = subprocess.run(
@@ -50,11 +44,10 @@ exit 99
env={
**os.environ,
"DRY_RUN": "1",
"PATH": f"{fake_bin}:{os.environ['PATH']}",
"RUNNER_DIR": str(runner_dir),
"DATA_DIR": str(data_dir),
"RUNNER_BINARY": str(binary),
"RUNNER_CONFIG": str(config),
"RUNNER_REGISTRATION": str(data_dir / ".runner"),
"RUNNER_REGISTRATION": str(runner_dir / ".runner"),
},
text=True,
stdout=subprocess.PIPE,