fix(runner): block unmanaged non110 runner containers
This commit is contained in:
@@ -48956,17 +48956,19 @@ production browser smoke:
|
||||
**下一個 P0**:
|
||||
- 使用既有 Gitea `workflow_dispatch` / controlled CD lane 對含 `6a933e3f0` source 變更的最新 `main` 觸發 CD 與 code-review;CD 成功後再做 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 helper;installer 的 `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 guard:random-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 readback:runner 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 readback:runner units 維持 masked/inactive,`ACTIVE_JOB_CONTAINERS=0`。
|
||||
|
||||
**仍維持**:
|
||||
- 沒有讀、複製、貼上、外送 runner token / secret / `.env` / raw sessions / SQLite / auth。
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user