feat(harness): P3 — uniform ctx hook convention (rcust)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
harness.meta.HookCtx (frozen): .domain, .base_url, .meta (RecipeMeta), .deps (provisioned dep creds from $CCCI_DEPS_FILE or None), .op (current lifecycle op or None); built via meta.hook_ctx() at each hook call site. All recipe callables now take ctx: EXTRA_ENV(ctx), UPGRADE_EXTRA_ENV(ctx), READY_PROBE(ctx), BACKUP_VERIFY(ctx), SCREENSHOT(page, ctx), ops.py pre_<op>(ctx). Dict-valued EXTRA_ENV/UPGRADE_EXTRA_ENV unchanged (only the callable signature moved). Call sites converted: deploy_app env shaping, perform_upgrade, wait_ready_probes (gains op=), _perform_op BACKUP_VERIFY, screenshot.capture, _run_pre_hook. Legacy signatures fail FAST with a clear migration message: the registry carries hook_params per hook key, enforced at meta.load() (MetaError names the old vs new signature); ops.py pre-op hooks get the same check at the orchestrator call site (meta.check_hook_signature) — no silent TypeError mid-run. Migrated every in-repo user mechanically (17 ops.py files; cryptpad/lasuite-*/ mailu EXTRA_ENV; mumble+lasuite-drive READY_PROBE; ghost/discourse BACKUP_VERIFY) — seeded values, probes and assertions byte-identical (domain -> ctx.domain; keycloak pre_restore's meta arg -> ctx.meta). Unit tests: hook_ctx field contract, ctx.deps from the run deps file, legacy- signature MetaError (READY_PROBE/EXTRA_ENV/SCREENSHOT + pre-op checker), ctx signatures accepted. Docs table regenerated (signature docs in key docs). Verified on cc-ci: cc-ci-run -m pytest tests/unit -q -> 180 passed; scripts/lint.sh -> PASS.
This commit is contained in:
@ -9,14 +9,14 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
import _p4 # noqa: E402
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_p4.create_account(domain)
|
||||
def pre_upgrade(ctx):
|
||||
_p4.create_account(ctx.domain)
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_p4.create_account(domain)
|
||||
def pre_backup(ctx):
|
||||
_p4.create_account(ctx.domain)
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
_p4.delete_account(domain)
|
||||
assert not _p4.account_exists(domain), "marker account delete did not take (pre_restore)"
|
||||
def pre_restore(ctx):
|
||||
_p4.delete_account(ctx.domain)
|
||||
assert not _p4.account_exists(ctx.domain), "marker account delete did not take (pre_restore)"
|
||||
|
||||
@ -15,13 +15,13 @@ def _write(domain, val):
|
||||
lifecycle.exec_in_app(domain, ["sh", "-c", f"echo {val} > {MARKER}"])
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_write(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_write(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_write(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_write(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
_write(domain, "mutated") # diverge so a successful restore is observable
|
||||
def pre_restore(ctx):
|
||||
_write(ctx.domain, "mutated") # diverge so a successful restore is observable
|
||||
|
||||
@ -7,9 +7,9 @@ DEPLOY_TIMEOUT = 600
|
||||
HTTP_TIMEOUT = 600
|
||||
|
||||
|
||||
def EXTRA_ENV(domain):
|
||||
def EXTRA_ENV(ctx):
|
||||
"""cryptpad needs a SANDBOX_DOMAIN distinct from the main DOMAIN (it serves user content from a
|
||||
separate origin; the web router routes both). Derive a sibling subdomain under the same wildcard
|
||||
(covered by the wildcard cert, so no cert work)."""
|
||||
label, _, rest = domain.partition(".")
|
||||
label, _, rest = ctx.domain.partition(".")
|
||||
return {"SANDBOX_DOMAIN": f"{label}-sb.{rest}"}
|
||||
|
||||
@ -12,8 +12,8 @@ from harness import lifecycle
|
||||
MARKER_PATH = "/usr/share/nginx/html/ci-marker.txt"
|
||||
|
||||
|
||||
def pre_restore(domain: str, meta: dict) -> None:
|
||||
def pre_restore(ctx) -> None:
|
||||
"""Write 'mutated' to the marker before restore runs. If restore brings back the
|
||||
snapshot (which has no marker — never seeded by pre_backup), the marker ends up
|
||||
MISSING or 'mutated' after restore → test_restore_returns_state FAILS → restore=RED."""
|
||||
lifecycle.exec_in_app(domain, ["sh", "-c", f"echo mutated > {MARKER_PATH}"])
|
||||
lifecycle.exec_in_app(ctx.domain, ["sh", "-c", f"echo mutated > {MARKER_PATH}"])
|
||||
|
||||
@ -11,5 +11,5 @@ from harness import lifecycle
|
||||
MARKER_PATH = "/usr/share/nginx/html/ci-marker.txt"
|
||||
|
||||
|
||||
def pre_restore(domain: str, meta: dict) -> None:
|
||||
lifecycle.exec_in_app(domain, ["sh", "-c", f"echo mutated > {MARKER_PATH}"])
|
||||
def pre_restore(ctx) -> None:
|
||||
lifecycle.exec_in_app(ctx.domain, ["sh", "-c", f"echo mutated > {MARKER_PATH}"])
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""custom-html — pre-op seed hooks (Phase 1e HC3). The orchestrator runs `pre_<op>(domain, meta)`
|
||||
"""custom-html — pre-op seed hooks (Phase 1e HC3). The orchestrator runs `pre_<op>(ctx)`
|
||||
BEFORE it performs the op; the matching test_<op>.py asserts the post-op state (assertion-only).
|
||||
|
||||
nginx serves the volume at /usr/share/nginx/html, so the marker file survives an upgrade / a
|
||||
@ -17,16 +17,16 @@ def _write(domain: str, val: str) -> None:
|
||||
lifecycle.exec_in_app(domain, ["sh", "-c", f"echo {val} > {MARKER_PATH}"])
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
def pre_upgrade(ctx):
|
||||
# seed a marker before the upgrade so the overlay can prove the data survives it
|
||||
_write(domain, "upgrade-survives")
|
||||
_write(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
def pre_backup(ctx):
|
||||
# establish a known original state before the backup op captures it
|
||||
_write(domain, "original")
|
||||
_write(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# diverge from the backed-up state so a successful restore (back to "original") is observable
|
||||
_write(domain, "mutated")
|
||||
_write(ctx.domain, "mutated")
|
||||
|
||||
@ -30,18 +30,18 @@ def _seed(domain, value):
|
||||
assert got == value, f"seed did not commit (read back {got!r}, expected {value!r})"
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# diverge from the backup so a successful restore is observable
|
||||
_psql(domain, "DROP TABLE IF EXISTS ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
_psql(ctx.domain, "DROP TABLE IF EXISTS ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -41,7 +41,7 @@ EXTRA_ENV = {
|
||||
}
|
||||
|
||||
|
||||
def BACKUP_VERIFY(domain):
|
||||
def BACKUP_VERIFY(ctx):
|
||||
"""Post-backup integrity check (Q4.6, same race ghost F2-14b hit). The recipe's backupbot db
|
||||
pre-hook (`/pg_backup.sh backup`) dumps the discourse postgres DB to `/var/lib/postgresql/data/
|
||||
backup.sql` (gzip), then restic captures that path. On the loaded single CI node the db container
|
||||
@ -60,7 +60,7 @@ def BACKUP_VERIFY(domain):
|
||||
|
||||
try:
|
||||
out = lifecycle.exec_in_app(
|
||||
domain,
|
||||
ctx.domain,
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
|
||||
@ -36,19 +36,19 @@ def _seed(domain, value):
|
||||
assert got == value, f"seed did not commit (read back {got!r}, expected {value!r})"
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# diverge from the backup so a successful restore is observable: drop the marker table.
|
||||
_mysql(domain, "DROP TABLE IF EXISTS ci_marker;")
|
||||
_mysql(ctx.domain, "DROP TABLE IF EXISTS ci_marker;")
|
||||
got = _mysql(
|
||||
domain,
|
||||
ctx.domain,
|
||||
"SELECT COUNT(*) FROM information_schema.tables "
|
||||
"WHERE table_schema='ghost' AND table_name='ci_marker';",
|
||||
)
|
||||
|
||||
@ -46,7 +46,7 @@ EXTRA_ENV = {
|
||||
}
|
||||
|
||||
|
||||
def BACKUP_VERIFY(domain):
|
||||
def BACKUP_VERIFY(ctx):
|
||||
"""Post-backup integrity check (F2-14b). The recipe's backupbot db pre-hook dumps the ghost MySQL
|
||||
DB to `/var/lib/mysql/backup.sql.gz` (then restic captures that path). On the loaded single CI node
|
||||
the db container intermittently CYCLES mid-dump (observed: full5/6/7 RED, full8 green — pure race;
|
||||
@ -61,7 +61,7 @@ def BACKUP_VERIFY(domain):
|
||||
|
||||
try:
|
||||
out = lifecycle.exec_in_app(
|
||||
domain,
|
||||
ctx.domain,
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
|
||||
@ -25,17 +25,17 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
_psql(domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
def pre_restore(ctx):
|
||||
_psql(ctx.domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -14,20 +14,20 @@ def _token(domain):
|
||||
return kc_admin.admin_token(domain, kc_admin.admin_password(domain))
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
def pre_upgrade(ctx):
|
||||
# create the marker realm (DB data) before the upgrade so the overlay can prove it survives
|
||||
assert kc_admin.create_marker_realm(domain, _token(domain)) in (201, 409)
|
||||
assert kc_admin.create_marker_realm(ctx.domain, _token(ctx.domain)) in (201, 409)
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
def pre_backup(ctx):
|
||||
# establish the marker realm before the backup op captures mariadb
|
||||
assert kc_admin.create_marker_realm(domain, _token(domain)) in (201, 409)
|
||||
assert kc_admin.create_marker_realm(ctx.domain, _token(ctx.domain)) in (201, 409)
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# backup-bot-two cycles the keycloak container during backup → wait for serving, re-auth, then
|
||||
# delete the realm (diverge from the backup) so a successful restore is observable
|
||||
generic.assert_serving(domain, meta)
|
||||
tok = _token(domain)
|
||||
assert kc_admin.delete_marker_realm(domain, tok) in (204, 200)
|
||||
assert not kc_admin.marker_realm_exists(domain, tok), "delete did not take"
|
||||
generic.assert_serving(ctx.domain, ctx.meta)
|
||||
tok = _token(ctx.domain)
|
||||
assert kc_admin.delete_marker_realm(ctx.domain, tok) in (204, 200)
|
||||
assert not kc_admin.marker_realm_exists(ctx.domain, tok), "delete did not take"
|
||||
|
||||
@ -24,18 +24,18 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# drop the marker table (diverge from the backup) so a successful restore is observable
|
||||
_psql(domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
_psql(ctx.domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -15,7 +15,7 @@ HTTP_TIMEOUT = 600
|
||||
DEPS = ["keycloak"]
|
||||
|
||||
|
||||
def EXTRA_ENV(domain):
|
||||
def EXTRA_ENV(ctx):
|
||||
# abra's internal per-deploy convergence timeout (the recipe's TIMEOUT env, default 300s) is too
|
||||
# short for this 9-service stack on a COLD image cache (~9 large images: impress frontend/backend,
|
||||
# minio, postgres18, redis, docspec, y-provider). Cold pulls exceed 300s -> "deploy timed out 🟠".
|
||||
|
||||
@ -13,14 +13,14 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
|
||||
def pre_install(domain, meta):
|
||||
def pre_install(ctx):
|
||||
"""Post-deploy seed for the custom tier (the former setup_custom_tests.sh, moved here in rcust
|
||||
P2b — install_steps.sh runs PRE-deploy and cannot touch the live stack). The deploy alone does
|
||||
NOT create the MinIO bucket: `minio-createbuckets` is a `replicas:0` one-shot (restart_policy:
|
||||
none) that must be triggered. The MinIO storage test asserts the bucket exists, so trigger it
|
||||
here and poll. `--detach` is REQUIRED: the job creates the bucket then EXITS 0, so it never
|
||||
holds a steady 1/1 replica — a blocking scale would wait forever."""
|
||||
stack = domain.replace(".", "_")
|
||||
stack = ctx.domain.replace(".", "_")
|
||||
print(" pre_install: creating MinIO bucket via the minio-createbuckets one-shot", flush=True)
|
||||
subprocess.run(
|
||||
["docker", "service", "scale", "--detach", f"{stack}_minio-createbuckets=1"],
|
||||
@ -91,21 +91,21 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
def pre_upgrade(ctx):
|
||||
# Gate the chaos redeploy on a fully-ready collabora (else it kills a still-booting coolwsd and
|
||||
# abra aborts the upgrade deploy — Q3.2a run 1). Then seed the data-integrity marker.
|
||||
_wait_collabora_ready(domain)
|
||||
_seed(domain, "upgrade-survives")
|
||||
_wait_collabora_ready(ctx.domain)
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# drop the marker table (diverge from the backup) so a successful restore is observable
|
||||
_psql(domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
_psql(ctx.domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -31,18 +31,18 @@ DEPS = ["keycloak"]
|
||||
# pre_install (the former setup_custom_tests.sh, deleted in P2b).
|
||||
|
||||
|
||||
def READY_PROBE(domain):
|
||||
def READY_PROBE(ctx):
|
||||
"""Readiness signals beyond replica-convergence + the app HEALTH_PATH (Q3.2/F2-12). collabora's
|
||||
coolwsd reports its container 1/1 'running' while still doing jail/config init, and its WOPI
|
||||
discovery endpoint 404s until ready — so the harness waits for `/hosting/discovery` → 200 on the
|
||||
collabora sibling host after the install deploy AND after the upgrade chaos redeploy. This is what
|
||||
makes the heavy prev→PR-head crossover reliably green (the new collabora 25.04.9.x finishes init
|
||||
within swarm's healthcheck retries; abra's own converge monitor was too impatient — F2-12)."""
|
||||
label, _, rest = domain.partition(".")
|
||||
return [{"host": f"collabora-{domain}", "path": "/hosting/discovery", "ok": (200,)}]
|
||||
label, _, rest = ctx.domain.partition(".")
|
||||
return [{"host": f"collabora-{ctx.domain}", "path": "/hosting/discovery", "ok": (200,)}]
|
||||
|
||||
|
||||
def EXTRA_ENV(domain):
|
||||
def EXTRA_ENV(ctx):
|
||||
# Two of lasuite-drive's services route on DOMAIN-DERIVED **nested** subdomains —
|
||||
# `MINIO_DOMAIN="minio.${DOMAIN}"` and `COLLABORA_DOMAIN="collabora.${DOMAIN}"`. The cc-ci
|
||||
# wildcard TLS cert is `*.ci.commoninternet.net` (single label only), so a 2-label name like
|
||||
@ -52,8 +52,8 @@ def EXTRA_ENV(domain):
|
||||
# no cert/gateway change. See DECISIONS.md "Phase 2 — nested DOMAIN-derived subdomains".
|
||||
# `AWS_S3_DOMAIN_REPLACE` derives from MINIO_DOMAIN in-compose, so setting MINIO_DOMAIN is enough.
|
||||
return {
|
||||
"MINIO_DOMAIN": f"minio-{domain}",
|
||||
"COLLABORA_DOMAIN": f"collabora-{domain}",
|
||||
"MINIO_DOMAIN": f"minio-{ctx.domain}",
|
||||
"COLLABORA_DOMAIN": f"collabora-{ctx.domain}",
|
||||
# abra's internal per-deploy convergence timeout (recipe TIMEOUT env, default 300s) is too
|
||||
# short for this 12-service stack on a cold image cache (impress frontend/backend, minio,
|
||||
# postgres, redis, collabora ~1GB, onlyoffice ~2GB). Bump so abra waits long enough for
|
||||
|
||||
@ -27,18 +27,18 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# drop the marker table (diverge from the backup) so a successful restore is observable
|
||||
_psql(domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
_psql(ctx.domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -21,7 +21,7 @@ HTTP_TIMEOUT = 600
|
||||
DEPS = ["keycloak"]
|
||||
|
||||
|
||||
def EXTRA_ENV(domain):
|
||||
def EXTRA_ENV(ctx):
|
||||
# lasuite-meet routes LiveKit's WebSocket signaling on a DOMAIN-derived **nested** subdomain
|
||||
# `LIVEKIT_DOMAIN="livekit.${DOMAIN}"`. The cc-ci wildcard TLS cert is `*.ci.commoninternet.net`
|
||||
# (single label only), so a 2-label name like `livekit.lasuite-meet-pr0-abc.ci.commoninternet.net`
|
||||
@ -30,7 +30,7 @@ def EXTRA_ENV(domain):
|
||||
# no cert/gateway change. Same fix as lasuite-drive's minio/collabora siblings (DECISIONS.md
|
||||
# "Phase 2 — nested DOMAIN-derived subdomains").
|
||||
return {
|
||||
"LIVEKIT_DOMAIN": f"livekit-{domain}",
|
||||
"LIVEKIT_DOMAIN": f"livekit-{ctx.domain}",
|
||||
# abra's internal per-deploy convergence TIMEOUT (default 300s) is too short for this stack on
|
||||
# a cold image cache; bump it (kept under DEPLOY_TIMEOUT so Python never kills abra mid-wait).
|
||||
"TIMEOUT": "1000",
|
||||
|
||||
@ -21,10 +21,10 @@ DEPLOY_TIMEOUT = 900
|
||||
HTTP_TIMEOUT = 600
|
||||
|
||||
|
||||
def EXTRA_ENV(domain):
|
||||
def EXTRA_ENV(ctx):
|
||||
return {
|
||||
"MAIL_DOMAIN": domain,
|
||||
"HOSTNAMES": domain,
|
||||
"MAIL_DOMAIN": ctx.domain,
|
||||
"HOSTNAMES": ctx.domain,
|
||||
"TRAEFIK_STACK_NAME": "traefik_ci_commoninternet_net",
|
||||
"TLS_FLAVOR": "notls",
|
||||
"SITENAME": "ccci-mail",
|
||||
|
||||
@ -24,18 +24,18 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# drop the marker table (diverge from the backup) so a successful restore is observable
|
||||
_psql(domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
_psql(ctx.domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -29,18 +29,18 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# drop the marker table (diverge from the backup) so a successful restore is observable
|
||||
_psql(domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
_psql(ctx.domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -38,16 +38,18 @@ def _seed(domain, value):
|
||||
assert got == value, f"seed did not commit (read back {got!r}, expected {value!r})"
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
def pre_restore(ctx):
|
||||
# diverge from the backup so a successful restore is observable: drop the marker table.
|
||||
_sqlite(domain, "DROP TABLE IF EXISTS ci_marker;")
|
||||
got = _sqlite(domain, "SELECT name FROM sqlite_master WHERE type='table' AND name='ci_marker';")
|
||||
_sqlite(ctx.domain, "DROP TABLE IF EXISTS ci_marker;")
|
||||
got = _sqlite(
|
||||
ctx.domain, "SELECT name FROM sqlite_master WHERE type='table' AND name='ci_marker';"
|
||||
)
|
||||
assert got == "", f"drop did not take (sqlite_master still lists ci_marker: {got!r})"
|
||||
|
||||
@ -53,7 +53,7 @@ UPGRADE_EXTRA_ENV = {
|
||||
}
|
||||
|
||||
|
||||
def READY_PROBE(domain):
|
||||
def READY_PROBE(ctx):
|
||||
# The voice server on 64738 is testable on-host ONLY when compose.host-ports.yml is active — i.e.
|
||||
# the post-upgrade LATEST, not the minimal 0.2.0 base. Read the live COMPOSE_FILE to decide, so the
|
||||
# SAME probe fn is correct in both phases: the post-install probe (base, no host-ports) returns []
|
||||
@ -64,7 +64,7 @@ def READY_PROBE(domain):
|
||||
# backup-bot would then exec into a not-running app container -> 409).
|
||||
from harness import abra # lazy: recipe_meta is exec'd with `harness` importable at call time
|
||||
|
||||
cf = abra.env_get(domain, "COMPOSE_FILE") or ""
|
||||
cf = abra.env_get(ctx.domain, "COMPOSE_FILE") or ""
|
||||
if "compose.host-ports.yml" in cf:
|
||||
return [{"tcp_host": "127.0.0.1", "tcp_port": 64738, "stable": 3}]
|
||||
return []
|
||||
|
||||
@ -15,13 +15,13 @@ def _write(domain, val):
|
||||
lifecycle.exec_in_app(domain, ["sh", "-c", f"echo {val} > {MARKER}"])
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_write(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_write(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_write(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_write(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
_write(domain, "mutated") # diverge so a successful restore is observable
|
||||
def pre_restore(ctx):
|
||||
_write(ctx.domain, "mutated") # diverge so a successful restore is observable
|
||||
|
||||
@ -24,17 +24,17 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
_seed(domain, "upgrade-survives")
|
||||
def pre_upgrade(ctx):
|
||||
_seed(ctx.domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
_seed(domain, "original")
|
||||
def pre_backup(ctx):
|
||||
_seed(ctx.domain, "original")
|
||||
|
||||
|
||||
def pre_restore(domain, meta):
|
||||
_psql(domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
def pre_restore(ctx):
|
||||
_psql(ctx.domain, "DROP TABLE ci_marker;")
|
||||
assert _psql(ctx.domain, "SELECT to_regclass('public.ci_marker');") in (
|
||||
"",
|
||||
"NULL",
|
||||
), "drop did not take"
|
||||
|
||||
@ -34,10 +34,12 @@ def _fake_clock(monkeypatch):
|
||||
|
||||
|
||||
# RecipeMeta (rcust P1: wait_ready_probes reads meta.READY_PROBE off the loaded object); defaults
|
||||
# + the drive-style probe hook.
|
||||
# + the drive-style probe hook (P3 ctx signature: the probe receives a HookCtx).
|
||||
_DRIVE_META = dataclasses.replace(
|
||||
harness_meta.load("ccci-no-such-recipe"),
|
||||
READY_PROBE=lambda d: [{"host": f"collabora-{d}", "path": "/hosting/discovery", "ok": (200,)}],
|
||||
READY_PROBE=lambda ctx: [
|
||||
{"host": f"collabora-{ctx.domain}", "path": "/hosting/discovery", "ok": (200,)}
|
||||
],
|
||||
)
|
||||
_NO_PROBE_META = harness_meta.load("ccci-no-such-recipe")
|
||||
|
||||
|
||||
@ -143,10 +143,11 @@ def test_underscore_names_are_private_and_exempt(tmp_path):
|
||||
def test_lowercase_helpers_ignored(tmp_path):
|
||||
r = _write_meta(
|
||||
tmp_path,
|
||||
"def _helper(d):\n return {'K': d}\n\ndef EXTRA_ENV(domain):\n return _helper(domain)\n",
|
||||
"def _helper(d):\n return {'K': d}\n\ndef EXTRA_ENV(ctx):\n return _helper(ctx.domain)\n",
|
||||
)
|
||||
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
assert meta_mod.extra_env(meta, "x.example") == {"K": "x.example"}
|
||||
ctx = meta_mod.hook_ctx("x.example", meta)
|
||||
assert meta_mod.extra_env(meta, ctx) == {"K": "x.example"}
|
||||
|
||||
|
||||
# ---- normalization + helpers --------------------------------------------------------------------
|
||||
@ -160,13 +161,85 @@ def test_health_ok_list_normalized_to_tuple(tmp_path):
|
||||
def test_extra_env_dict_and_callable_forms(tmp_path):
|
||||
r = _write_meta(tmp_path, "EXTRA_ENV = {'A': 1}\n")
|
||||
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
assert meta_mod.extra_env(meta, "d") == {"A": "1"} # values stringified
|
||||
assert meta_mod.extra_env(meta, meta_mod.hook_ctx("d", meta)) == {"A": "1"} # stringified
|
||||
r2 = _write_meta(
|
||||
tmp_path, "UPGRADE_EXTRA_ENV = lambda domain: {'COMPOSE_FILE': domain}\n", recipe="r2"
|
||||
tmp_path, "UPGRADE_EXTRA_ENV = lambda ctx: {'COMPOSE_FILE': ctx.domain}\n", recipe="r2"
|
||||
)
|
||||
meta2 = meta_mod.load(r2, tests_dir=str(tmp_path))
|
||||
assert meta_mod.upgrade_extra_env(meta2, "dom.x") == {"COMPOSE_FILE": "dom.x"}
|
||||
assert meta_mod.extra_env(meta2, "dom.x") == {} # unset EXTRA_ENV resolves to {}
|
||||
ctx2 = meta_mod.hook_ctx("dom.x", meta2, op="upgrade")
|
||||
assert meta_mod.upgrade_extra_env(meta2, ctx2) == {"COMPOSE_FILE": "dom.x"}
|
||||
assert meta_mod.extra_env(meta2, ctx2) == {} # unset EXTRA_ENV resolves to {}
|
||||
|
||||
|
||||
# ---- P3: uniform ctx hook convention -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_hook_ctx_fields(tmp_path):
|
||||
meta = meta_mod.load("no-such", tests_dir=str(tmp_path))
|
||||
ctx = meta_mod.hook_ctx("app.ci.example", meta, op="backup")
|
||||
assert ctx.domain == "app.ci.example"
|
||||
assert ctx.base_url == "https://app.ci.example"
|
||||
assert ctx.meta is meta
|
||||
assert ctx.op == "backup"
|
||||
assert meta_mod.hook_ctx("d", meta).op is None
|
||||
|
||||
|
||||
def test_hook_ctx_deps_from_run_file(tmp_path, monkeypatch):
|
||||
import json
|
||||
|
||||
meta = meta_mod.load("no-such", tests_dir=str(tmp_path))
|
||||
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
|
||||
assert meta_mod.hook_ctx("d", meta).deps is None
|
||||
f = tmp_path / "deps.json"
|
||||
f.write_text(json.dumps({"keycloak": {"recipe": "keycloak", "domain": "kc.x"}}))
|
||||
monkeypatch.setenv("CCCI_DEPS_FILE", str(f))
|
||||
deps = meta_mod.hook_ctx("d", meta).deps
|
||||
assert deps["keycloak"]["domain"] == "kc.x"
|
||||
f.write_text("{}") # empty dict -> None (deps declared but not provisioned)
|
||||
assert meta_mod.hook_ctx("d", meta).deps is None
|
||||
|
||||
|
||||
def test_legacy_hook_signature_raises_clear_meta_error(tmp_path):
|
||||
"""A pre-restructure hook signature must fail AT LOAD with a migration message — never a
|
||||
silent TypeError mid-run (P3.4)."""
|
||||
r = _write_meta(tmp_path, "def READY_PROBE(domain):\n return []\n")
|
||||
with pytest.raises(MetaError, match="ctx"):
|
||||
meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
r2 = _write_meta(tmp_path, "EXTRA_ENV = lambda domain: {}\n", recipe="r2")
|
||||
with pytest.raises(MetaError, match="restructure"):
|
||||
meta_mod.load(r2, tests_dir=str(tmp_path))
|
||||
r3 = _write_meta(
|
||||
tmp_path, "def SCREENSHOT(page, domain, meta):\n return None\n", recipe="r3"
|
||||
)
|
||||
with pytest.raises(MetaError, match="page, ctx"):
|
||||
meta_mod.load(r3, tests_dir=str(tmp_path))
|
||||
|
||||
|
||||
def test_ctx_hook_signatures_accepted(tmp_path):
|
||||
r = _write_meta(
|
||||
tmp_path,
|
||||
"def READY_PROBE(ctx):\n return []\n"
|
||||
"def BACKUP_VERIFY(ctx):\n return True\n"
|
||||
"def SCREENSHOT(page, ctx):\n return None\n"
|
||||
"def EXTRA_ENV(ctx):\n return {}\n",
|
||||
)
|
||||
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
assert callable(meta.READY_PROBE) and callable(meta.SCREENSHOT)
|
||||
|
||||
|
||||
def test_check_hook_signature_for_pre_op_hooks():
|
||||
"""The orchestrator validates ops.py pre_<op> hooks with the same checker (legacy
|
||||
(domain, meta) form names the migration)."""
|
||||
|
||||
def legacy(domain, meta):
|
||||
pass
|
||||
|
||||
def new(ctx):
|
||||
pass
|
||||
|
||||
with pytest.raises(MetaError, match="ctx"):
|
||||
meta_mod.check_hook_signature(legacy, ("ctx",), "tests/x/ops.py::pre_upgrade")
|
||||
meta_mod.check_hook_signature(new, ("ctx",), "tests/x/ops.py::pre_upgrade") # no raise
|
||||
|
||||
|
||||
def test_non_default_reports_only_customized_keys(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user