mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Fix release smoke workspace identity on rootless Docker
The exact rehearsal passed its test suites and image build but its browser could not enter the private bind mount. A host UID is remapped to an unrelated subordinate identity inside rootless Docker. Use the daemon owner's container identity for rootless Docker and the host UID/GID for rootful Docker. Execute the locked Playwright CLI directly and probe it during integration preparation, before expensive suites, so missing dependencies or inaccessible mounts fail early without fetching an unqualified CLI version.
This commit is contained in:
@@ -96,6 +96,23 @@ if [ "$(node -p "process.versions.node.split('.')[0]")" != "24" ]; then
|
||||
exit 3
|
||||
fi
|
||||
|
||||
playwright_container_user() {
|
||||
local security_options
|
||||
security_options="$(docker info --format '{{json .SecurityOptions}}')" || return
|
||||
python3 - "$security_options" "$(id -u):$(id -g)" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
options = json.loads(sys.argv[1])
|
||||
if not isinstance(options, list) or not all(isinstance(value, str) for value in options):
|
||||
raise SystemExit("Docker security options must be a JSON list of strings.")
|
||||
# Container root maps to the daemon owner on rootless Docker. Reusing that
|
||||
# owner's host UID instead selects an unrelated subordinate host identity.
|
||||
print("0:0" if "name=rootless" in options else sys.argv[2])
|
||||
PY
|
||||
}
|
||||
PLAYWRIGHT_CONTAINER_USER="$(playwright_container_user)"
|
||||
|
||||
mkdir -p \
|
||||
"$CACHE_DIR/go-build" \
|
||||
"$CACHE_DIR/go-mod" \
|
||||
@@ -204,11 +221,29 @@ run_backend() {
|
||||
fi
|
||||
}
|
||||
|
||||
run_playwright() {
|
||||
docker run --rm \
|
||||
--network host \
|
||||
--ipc host \
|
||||
--user "$PLAYWRIGHT_CONTAINER_USER" \
|
||||
--env CI=true \
|
||||
--env HOME=/tmp \
|
||||
--env "PULSE_E2E_DIAGNOSTIC=${PULSE_E2E_DIAGNOSTIC:-}" \
|
||||
--env "PLAYWRIGHT_BASE_URL=${PULSE_E2E_BASE_URL}" \
|
||||
--volume "$REPOSITORY_DIR/tests/integration:/work" \
|
||||
--workdir /work \
|
||||
"$PLAYWRIGHT_IMAGE" \
|
||||
node /work/node_modules/@playwright/test/cli.js "$@"
|
||||
}
|
||||
|
||||
run_integration_prep() {
|
||||
phase integration-dependencies npm --prefix tests/integration ci
|
||||
PLAYWRIGHT_VERSION="$(node -p "require('./tests/integration/node_modules/@playwright/test/package.json').version")"
|
||||
PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble"
|
||||
phase playwright-image docker pull "$PLAYWRIGHT_IMAGE"
|
||||
# Check the real mount and locked CLI before the expensive test suites and
|
||||
# image build. Never let npx download a different browser test version.
|
||||
phase playwright-runtime run_playwright --version
|
||||
phase mock-github-image docker build --tag pulse-mock-github:test tests/integration/mock-github-server
|
||||
}
|
||||
|
||||
@@ -257,21 +292,6 @@ else
|
||||
--tag pulse:test \
|
||||
.
|
||||
fi
|
||||
run_playwright() {
|
||||
docker run --rm \
|
||||
--network host \
|
||||
--ipc host \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--env CI=true \
|
||||
--env HOME=/tmp \
|
||||
--env "PULSE_E2E_DIAGNOSTIC=${PULSE_E2E_DIAGNOSTIC:-}" \
|
||||
--env "PLAYWRIGHT_BASE_URL=${PULSE_E2E_BASE_URL}" \
|
||||
--volume "$REPOSITORY_DIR/tests/integration:/work" \
|
||||
--workdir /work \
|
||||
"$PLAYWRIGHT_IMAGE" \
|
||||
npx playwright test "$@"
|
||||
}
|
||||
|
||||
run_rehearsal_smoke() {
|
||||
cd tests/integration
|
||||
export MOCK_CHECKSUM_ERROR=false
|
||||
@@ -281,7 +301,7 @@ run_rehearsal_smoke() {
|
||||
export PULSE_E2E_DIAGNOSTIC=1
|
||||
docker compose -f docker-compose.test.yml up -d --wait
|
||||
timeout 60 sh -c 'until curl -fsS ${PULSE_E2E_BASE_URL}/api/health >/dev/null; do sleep 2; done'
|
||||
run_playwright tests/00-diagnostic.spec.ts --project=chromium --reporter=list
|
||||
run_playwright test tests/00-diagnostic.spec.ts --project=chromium --reporter=list
|
||||
local status
|
||||
status="$(curl -s -o "$RUN_DIR/update-status.json" -w '%{http_code}' ${PULSE_E2E_BASE_URL}/api/updates/status || true)"
|
||||
case "$status" in
|
||||
@@ -306,7 +326,7 @@ run_release_smoke() {
|
||||
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-mock-github | grep -q healthy; do sleep 2; done'
|
||||
timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-test-server | grep -q healthy; do sleep 2; done'
|
||||
timeout 60 sh -c 'until curl -fsS ${PULSE_E2E_BASE_URL}/api/health >/dev/null; do sleep 2; done'
|
||||
run_playwright tests/95-release-smoke.spec.ts --project=chromium --reporter=list
|
||||
run_playwright test tests/95-release-smoke.spec.ts --project=chromium --reporter=list
|
||||
docker compose -f docker-compose.test.yml down -v
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
@@ -281,6 +282,66 @@ class ReleasePreflightTest(unittest.TestCase):
|
||||
self.assertLess(block.index("done\n"), block.index("run_frontend_tests\n"))
|
||||
self.assertLess(block.index("run_frontend_tests\n"), block.index("run_backend\n"))
|
||||
|
||||
def test_browser_mount_identity_and_locked_cli_follow_the_docker_daemon(self) -> None:
|
||||
worker = (ROOT / "scripts/release-preflight-worker.sh").read_text()
|
||||
functions = []
|
||||
for name in ("playwright_container_user", "run_playwright"):
|
||||
match = re.search(rf"^{name}\(\) \{{\n.*?^\}}", worker, re.MULTILINE | re.DOTALL)
|
||||
self.assertIsNotNone(match, name)
|
||||
functions.append(match.group(0))
|
||||
for options, info_rc, valid, expected_user in (
|
||||
(["name=seccomp,profile=builtin", "name=rootless"], 0, True, "0:0"),
|
||||
(["name=seccomp,profile=builtin"], 0, True, f"{os.getuid()}:{os.getgid()}"),
|
||||
([], 42, False, None),
|
||||
({"rootless": True}, 0, False, None),
|
||||
(None, 0, False, None),
|
||||
):
|
||||
with self.subTest(options=options, info_rc=info_rc), tempfile.TemporaryDirectory() as raw:
|
||||
temp = pathlib.Path(raw)
|
||||
docker = temp / "docker"
|
||||
docker.write_text(
|
||||
f"#!{sys.executable}\n"
|
||||
"import json,os,pathlib,sys\n"
|
||||
"if sys.argv[1] == 'info':\n"
|
||||
" print(os.environ['TEST_SECURITY_OPTIONS'])\n"
|
||||
" raise SystemExit(int(os.environ['TEST_INFO_RC']))\n"
|
||||
"pathlib.Path(os.environ['TEST_CONTAINER_STARTED']).touch()\n"
|
||||
"print(json.dumps(sys.argv[1:]))\n"
|
||||
)
|
||||
docker.chmod(0o755)
|
||||
started = temp / "started"
|
||||
script = "set -euo pipefail\n" + "\n".join(functions) + "\n"
|
||||
script += 'PLAYWRIGHT_CONTAINER_USER="$(playwright_container_user)"\n'
|
||||
script += "run_playwright --version\n"
|
||||
script += "run_playwright test tests/00-diagnostic.spec.ts --project=chromium\n"
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True, check=False,
|
||||
env={**os.environ, "PATH": f"{temp}:{os.environ['PATH']}",
|
||||
"TEST_SECURITY_OPTIONS": json.dumps(options), "TEST_INFO_RC": str(info_rc),
|
||||
"TEST_CONTAINER_STARTED": str(started),
|
||||
"REPOSITORY_DIR": str(temp / "private workspace"),
|
||||
"PLAYWRIGHT_IMAGE": "locked-playwright-image",
|
||||
"PULSE_E2E_BASE_URL": "http://127.0.0.1:27655"},
|
||||
)
|
||||
self.assertEqual(valid, result.returncode == 0, result.stderr)
|
||||
self.assertEqual(valid, started.exists())
|
||||
if not valid:
|
||||
continue
|
||||
calls = [json.loads(line) for line in result.stdout.splitlines()]
|
||||
self.assertEqual(2, len(calls))
|
||||
for args in calls:
|
||||
self.assertEqual(expected_user, args[args.index("--user") + 1])
|
||||
self.assertEqual(f"{temp}/private workspace/tests/integration:/work",
|
||||
args[args.index("--volume") + 1])
|
||||
self.assertIn("node", args)
|
||||
self.assertIn("/work/node_modules/@playwright/test/cli.js", args)
|
||||
self.assertNotIn("npx", args)
|
||||
self.assertEqual("--version", calls[0][-1])
|
||||
self.assertEqual(["test", "tests/00-diagnostic.spec.ts", "--project=chromium"], calls[1][-3:])
|
||||
prep = worker[worker.index("run_integration_prep() {"):worker.index("# Static frontend checks")]
|
||||
self.assertLess(prep.index("phase playwright-image"), prep.index("phase playwright-runtime"))
|
||||
self.assertIn("phase playwright-runtime run_playwright --version", prep)
|
||||
|
||||
def test_api_shard_plan_is_deterministic_complete_and_disjoint(self) -> None:
|
||||
test_names = [
|
||||
f"TestReleaseCase{index:04d}"
|
||||
|
||||
Reference in New Issue
Block a user