diff --git a/docs/recipe-customization.md b/docs/recipe-customization.md index 2210a0e..4617efd 100644 --- a/docs/recipe-customization.md +++ b/docs/recipe-customization.md @@ -123,9 +123,6 @@ _This table is GENERATED from the `runner/harness/meta.py` KEYS registry by `scr | `DEPS` | `list[str]` | `[]` | Dep recipes deployed/provisioned alongside (e.g. `["keycloak"]`); creds land in `$CCCI_DEPS_FILE`. | | `WARM_CANONICAL` | `bool` | `False` | Enroll the recipe in the warm/canonical app system (docs/warm.md): green cold runs on LATEST advance the canonical snapshot. | | `SCREENSHOT` | `hook` | `None` | Callable driving Playwright to a safe, credential-free post-login view for the results-card screenshot (default: landing page). | -| `CHAOS_BASE_DEPLOY` **(deprecated)** | `bool` | `False` | DEPRECATED (P2 deletes): ship `tests//compose.ccci.yml` instead — the harness auto-copies it and auto-uses `--chaos` for the base deploy. | -| `OIDC_AT_INSTALL` **(deprecated)** | `bool` | `False` | DEPRECATED (P2 deletes): install-time deps provisioning becomes the ONLY mode when `DEPS` is set. | -| `SKIP_GENERIC` **(deprecated)** | `list[str]` | `[]` | DEPRECATED (P2 deletes; zero users): suppress the generic floor for the listed ops. The env form `CCCI_SKIP_GENERIC*` stays as a dev-only escape hatch. | diff --git a/runner/harness/deps.py b/runner/harness/deps.py index 11445ae..f0fc97e 100644 --- a/runner/harness/deps.py +++ b/runner/harness/deps.py @@ -20,7 +20,7 @@ Per Phase-2 DECISIONS: Run state: - `$CCCI_DEPS_FILE` — JSON file written by the orchestrator after each dep deploys; each entry is `{"recipe": "", "domain": "", "version": null}`. Tests access via the - `deps_apps` pytest fixture defined in `tests/conftest.py`. + `deps` pytest fixture defined in `tests/conftest.py`. """ from __future__ import annotations @@ -50,11 +50,11 @@ def write_run_state(deps_state) -> None: """Write the deps state file ($CCCI_DEPS_FILE). Two shapes supported (canonical=keyed dict): 1. **Legacy list-of-entries:** `[{"recipe": "", "domain": ""}, ...]` (Q2.3 original). - Still accepted by `load_run_state` for backwards compat — `deps_apps` fixture flattens. + Still accepted by `load_run_state` for backwards compat — the `deps` fixture flattens. 2. **NEW per-spec dict (operator-2026-05-28 SSO-dep plan §3.2):** `{"": {"recipe": "", "domain": "", "realm": "...", "client_id": "...", "client_secret": "...", "admin_user": "...", "admin_password": "..."}}`. - The `setup_custom_tests.sh` per-recipe hook reads this via `jq` to wire OIDC env. + The per-recipe `install_steps.sh` hook reads this via `jq` to wire OIDC env. No-op if `$CCCI_DEPS_FILE` isn't set.""" path = os.environ.get("CCCI_DEPS_FILE") @@ -153,7 +153,7 @@ def load_run_state(): def deps_as_dict(state) -> dict[str, dict]: - """Coerce either shape (legacy list or new dict) into a recipe→entry dict for the deps_apps + """Coerce either shape (legacy list or new dict) into a recipe→entry dict for the `deps` fixture + dependent-tests consumption.""" if isinstance(state, dict): return state diff --git a/runner/harness/lifecycle.py b/runner/harness/lifecycle.py index c670496..d8eebd6 100644 --- a/runner/harness/lifecycle.py +++ b/runner/harness/lifecycle.py @@ -12,6 +12,7 @@ import glob import json import os import re +import shutil import socket import ssl import subprocess @@ -125,6 +126,34 @@ def _record_deploy() -> None: f.write(str(n + 1)) +def ccci_overlay_path(recipe: str) -> str: + """The cc-ci-owned compose overlay for a recipe (rcust P2a: first-class, auto-discovered).""" + return os.path.join(meta_mod.TESTS_DIR, recipe, "compose.ccci.yml") + + +def has_ccci_overlay(recipe: str) -> bool: + return os.path.isfile(ccci_overlay_path(recipe)) + + +def provide_ccci_overlay(recipe: str) -> None: + """Copy tests//compose.ccci.yml into THIS run's recipe checkout (ABRA_DIR-aware), so + the recipe's COMPOSE_FILE reference resolves (rcust P2a — the harness owns the copy; recipes + no longer ship install_steps.sh boilerplate for it). No-op for recipes without an overlay.""" + src = ccci_overlay_path(recipe) + if not os.path.isfile(src): + return + dest_dir = abra.recipe_dir(recipe) + if not os.path.isdir(dest_dir): + print(f" ccci-overlay: recipe dir {dest_dir} missing — cannot provide overlay", flush=True) + raise RuntimeError(f"recipe checkout missing for {recipe}: {dest_dir}") + shutil.copy(src, os.path.join(dest_dir, "compose.ccci.yml")) + print( + f" ccci-overlay: provided compose.ccci.yml to the {recipe} checkout " + "(first-class overlay; base deploy auto-chaos)", + flush=True, + ) + + def _run_install_steps(hook: tuple[str, str], recipe: str, domain: str) -> None: """Run a recipe's custom install-steps hook (install_steps.sh) during the install tier — after `abra app new` + env defaults + secret generate, before deploy (Phase 1d DG5). The hook gets the @@ -212,11 +241,12 @@ def deploy_app( ) -> None: """Create + configure + deploy an app. Forces LETS_ENCRYPT_ENV='' so traefik serves the wildcard cert via the file provider and NEVER attempts ACME (adversary finding A1). Applies any - per-recipe EXTRA_ENV (recipe_meta.py) and the custom install-steps hook (Phase 1d) before deploy. + per-recipe EXTRA_ENV (recipe_meta.py), the custom install-steps hook (Phase 1d), and the + first-class `tests//compose.ccci.yml` overlay (rcust P2a) before deploy. - `meta` is the recipe's loaded RecipeMeta (EXTRA_ENV / CHAOS_BASE_DEPLOY); the orchestrator - loads once and passes it down. Callers without one in hand (fixtures, warm reconcile) may omit - it — it is then loaded here via the single meta.load() path. + `meta` is the recipe's loaded RecipeMeta (EXTRA_ENV); the orchestrator loads once and passes + it down. Callers without one in hand (fixtures, warm reconcile) may omit it — it is then + loaded here via the single meta.load() path. `deploy_timeout` is the subprocess timeout for `abra app deploy`. Caller (orchestrator) passes `recipe_meta.DEPLOY_TIMEOUT` so heavy recipes (ghost, matrix-synapse, lasuite-meet) can extend @@ -251,16 +281,18 @@ def deploy_app( flush=True, ) chaos = True - # A recipe may force a chaos base deploy via recipe_meta CHAOS_BASE_DEPLOY=True when an - # install_steps hook adds an untracked compose overlay to the recipe checkout (e.g. discourse's - # compose.ccci.yml, provided by install_steps for the pinned base). The untracked file makes - # abra's pinned-deploy clean-tree check FATA ('has locally unstaged changes'); chaos skips lint + - # the clean-tree gate and deploys the EXPLICITLY-checked-out pinned version (we already ran - # recipe_checkout(version) above) — NOT latest. Same mechanism as the lightweight-tag branch. - elif meta.CHAOS_BASE_DEPLOY: + # A first-class cc-ci compose overlay (tests//compose.ccci.yml, copied into the + # checkout below — rcust P2a) is an UNTRACKED file in the recipe checkout, which makes + # abra's pinned-deploy clean-tree check FATA ('has locally unstaged changes'). Auto-chaos: + # chaos skips lint + the clean-tree gate and deploys the EXPLICITLY-checked-out pinned + # version (we already ran recipe_checkout(version) above) — NOT latest. Same mechanism as + # the lightweight-tag branch. (Replaces the deleted CHAOS_BASE_DEPLOY meta flag — the + # overlay's presence IS the signal, killing the R7 implicit coupling.) + elif has_ccci_overlay(recipe): print( - f" deploy_app({recipe}@{version}): CHAOS_BASE_DEPLOY set → chaos base deploy of the " - "checked-out pinned version (skips clean-tree/lint; deploys version, not LATEST)", + f" deploy_app({recipe}@{version}): compose.ccci.yml overlay present → chaos base " + "deploy of the checked-out pinned version (skips clean-tree/lint; deploys version, " + "not LATEST)", flush=True, ) chaos = True @@ -276,6 +308,12 @@ def deploy_app( abra.secret_generate(domain) if install_steps_hook: _run_install_steps(install_steps_hook, recipe, domain) + # First-class cc-ci compose overlay (rcust P2a): if the recipe ships + # tests//compose.ccci.yml, copy it into THIS run's recipe checkout (ABRA_DIR-aware) + # so the COMPOSE_FILE reference in the recipe's EXTRA_ENV resolves. Untracked, so it persists + # across the later PR-head checkout (idempotent when the head ships the same fix). Replaces + # the per-recipe install_steps.sh copy boilerplate + CHAOS_BASE_DEPLOY flag (auto-chaos above). + provide_ccci_overlay(recipe) # HQ1: warm the local image store before the (real, unchanged) abra deploy. prepull_images(recipe, domain) abra.deploy(domain, chaos=chaos, timeout=deploy_timeout) diff --git a/runner/harness/meta.py b/runner/harness/meta.py index 8214cee..9f1c67d 100644 --- a/runner/harness/meta.py +++ b/runner/harness/meta.py @@ -120,29 +120,9 @@ KEYS: tuple[Key, ...] = ( None, "Callable driving Playwright to a safe, credential-free post-login view for the results-card screenshot (default: landing page).", ), - # ---- Deprecated (deleted in restructure P2; registered so P1 lands green before P2 removes - # them — see recipe-custom-restructure-full-plan.md "Decisions locked") ----------------------- - Key( - "CHAOS_BASE_DEPLOY", - "bool", - False, - "DEPRECATED (P2 deletes): ship `tests//compose.ccci.yml` instead — the harness auto-copies it and auto-uses `--chaos` for the base deploy.", - deprecated=True, - ), - Key( - "OIDC_AT_INSTALL", - "bool", - False, - "DEPRECATED (P2 deletes): install-time deps provisioning becomes the ONLY mode when `DEPS` is set.", - deprecated=True, - ), - Key( - "SKIP_GENERIC", - "list[str]", - [], - "DEPRECATED (P2 deletes; zero users): suppress the generic floor for the listed ops. The env form `CCCI_SKIP_GENERIC*` stays as a dev-only escape hatch.", - deprecated=True, - ), + # (CHAOS_BASE_DEPLOY, OIDC_AT_INSTALL and SKIP_GENERIC were deleted in restructure P2: + # compose.ccci.yml is first-class + auto-chaos; install-time deps wiring is the only mode; + # the generic floor is suppressible only via the dev-only CCCI_SKIP_GENERIC* env form.) ) _REGISTRY: dict[str, Key] = {k.name: k for k in KEYS} diff --git a/runner/run_recipe_ci.py b/runner/run_recipe_ci.py index 2045b2f..59f84ef 100644 --- a/runner/run_recipe_ci.py +++ b/runner/run_recipe_ci.py @@ -73,7 +73,7 @@ ALL_STAGES = ("install", "upgrade", "backup", "restore", "custom") def sso_dep_unverified(declared, deps_ready: bool, requires_deps_skipped: int) -> bool: """F2-11 gate predicate (pure, unit-tested). True when a recipe declares DEPS but its - setup_custom_tests failed (deps not ready) AND that caused ≥1 `requires_deps` (SSO/OIDC) test + dep provisioning failed (deps not ready) AND that caused ≥1 `requires_deps` (SSO/OIDC) test to SKIP. In that case the recipe's characteristic SSO claim was NOT verified, so the run must NOT report GREEN — even though a skip-only pytest file exits 0 and leaves every tier 'pass'. Generic-tier failure-isolation is preserved (those results stand); only the green SIGNAL is @@ -254,16 +254,22 @@ def _tier_env(domain: str) -> dict: return dict(os.environ, CCCI_APP_DOMAIN=domain, CCCI_BASE_URL=f"https://{domain}") -def _skip_generic(op: str, meta) -> bool: +def skip_generic_env_overrides() -> list[str]: + """Active CCCI_SKIP_GENERIC* env overrides (rcust P2c: the meta key is deleted; the env form + is a documented LOCAL-DEV-ONLY escape hatch). Surfaced loudly when set in a CI (drone) run — + it reduces generic-floor coverage and must never silently ride a CI verdict.""" + return sorted( + k for k in os.environ if k.startswith("CCCI_SKIP_GENERIC") and _truthy(os.environ.get(k)) + ) + + +def _skip_generic(op: str) -> bool: """Whether the generic assertion for `op` is opted out (Phase 1e HC3). Default: run (additive). - Opt-out, any of: env CCCI_SKIP_GENERIC (all ops), env CCCI_SKIP_GENERIC_, or the recipe's - declarative recipe_meta.SKIP_GENERIC list (op name, or "all"/"*").""" + Opt-out via env only (dev-only escape hatch, P2c): CCCI_SKIP_GENERIC (all ops) or + CCCI_SKIP_GENERIC_. The recipe_meta SKIP_GENERIC key is deleted (zero users).""" if _truthy(os.environ.get("CCCI_SKIP_GENERIC")): return True - if _truthy(os.environ.get(f"CCCI_SKIP_GENERIC_{op.upper()}")): - return True - sg = [str(s).lower() for s in (meta.SKIP_GENERIC or [])] - return "all" in sg or "*" in sg or op in sg + return _truthy(os.environ.get(f"CCCI_SKIP_GENERIC_{op.upper()}")) def _run_pre_hook(recipe: str, op: str, repo_local: str | None, domain: str, meta) -> None: @@ -360,7 +366,7 @@ def run_lifecycle_tier( a {tier,source,file,rc,junit} record appended, so the run can assemble per-stage/per-test results.json + the level afterwards. Purely additive — does not change the verdict.""" overlay = discovery.resolve_overlay_op(recipe, op, repo_local) - skip_gen = _skip_generic(op, meta) + skip_gen = _skip_generic(op) files: list[tuple[str, str]] = [] if not skip_gen: files.append(discovery.generic_op(op)) @@ -423,7 +429,7 @@ def run_lifecycle_tier( def _enrich_deps_with_sso(parent_recipe: str, parent_domain: str, deps_list) -> dict[str, dict]: """For each dep, set up a fresh realm/client + test user via the harness's provider-specific setup function, then return a recipe→entry dict carrying domain + admin + realm/client/user - info — the shape the `setup_custom_tests.sh` hook (and dependent tests) read. + info — the shape the `install_steps.sh` hook (and dependent tests) read. Provider routing: today only `keycloak` is supported. authentik will need a parallel `setup_authentik_realm` when an authentik-dep recipe enrolls (DEFERRED.md #9). @@ -437,7 +443,7 @@ def _enrich_deps_with_sso(parent_recipe: str, parent_domain: str, deps_list) -> if not dep_recipe or not dep_domain: continue if dep_recipe != "keycloak": - # Provider not yet supported — record bare entry; setup_custom_tests.sh / tests will + # Provider not yet supported — record bare entry; install_steps.sh / tests will # raise if they need realm/client info they don't see. out[dep_recipe] = entry continue @@ -481,12 +487,10 @@ def _provision_deps( Splits deps into live-warm (shared provider at a stable domain + a per-run realm) vs cold (co-deployed per run), provisions each dep's SSO realm/client/user, and persists the enriched - dict the `setup_custom_tests.sh`/`install_steps.sh` hooks + dependent tests read. Raises on any - failure (the caller marks deps-not-ready). Used by BOTH wiring paths: - - post-deploy (legacy): provision AFTER generic tiers, then `setup_custom_tests.sh` does an - in-place OIDC redeploy. - - install-time (`OIDC_AT_INSTALL`, Q3.2a): provision BEFORE the single deploy so the - install-tier `install_steps.sh` hook wires OIDC env into that one deploy — no reconverge. + dict the `install_steps.sh` hooks + dependent tests read. Raises on any failure (the caller + marks deps-not-ready). Install-time wiring is the ONLY mode (rcust P2b): provision BEFORE the + single deploy so the install-tier `install_steps.sh` hook wires OIDC env into that one deploy — + no reconverge, no post-deploy `setup_custom_tests.sh` machinery. """ warm_deps, cold_deps = [], [] for d in declared: @@ -515,32 +519,6 @@ def _provision_deps( return deps_state -def _run_setup_custom_tests_hook(recipe: str, domain: str, deps_file: str) -> None: - """Run `tests//setup_custom_tests.sh` if present (operator-2026-05-28 SSO-dep plan - §3.2). The hook reads `$CCCI_DEPS_FILE`, sets OIDC env via `abra app config set` + secret - insert, and triggers an in-place `abra app deploy --force --chaos`. Failure here propagates - to mark deps-not-ready (caught in main()).""" - path = os.path.join(ROOT, "tests", recipe, "setup_custom_tests.sh") - if not os.path.isfile(path): - # No hook = recipe doesn't need post-deps wiring; deps are deployed + creds available - # via deps_apps fixture as-is. - print( - f" setup_custom_tests: no hook at {os.path.relpath(path, ROOT)} (deps creds ready in $CCCI_DEPS_FILE)", - flush=True, - ) - return - print(f" setup_custom_tests hook: {os.path.relpath(path, ROOT)}", flush=True) - rc = subprocess.run( - ["bash", path], - check=False, - env=dict(os.environ, CCCI_APP_DOMAIN=domain, CCCI_RECIPE=recipe, CCCI_DEPS_FILE=deps_file), - ) - if rc.returncode != 0: - raise RuntimeError( - f"setup_custom_tests.sh exited {rc.returncode} (deps env not wired into parent)" - ) - - def run_custom( recipe: str, repo_local: str | None, @@ -644,9 +622,13 @@ def run_quick( print(f"!! canonical reattach/readiness failed: {_scrub(str(e))}", flush=True) if warm_ok: - # 2) deps (warm keycloak + per-run realm) — mirrors main()'s warm/cold split + # 2) deps (warm keycloak + per-run realm) — mirrors main()'s warm/cold split. NB + # (rcust P2b): deps are provisioned (realm/creds in $CCCI_DEPS_FILE) but quick mode + # cannot do install-time OIDC env wiring — the canonical app pre-exists its per-run + # realm. No quick-enrolled recipe declares DEPS today; if one ever does, its + # requires_deps tests will exercise creds-only flows or skip (F2-11 keeps the signal). if declared: - print(f"\n===== setup_custom_tests (quick): deps {declared} =====", flush=True) + print(f"\n===== deps (quick): {declared} =====", flush=True) try: warm_deps, cold_deps = [], [] for d in declared: @@ -667,12 +649,11 @@ def run_quick( print(f" dep: using live-warm {d} @ {wd} (per-run realm)", flush=True) deps_state = _enrich_deps_with_sso(recipe, domain, deps_list) deps_mod.write_run_state(deps_state) - _run_setup_custom_tests_hook(recipe, domain, depsfile) except Exception as e: # noqa: BLE001 deps_ready = False deps_not_ready_reason = _scrub(str(e))[:300] print( - f"!! setup_custom_tests failed (deps-not-ready): {deps_not_ready_reason}", + f"!! dep provisioning failed (deps-not-ready): {deps_not_ready_reason}", flush=True, ) @@ -787,7 +768,7 @@ def run_quick( overall = 1 if sso_unverified: print( - f"!! DEPS={declared} but setup_custom_tests failed and {requires_deps_skipped} " + f"!! DEPS={declared} but dep provisioning failed and {requires_deps_skipped} " "requires_deps SKIPPED — SSO NOT verified (F2-11)", file=sys.stderr, ) @@ -871,6 +852,17 @@ def main() -> int: print( f"== cc-ci run: recipe={recipe} ref={ref} pr={os.environ.get('PR', '0')} stages={sorted(stages)}" ) + # P2c: the CCCI_SKIP_GENERIC* env escape hatch is LOCAL-DEV-ONLY. If it rides a CI (drone) + # run, shout — generic-floor coverage is reduced and the verdict must not look routine. + for ov in skip_generic_env_overrides(): + if os.environ.get("DRONE"): + print( + f"!! {ov}=1 — dev-only generic-floor override ACTIVE IN A CI RUN; generic " + "assertions are suppressed for the affected op(s). This must never gate a merge.", + flush=True, + ) + else: + print(f"== {ov}=1 (dev-only generic-floor override active)", flush=True) # Concurrent-run safety is structural: this run's recipe trees live in its own ABRA_DIR # (exported here, before ANY abra call), so no recipe-tree lock exists; same-DOMAIN runs # serialise on the app-domain flock taken in deploy_app (see docs/concurrency.md). @@ -933,10 +925,8 @@ def main() -> int: os.environ["CCCI_OP_STATE_FILE"] = statefile op_state: dict = {} - # Run-scoped dep state (Phase 2 Q2.3, refined per operator-2026-05-28 SSO-dep plan §1): - # deps now deploy AFTER generic tiers (between RESTORE and CUSTOM) so a failed dep deploy - # cannot break the generic-tier signal. The `setup_custom_tests` step deploys each dep + runs - # `tests//setup_custom_tests.sh` to wire OIDC env via in-place redeploy. + # Run-scoped dep state (Phase 2 Q2.3; install-time-only since rcust P2b): deps are provisioned + # BEFORE the single deploy so install_steps.sh wires OIDC env into that one deploy. # `$CCCI_DEPS_FILE` is written with the full creds dict the hook script needs (jq-readable). depsfile = _run_state_path("deps") + ".json" with open(depsfile, "w") as f: @@ -948,14 +938,8 @@ def main() -> int: os.remove(skipfile) os.environ["CCCI_DEPS_SKIP_REPORT"] = skipfile declared = list(meta.DEPS) - # Q3.2a: a recipe that tolerates OIDC env at first boot AND whose deps are live-warm wires OIDC - # at INSTALL time (provision the realm BEFORE the single deploy; install_steps.sh writes the env - # into it) instead of the post-deploy in-place `--chaos` redeploy — which is flaky on the heavy - # 12-service lasuite-drive stack (collabora WOPI race; see JOURNAL Step 0). Opt-in per recipe. - oidc_at_install = bool(meta.OIDC_AT_INSTALL) and bool(declared) if declared: - when = "BEFORE deploy (install-time OIDC)" if oidc_at_install else "AFTER generic tiers" - print(f"\n===== DEPS declared (provision {when}): {declared} =====", flush=True) + print(f"\n===== DEPS declared (provision BEFORE deploy): {declared} =====", flush=True) deps_state: dict[str, dict] = {} # new shape: recipe→entry dict (sso-dep plan §1) deps_ready = True deps_not_ready_reason: str = "" @@ -969,7 +953,7 @@ def main() -> int: # install_steps.sh can read $CCCI_DEPS_FILE and wire the OIDC env into that one deploy. On # failure we mark deps-not-ready but STILL deploy the recipe alone (install_steps.sh no-ops # on an empty deps file) so the generic tiers run; the OIDC custom test then skips → F2-11. ---- - if oidc_at_install: + if declared: print( f"\n===== install-time OIDC: provisioning deps {declared} BEFORE deploy =====", flush=True, @@ -1105,41 +1089,11 @@ def main() -> int: if backup_cap else "skip" ) - # ---- setup_custom_tests step (NEW, operator-2026-05-28 SSO-dep plan §3.2) ---- - # Deploy each declared dep + wire OIDC env into the parent app via the per-recipe - # setup_custom_tests.sh hook + in-place redeploy. Failure here marks deps-not-ready - # but does NOT abort the run — @pytest.mark.requires_deps tests skip with reason; - # non-deps custom tests still run normally. - if declared and not oidc_at_install: - # LEGACY post-deploy path: provision deps AFTER generic tiers, then wire OIDC env - # into the parent via the setup_custom_tests.sh hook + an in-place `--chaos` redeploy. - print("\n===== setup_custom_tests: deps + OIDC wiring =====", flush=True) - try: - deps_state = _provision_deps(recipe, domain, ref, declared) - # Run the per-recipe post-deps hook (jq-driven OIDC wiring + in-place redeploy) - _run_setup_custom_tests_hook(recipe, domain, depsfile) - except Exception as e: # noqa: BLE001 — setup failure is ISOLATED to dep-marked tests - deps_ready = False - deps_not_ready_reason = _scrub(str(e))[:300] - print( - f"!! setup_custom_tests failed (deps-not-ready): {deps_not_ready_reason}", - flush=True, - ) - elif declared and oidc_at_install and deps_ready: - # INSTALL-TIME path (Q3.2a): deps were provisioned BEFORE the single deploy and the - # install-tier install_steps.sh hook already wired OIDC env into that one deploy — - # so NO re-provision, NO reconverge here. Run only the post-deploy setup hook - # (e.g. lasuite-drive's minio-createbuckets one-shot), which needs the live stack. - print("\n===== post-deploy setup (OIDC already wired at install) =====", flush=True) - try: - _run_setup_custom_tests_hook(recipe, domain, depsfile) - except Exception as e: # noqa: BLE001 — isolated to dep-marked / state-dependent tests - deps_ready = False - deps_not_ready_reason = _scrub(str(e))[:300] - print( - f"!! post-deploy setup failed: {deps_not_ready_reason}", - flush=True, - ) + # (rcust P2b: install-time deps wiring is the ONLY mode — deps were provisioned BEFORE + # the single deploy and install_steps.sh wired the OIDC env into it. The legacy + # post-deploy provisioning + setup_custom_tests.sh redeploy machinery is deleted; a + # recipe's post-deploy seeding belongs in ops.py pre_install, e.g. lasuite-drive's + # MinIO bucket one-shot.) # ---- CUSTOM tier ---- if "custom" in stages: @@ -1214,8 +1168,7 @@ def main() -> int: # ---- per-op summary (DG6 feed) ---- # SSO-dep plan §1: DG4.1 generalised — one `abra app new` per app in the run (recipe + each - # COLD dep). In-place reconfigure-and-redeploy (the setup_custom_tests step's - # `abra app deploy --force --chaos`) is NOT a fresh `app_new` and does NOT increment the count. + # COLD dep). Chaos redeploys are NOT a fresh `app_new` and do NOT increment the count. # WC1: a live-warm dep (keycloak) is NOT deployed by the run — it only gets a per-run realm — so # warm deps contribute 0. So expected = 1 + (number of COLD deps that actually got deployed). _dep_entries = deps_state.values() if isinstance(deps_state, dict) else (deps_state or []) @@ -1256,12 +1209,12 @@ def main() -> int: overall = 1 if any(v == "fail" for v in results.values()): overall = 1 - # F2-11: a deps-declaring recipe whose setup_custom_tests failed has NOT verified its SSO/OIDC + # F2-11: a deps-declaring recipe whose dep provisioning failed has NOT verified its SSO/OIDC # claim — its requires_deps tests SKIPPED (a skip-only file exits 0, so without this the run # would report GREEN). Fail the run for that recipe; generic-tier results above are untouched. if sso_dep_unverified(declared, deps_ready, requires_deps_skipped): print( - f"!! recipe declares DEPS={declared} but setup_custom_tests failed and " + f"!! recipe declares DEPS={declared} but dep provisioning failed and " f"{requires_deps_skipped} requires_deps (SSO) test(s) were SKIPPED — SSO claim NOT " f"verified; failing run (F2-11). deps-not-ready: {deps_not_ready_reason}", file=sys.stderr, diff --git a/tests/conftest.py b/tests/conftest.py index 017a279..35d47bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,12 +14,7 @@ import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "runner")) from harness import deps as deps_mod # noqa: E402 -from harness import lifecycle, naming -from harness import meta as meta_mod - - -def _short(s: str, n: int = 8) -> str: - return "".join(c for c in s if c.isalnum())[:n] or "local" +from harness import meta as meta_mod # noqa: E402 @pytest.fixture(scope="session") @@ -27,16 +22,6 @@ def recipe() -> str: return os.environ.get("RECIPE", "custom-html") -@pytest.fixture(scope="session") -def app_domain(recipe) -> str: - # Docker swarm config/secret names = __ must be <= 64 chars, and - # stackname is the sanitized domain. ".ci.commoninternet.net" alone is 22 chars, so the - # subdomain label must stay short. Use -<6hex(recipe|pr|ref)> — unique per run, - # collision-safe across recipes (full recipe in the hash), readable context lives in the - # Drone build params + PR comment. (Deviation from plan §4.0 long name; see DECISIONS.md.) - return naming.app_domain(recipe, os.environ.get("PR", "0"), os.environ.get("REF")) - - @pytest.fixture(scope="session") def meta(recipe): """The recipe's FULL validated customization (RecipeMeta, attribute access) via the single @@ -55,32 +40,33 @@ def live_app() -> str: return domain -@pytest.fixture(scope="session") -def deps_apps() -> dict[str, str]: - """Phase 2 Q2.3 dependency-resolver contract (refined operator-2026-05-28 SSO-dep plan §1): - when a recipe declares `DEPS = [...]` in its `recipe_meta.py`, the orchestrator deploys each - dep AFTER the generic tiers (between RESTORE and CUSTOM) and persists their per-run identity - + SSO creds to `$CCCI_DEPS_FILE`. Tests access the dep's per-run domain via this fixture. - For full SSO creds (realm/client/secret/admin) use the `deps_creds` fixture instead. +class _DepEntry(dict): + """One provisioned dep (full creds dict) with attribute sugar: `entry.domain`, `entry.realm`, + `entry.client_secret`, ... — dict-style access works too (rcust P2d).""" - Returns `{dep_recipe: domain}` (str→str). Empty when no deps declared OR deps-not-ready.""" + def __getattr__(self, name): + try: + return self[name] + except KeyError as e: + raise AttributeError(name) from e + + +@pytest.fixture(scope="session") +def deps() -> dict[str, _DepEntry]: + """The recipe's provisioned deps (rcust P2d — consolidates the old `deps_apps`+`deps_creds` + pair). When a recipe declares `DEPS = [...]` in its `recipe_meta.py`, the orchestrator + provisions each dep BEFORE the single deploy and persists per-run identity + SSO creds to + `$CCCI_DEPS_FILE`. `deps["keycloak"]` carries domain/realm/client_id/client_secret/user/ + password/email/admin_user/admin_password/discovery_url/token_url/... (`.domain` etc. work as + attributes). Empty when no deps declared OR deps-not-ready — pair with + `@pytest.mark.requires_deps` so the F2-11 skip-report keeps the green signal honest.""" state = deps_mod.deps_as_dict(deps_mod.load_run_state()) - return {r: e["domain"] for r, e in state.items() if e.get("domain")} - - -@pytest.fixture(scope="session") -def deps_creds() -> dict[str, dict]: - """Full SSO-creds dict for each declared dep (operator-2026-05-28 SSO-dep plan §1). - `deps_creds["keycloak"]` returns the entry written by setup_custom_tests with keys - domain/realm/client_id/client_secret/user/password/email/admin_user/admin_password/ - discovery_url/token_url/.... Use this in `@pytest.mark.requires_deps` tests that need to - authenticate via OIDC.""" - return deps_mod.deps_as_dict(deps_mod.load_run_state()) + return {r: _DepEntry(e) for r, e in state.items()} def pytest_collection_modifyitems(config, items): """SSO-dep plan §4: tests marked `@pytest.mark.requires_deps` are skipped with reason - `deps-not-ready: ` when the orchestrator's setup_custom_tests step failed + `deps-not-ready: ` when the orchestrator's dep provisioning failed (orchestrator sets CCCI_DEPS_READY=0 in env). Non-deps custom tests are unaffected. This is failure-isolation per plan §1 — generic tiers cannot break the SSO-marked tests' @@ -113,40 +99,5 @@ def pytest_configure(config): """Register the `requires_deps` marker so pytest doesn't warn about it.""" config.addinivalue_line( "markers", - "requires_deps: test requires DEPS-declared services + setup_custom_tests success.", + "requires_deps: test requires DEPS-declared services + dep provisioning success.", ) - - -def _wait_healthy(domain, meta): - lifecycle.wait_healthy( - domain, - ok_codes=tuple(meta.HEALTH_OK), - path=meta.HEALTH_PATH, - deploy_timeout=meta.DEPLOY_TIMEOUT, - http_timeout=meta.HTTP_TIMEOUT, - ) - - -@pytest.fixture -def deployed(recipe, app_domain, meta, request): - """Function-scoped: deploy the current/$REF version healthy, guaranteed teardown after. - Used by stages that start from current (install/backup).""" - version = os.environ.get("VERSION") or None - lifecycle.janitor() - request.addfinalizer(lambda: lifecycle.teardown_app(app_domain)) - lifecycle.deploy_app(recipe, app_domain, version=version) - _wait_healthy(app_domain, meta) - return app_domain - - -@pytest.fixture(scope="session") -def deployed_app(recipe, app_domain, meta): - """Install stage: deploy the recipe and wait until healthy; tear down at session end.""" - version = os.environ.get("VERSION") or None - lifecycle.janitor() # sweep orphans from crashed runs first - try: - lifecycle.deploy_app(recipe, app_domain, version=version, secrets=True) - _wait_healthy(app_domain, meta) - yield app_domain - finally: - lifecycle.teardown_app(app_domain) diff --git a/tests/discourse/install_steps.sh b/tests/discourse/install_steps.sh deleted file mode 100755 index 930e663..0000000 --- a/tests/discourse/install_steps.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# discourse — INSTALL-TIME hook (Phase 2 Q4.6). Runs during the install tier AFTER `abra app new` + -# EXTRA_ENV + `abra app secret generate` and BEFORE the single `abra app deploy` -# (lifecycle.py::_run_install_steps), with CCCI_RECIPE / CCCI_APP_DOMAIN in env. -# -# Purpose: provide the cc-ci re-pin+grace overlay (compose.ccci.yml) to the recipe checkout so the -# UPGRADE-tier BASE deploy (published 0.7.0+3.3.1, whose compose pins the Docker-Hub-removed -# `bitnami/discourse:3.3.1` and ships a too-tight 5m start_period) is deployable and can survive the -# 15-25min Rails cold boot — so upgrade-to-latest can run. See compose.ccci.yml's header for the full -# rationale. The overlay is referenced by recipe_meta COMPOSE_FILE; it is a cc-ci file (not part of the -# recipe), so copying it here makes it resolvable. It persists across the later `git checkout ` -# (untracked) so the head deploy also merges it (idempotent — the PR head already re-pins + ships 20m). -# CHAOS_BASE_DEPLOY=True is set so abra's pinned-deploy clean-tree check doesn't FATA on the overlay. -set -euo pipefail - -: "${CCCI_RECIPE:?missing CCCI_RECIPE}" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Resolve the recipe tree the way abra does: $ABRA_DIR (the per-run tree inside a CI run) else -# the canonical ~/.abra — the overlay must land in the tree this run actually deploys from. -RECIPE_DIR="${ABRA_DIR:-${HOME}/.abra}/recipes/${CCCI_RECIPE}" - -if [ ! -d "$RECIPE_DIR" ]; then - echo " discourse install_steps: recipe dir $RECIPE_DIR missing — cannot provide compose.ccci.yml" >&2 - exit 1 -fi - -cp "$SCRIPT_DIR/compose.ccci.yml" "$RECIPE_DIR/compose.ccci.yml" -echo " discourse install_steps: provided compose.ccci.yml (bitnamilegacy re-pin + 20m start_period grace) to recipe checkout (${CCCI_RECIPE})" diff --git a/tests/discourse/recipe_meta.py b/tests/discourse/recipe_meta.py index 9092875..d532a42 100644 --- a/tests/discourse/recipe_meta.py +++ b/tests/discourse/recipe_meta.py @@ -29,11 +29,11 @@ HTTP_TIMEOUT = 1200 # (1) it pins the Docker-Hub-removed `bitnami/discourse:3.3.1` (404) → overlay re-pins app+sidekiq to # `bitnamilegacy/discourse:3.3.1` (namespace-only, identical image), the same re-pin the PR makes; # (2) its 5m start_period is too tight for the 15-25min Rails boot → overlay widens it to 20m (grace). -# install_steps.sh provides the overlay; CHAOS_BASE_DEPLOY skips the clean-tree gate on the untracked -# overlay; it persists across the head checkout (idempotent — the PR head already re-pins + ships 20m). +# The harness auto-provides the overlay to the checkout and auto-chaoses the base deploy +# (first-class compose.ccci.yml, rcust P2a); it persists across the head checkout (idempotent — the +# PR head already re-pins + ships 20m). # Upgrade crossover: 0.7.0 (re-pinned base) → PR head; full assertions run on the HEAD. The 0.7.0 # *custom* tests are not separately run (custom tier runs once, on the head — policy §1 allows skip+record). -CHAOS_BASE_DEPLOY = True UPGRADE_BASE_VERSION = "0.7.0+3.3.1" EXTRA_ENV = { "TIMEOUT": "3600", # abra's internal convergence wait; matches DEPLOY_TIMEOUT (slow Rails boot headroom) diff --git a/tests/ghost/install_steps.sh b/tests/ghost/install_steps.sh deleted file mode 100755 index 2c2dc50..0000000 --- a/tests/ghost/install_steps.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# ghost — INSTALL-TIME hook (Phase 2 F2-14b). Runs during the install tier AFTER `abra app new` + -# EXTRA_ENV + `abra app secret generate` and BEFORE the single `abra app deploy` -# (lifecycle.py::_run_install_steps), with CCCI_RECIPE / CCCI_APP_DOMAIN in env. -# -# Purpose: provide the cc-ci start_period-grace overlay (compose.ccci.yml) to the recipe checkout so -# the UPGRADE-tier BASE deploy (a previous published version whose app healthcheck still ships the -# too-tight 1m start_period) can survive ghost's ~6-9min fresh-DB migration and converge. See -# compose.ccci.yml's header for the full rationale. The overlay is referenced by recipe_meta -# COMPOSE_FILE; copying it here (it is a cc-ci file, not part of the recipe) makes it resolvable. -# It persists across the later `git checkout ` (untracked) so the head deploy also merges it -# (idempotent — the PR head already ships 15m). CHAOS_BASE_DEPLOY=True is set so abra's pinned-deploy -# clean-tree check doesn't FATA on the untracked overlay. -set -euo pipefail - -: "${CCCI_RECIPE:?missing CCCI_RECIPE}" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Resolve the recipe tree the way abra does: $ABRA_DIR (the per-run tree inside a CI run) else -# the canonical ~/.abra — the overlay must land in the tree this run actually deploys from. -RECIPE_DIR="${ABRA_DIR:-${HOME}/.abra}/recipes/${CCCI_RECIPE}" - -if [ ! -d "$RECIPE_DIR" ]; then - echo " ghost install_steps: recipe dir $RECIPE_DIR missing — cannot provide compose.ccci.yml" >&2 - exit 1 -fi - -cp "$SCRIPT_DIR/compose.ccci.yml" "$RECIPE_DIR/compose.ccci.yml" -echo " ghost install_steps: provided compose.ccci.yml (app start_period grace) to recipe checkout (${CCCI_RECIPE})" diff --git a/tests/ghost/recipe_meta.py b/tests/ghost/recipe_meta.py index 710f50f..b7c29bd 100644 --- a/tests/ghost/recipe_meta.py +++ b/tests/ghost/recipe_meta.py @@ -31,16 +31,15 @@ HTTP_TIMEOUT = 900 # (plan-ccci-compose-overlay-policy.md §1), so the harness base-deploys the previous PUBLISHED version # (1.1.1+6-alpine) — which predates the PR and still ships the too-tight 1m start_period → it would # deadlock on the same migration kill. compose.ccci.yml re-applies the 15m grace to the BASE so the -# from-version is deployable; install_steps.sh provides it to the checkout; CHAOS_BASE_DEPLOY skips the -# clean-tree gate on that untracked overlay. It persists across the head checkout (idempotent — the PR -# head already ships 15m). This is the policy-blessed "minimal overlay on the from-version so +# from-version is deployable; the harness auto-provides it to the checkout and auto-chaoses the base +# deploy (first-class compose.ccci.yml, rcust P2a). It persists across the head checkout (idempotent — +# the PR head already ships 15m). This is the policy-blessed "minimal overlay on the from-version so # upgrade-to-latest can run" — grace-only, masks no defect, weakens no test. # TIMEOUT/DEPLOY_TIMEOUT 2400s: the BASE cold boot's wall-time is mysql fresh-dir init (~6min, during # which the app crash-loops harmlessly on `ECONNREFUSED 3306` until mysql accepts connections — no # migration progress lost, it hasn't started) PLUS the ~9-15min schema migration (round-trip-bound, # slower under host load). 1200s was too tight (full4 killed at the near-final `email_recipients` # tables while still 0/1); 2400s gives headroom while still bounding a genuine hang (matches discourse). -CHAOS_BASE_DEPLOY = True EXTRA_ENV = { "TIMEOUT": "2400", "COMPOSE_FILE": "compose.yml:compose.ccci.yml", diff --git a/tests/lasuite-docs/functional/test_create_doc.py b/tests/lasuite-docs/functional/test_create_doc.py index 161462d..e73d70f 100644 --- a/tests/lasuite-docs/functional/test_create_doc.py +++ b/tests/lasuite-docs/functional/test_create_doc.py @@ -5,7 +5,7 @@ persistence". This is the canonical create-an-object + read-it-back for lasuite- Flow (uses an OIDC token from the dep keycloak): 1. Obtain a JWT via OIDC password grant against the dep keycloak (the test user is provisioned - by the orchestrator's setup_custom_tests step). + by the orchestrator's dep-provisioning step). 2. POST `/api/v1.0/documents/` with `Authorization: Bearer ` to create a new doc with a unique title; capture the returned `id`. 3. GET `/api/v1.0/documents//` with the same Bearer token; assert the returned title and @@ -15,7 +15,7 @@ Non-vacuous: a misconfigured OIDC, broken backend, or missing endpoint fails at broken. The marker-in-the-title + id round-trip proves the doc actually persisted in lasuite- docs's database after going through the recipe's nginx → backend → postgres path. -Marked @pytest.mark.requires_deps — skips with `deps-not-ready` if setup_custom_tests failed. +Marked @pytest.mark.requires_deps — skips with `deps-not-ready` if dep provisioning failed. """ from __future__ import annotations @@ -32,9 +32,9 @@ from harness import sso @pytest.mark.requires_deps -def test_create_doc_and_read_back(live_app, deps_creds): +def test_create_doc_and_read_back(live_app, deps): """Create a doc via the authenticated API; fetch it back; assert round-trip.""" - kc = deps_creds["keycloak"] + kc = deps["keycloak"] # Obtain a JWT via OIDC password grant access_token = sso.oidc_password_grant( diff --git a/tests/lasuite-docs/functional/test_oidc_login.py b/tests/lasuite-docs/functional/test_oidc_login.py index 2a05c68..fa527cb 100644 --- a/tests/lasuite-docs/functional/test_oidc_login.py +++ b/tests/lasuite-docs/functional/test_oidc_login.py @@ -5,13 +5,13 @@ SOURCE: references/recipe-maintainer/recipe-info/lasuite-docs/tests/oidc_login.p End-to-end flow: 1. GET `/api/v1.0/users/me/` without auth → asserts the response REDIRECTS to the dep keycloak's realm auth endpoint (the recipe is correctly configured to challenge - unauthenticated callers — wired via setup_custom_tests.sh). + unauthenticated callers — wired via install_steps.sh). 2. Obtain an OIDC token from the dep keycloak via password grant (the test user provisioned by the orchestrator's realm setup). 3. Call `/api/v1.0/users/me/` with `Authorization: Bearer ` → asserts 200 and the returned user's email matches the provisioned test user. -Marked @pytest.mark.requires_deps — skips with `deps-not-ready` if setup_custom_tests failed. +Marked @pytest.mark.requires_deps — skips with `deps-not-ready` if dep provisioning failed. """ from __future__ import annotations @@ -51,9 +51,9 @@ def _get_no_redirect(url: str) -> tuple[int, str]: @pytest.mark.requires_deps -def test_oidc_login_via_keycloak(live_app, deps_creds): +def test_oidc_login_via_keycloak(live_app, deps): """Anonymous → redirect to keycloak; password-grant token → 200 from /api/v1.0/users/me/.""" - kc = deps_creds["keycloak"] + kc = deps["keycloak"] # Step 1: unauthenticated GET → 302 to keycloak realm's auth endpoint status, redirect = _get_no_redirect(f"https://{live_app}/api/v1.0/users/me/") diff --git a/tests/lasuite-docs/functional/test_oidc_with_keycloak.py b/tests/lasuite-docs/functional/test_oidc_with_keycloak.py index 4c3d19e..bd865de 100644 --- a/tests/lasuite-docs/functional/test_oidc_with_keycloak.py +++ b/tests/lasuite-docs/functional/test_oidc_with_keycloak.py @@ -3,10 +3,10 @@ Refactored to the refined SSO-dep model: - The orchestrator deploys a per-run keycloak dep AFTER generic tiers and provisions a fresh realm/client/user via `harness.sso.setup_keycloak_realm`. The creds are written to - `$CCCI_DEPS_FILE` (read here via the `deps_creds` fixture). + `$CCCI_DEPS_FILE` (read here via the `deps` fixture). - This test no longer calls `setup_keycloak_realm` itself — that's the orchestrator's job in - the setup_custom_tests step. We just consume the credentials and exercise the OIDC flow. -- Marked `@pytest.mark.requires_deps` so if setup_custom_tests failed, this test SKIPs with a + the dep-provisioning step. We just consume the credentials and exercise the OIDC flow. +- Marked `@pytest.mark.requires_deps` so if dep provisioning failed, this test SKIPs with a clear `deps-not-ready` reason rather than red-flagging a non-recipe failure. """ @@ -31,13 +31,13 @@ def _b64url_decode(seg: str) -> bytes: @pytest.mark.requires_deps -def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): +def test_oidc_password_grant_against_dep_keycloak(live_app, deps): """The dep keycloak issues a JWT for the pre-provisioned test user via OIDC password grant.""" - assert "keycloak" in deps_creds, ( - f"keycloak creds not in deps_creds; got {list(deps_creds.keys())}. " - "setup_custom_tests should have populated this." + assert "keycloak" in deps, ( + f"keycloak creds not in deps; got {list(deps.keys())}. " + "dep provisioning should have populated this." ) - kc = deps_creds["keycloak"] + kc = deps["keycloak"] # Sanity-check the creds shape — orchestrator-written assert kc["domain"] diff --git a/tests/lasuite-docs/install_steps.sh b/tests/lasuite-docs/install_steps.sh new file mode 100755 index 0000000..d845418 --- /dev/null +++ b/tests/lasuite-docs/install_steps.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# lasuite-docs — INSTALL-TIME OIDC wiring hook (rcust P2b; migrated from the deleted +# setup_custom_tests.sh post-deploy path — sibling of lasuite-drive/-meet's hooks). +# +# Runs during the install tier AFTER `abra app new` + EXTRA_ENV + `abra app secret generate`, and +# BEFORE the single `abra app deploy` (lifecycle.py::_run_install_steps). Writing OIDC env + the +# real client secret HERE means the recipe deploys ONCE with OIDC already wired — no post-deploy +# reconverge. The orchestrator provisions the per-run realm/client on the (live-warm) keycloak +# BEFORE this hook and writes $CCCI_DEPS_FILE (the recipe→creds dict). docs' OIDC settings are +# config-only (validated by `manage.py check`, not fetched at boot), so the stack boots healthy +# with the env set. Env names per lasuite-docs's .env.sample (same values the old post-deploy +# hook wrote — byte-identical wiring, only the timing moved). +# +# Env supplied by the harness: +# CCCI_APP_DOMAIN — the per-run lasuite-docs app domain +# CCCI_APP_ENV — path to the app's .env (the one `abra app deploy` reads) +# CCCI_DEPS_FILE — JSON {keycloak: {domain, realm, client_id, client_secret, ...}} (may be empty) +set -euo pipefail + +: "${CCCI_APP_DOMAIN:?missing}" +ENV_PATH="${CCCI_APP_ENV:?missing}" + +# No deps file / no keycloak entry → install-time provisioning failed or was skipped. NO-OP so the +# recipe still boots; the @requires_deps OIDC custom test then SKIPs and F2-11 flips the run RED. +if [ -z "${CCCI_DEPS_FILE:-}" ] || [ ! -s "${CCCI_DEPS_FILE}" ]; then + echo " install_steps: no deps file — skipping OIDC wiring (recipe boots without OIDC)" + exit 0 +fi +KC_DOMAIN=$(jq -r '.keycloak.domain // empty' "$CCCI_DEPS_FILE") +KC_REALM=$(jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE") +KC_CLIENT=$(jq -r '.keycloak.client_id // empty' "$CCCI_DEPS_FILE") +KC_SECRET=$(jq -r '.keycloak.client_secret // empty' "$CCCI_DEPS_FILE") +if [ -z "$KC_DOMAIN" ] || [ -z "$KC_SECRET" ]; then + echo " install_steps: deps file has no keycloak domain/secret — skipping OIDC wiring" + exit 0 +fi + +echo " lasuite-docs install_steps: wiring OIDC at install against keycloak ${KC_DOMAIN}" + +# 1) Insert the OIDC client secret at a bumped version (abra already generated oidc_rpcs:v1; swarm +# forbids overwriting a secret at the same version). The app is not deployed yet — a swarm secret +# can be created independently — so the single deploy below picks up v2. +CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1") +NEW_NUM=$((${CUR_VER#v} + 1)) +NEW_VER="v${NEW_NUM}" +INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) || + INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) || + { + echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG" + exit 1 + } +sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH" +echo " install_steps: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)" + +# 2) Write OIDC env vars to the app's .env (names per lasuite-docs's .env.sample). Ensure a +# trailing newline first so appends never concatenate onto the last line. +write_env() { + local key="$1" val="$2" + sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH" + [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH" + printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH" +} +write_env OIDC_REALM "$KC_REALM" +write_env OIDC_OP_DISCOVERY_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/.well-known/openid-configuration" +write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" +write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" +write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" +write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" +write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" +write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" +write_env OIDC_RP_SIGN_ALGO "RS256" +write_env OIDC_RP_SCOPES "openid email profile" + +echo " lasuite-docs install_steps: OIDC env wired into .env (deploy will pick it up, no reconverge)" diff --git a/tests/lasuite-docs/setup_custom_tests.sh b/tests/lasuite-docs/setup_custom_tests.sh deleted file mode 100755 index 8f61331..0000000 --- a/tests/lasuite-docs/setup_custom_tests.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# lasuite-docs — post-deps setup hook (operator-2026-05-28 SSO-dep plan §3.2). -# -# Runs AFTER the generic tiers (install/upgrade/backup/restore) and AFTER each declared dep is -# deployed + provisioned with realm/client via the harness. The orchestrator has written -# $CCCI_DEPS_FILE with the keycloak dep's domain + realm + client_secret + admin creds. -# -# This hook: -# 1. Reads the dep's connection info from $CCCI_DEPS_FILE. -# 2. Inserts the OIDC client secret as an abra app secret (recipe-conventional name oidc_rpcs). -# 3. Writes the OIDC env vars to the running app's .env via `abra app config set`. -# 4. Triggers an in-place `abra app deploy --force --chaos` so the new env takes effect. -# THIS IS NOT a fresh `abra app new` — the deploy-count guard (DG4.1, generalised) still -# sees one app_new per app. -# -# Env supplied by the orchestrator: -# CCCI_APP_DOMAIN — the running per-run lasuite-docs app domain -# CCCI_RECIPE — "lasuite-docs" -# CCCI_DEPS_FILE — JSON file (dict shape: {dep_recipe: {domain, realm, client_id, ...}, ...}) -set -euo pipefail - -: "${CCCI_APP_DOMAIN:?missing}" -: "${CCCI_DEPS_FILE:?missing}" -test -s "$CCCI_DEPS_FILE" || { - echo " setup_custom_tests: deps file empty" - exit 1 -} - -# Read keycloak dep info via jq -KC_DOMAIN=$(jq -r '.keycloak.domain' "$CCCI_DEPS_FILE") -KC_REALM=$(jq -r '.keycloak.realm' "$CCCI_DEPS_FILE") -KC_CLIENT=$(jq -r '.keycloak.client_id' "$CCCI_DEPS_FILE") -KC_SECRET=$(jq -r '.keycloak.client_secret' "$CCCI_DEPS_FILE") -if [ -z "$KC_DOMAIN" ] || [ "$KC_DOMAIN" = "null" ]; then - echo " setup_custom_tests: no keycloak.domain in deps" - exit 1 -fi -if [ -z "$KC_SECRET" ] || [ "$KC_SECRET" = "null" ]; then - echo " setup_custom_tests: no keycloak.client_secret" - exit 1 -fi - -echo " lasuite-docs setup_custom_tests: wiring OIDC against keycloak dep ${KC_DOMAIN}" - -# 1) Insert the OIDC client secret AT A BUMPED VERSION (the recipe-maintainer pattern). -# `abra app new -S` already generated `oidc_rpcs:v1` (random) — Docker Swarm forbids overwriting -# a secret at the same version, so we bump the version (v2), insert our value there, then -# update SECRET_OIDC_RPCS_VERSION in the .env to point at the new one. -ENV_PATH="$HOME/.abra/servers/default/${CCCI_APP_DOMAIN}.env" -CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1") -NEW_NUM=$((${CUR_VER#v} + 1)) -NEW_VER="v${NEW_NUM}" - -INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) || - INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) || - { - echo " setup_custom_tests: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG" - exit 1 - } -# Repoint the env var to the new version -sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH" -echo " setup_custom_tests: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)" - -# 2) Write OIDC env vars to the app's .env (names per lasuite-docs's .env.sample). -# Ensure the file ends with a newline FIRST so our appends don't concatenate onto the last line -# (we saw `TIMEOUT=900OIDC_REALM=...` malformed by a missing-trailing-newline file). -[ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH" -write_env() { - local key="$1" val="$2" - # remove any existing key (commented or live) then append the live key=val - sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH" - # Re-ensure trailing newline after each delete (sed may leave the file without one) - [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH" - printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH" -} -write_env OIDC_REALM "$KC_REALM" -write_env OIDC_OP_DISCOVERY_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/.well-known/openid-configuration" -write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth" -write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token" -write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo" -write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout" -write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs" -write_env OIDC_RP_CLIENT_ID "$KC_CLIENT" -write_env OIDC_RP_SIGN_ALGO "RS256" -write_env OIDC_RP_SCOPES "openid email profile" - -# 3) Trigger an in-place redeploy so the env update takes effect. --force re-deploys even when -# the recipe hasn't changed; --chaos avoids the chaos prompt; --no-input non-interactive. -abra app deploy "$CCCI_APP_DOMAIN" --force --chaos --no-input 2>&1 | tail -10 - -echo " lasuite-docs setup_custom_tests: OIDC wired + redeployed" diff --git a/tests/lasuite-drive/functional/test_oidc_with_keycloak.py b/tests/lasuite-drive/functional/test_oidc_with_keycloak.py index e2aaef7..7a8f8f7 100644 --- a/tests/lasuite-drive/functional/test_oidc_with_keycloak.py +++ b/tests/lasuite-drive/functional/test_oidc_with_keycloak.py @@ -3,12 +3,12 @@ Drive (La Suite Drive) is OIDC-required: login is gated by an external OpenID Connect provider. Mirrors the proven lasuite-docs SSO model: - The orchestrator deploys a per-run keycloak dep AFTER the generic tiers and provisions a fresh - realm/client/user via `harness.sso.setup_keycloak_realm`; `setup_custom_tests.sh` then wires the + realm/client/user via `harness.sso.setup_keycloak_realm`; `install_steps.sh` then wires the OIDC env + client secret into the running drive app and redeploys. Creds land in `$CCCI_DEPS_FILE` - (read here via the `deps_creds` fixture). + (read here via the `deps` fixture). - This test consumes those creds and exercises the real OIDC flow against the dep keycloak: discovery endpoint advertises the realm, and a password grant yields a valid JWT with the expected claims. -- Marked `@pytest.mark.requires_deps` so if setup_custom_tests failed the test SKIPs with a clear +- Marked `@pytest.mark.requires_deps` so if dep provisioning failed the test SKIPs with a clear `deps-not-ready` reason — and (per F2-11) the orchestrator then fails the run rather than going green on a skipped SSO test. @@ -36,13 +36,13 @@ def _b64url_decode(seg: str) -> bytes: @pytest.mark.requires_deps -def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): +def test_oidc_password_grant_against_dep_keycloak(live_app, deps): """The dep keycloak issues a JWT for the pre-provisioned test user via OIDC password grant.""" - assert "keycloak" in deps_creds, ( - f"keycloak creds not in deps_creds; got {list(deps_creds.keys())}. " - "setup_custom_tests should have populated this." + assert "keycloak" in deps, ( + f"keycloak creds not in deps; got {list(deps.keys())}. " + "dep provisioning should have populated this." ) - kc = deps_creds["keycloak"] + kc = deps["keycloak"] # Creds shape. WC1: realm is per-run namespaced "-<6hex>"; client_id stays the parent. assert kc["domain"] diff --git a/tests/lasuite-drive/install_steps.sh b/tests/lasuite-drive/install_steps.sh index c864ff3..01999e3 100755 --- a/tests/lasuite-drive/install_steps.sh +++ b/tests/lasuite-drive/install_steps.sh @@ -6,7 +6,7 @@ # BEFORE the single `abra app deploy` (runner/harness/lifecycle.py::_run_install_steps). By writing # the OIDC env + the real client secret into the app's `.env` HERE, the recipe deploys ONCE with # OIDC already wired — eliminating the flaky post-deploy in-place `--force --chaos` 12-service -# reconverge that the old setup_custom_tests.sh did (collabora WOPI-discovery race; see JOURNAL +# post-deploy reconverge (collabora WOPI-discovery race; see JOURNAL # Step 0). The orchestrator provisions the per-run realm/client on the live-warm keycloak BEFORE # this hook and writes $CCCI_DEPS_FILE (the recipe→creds dict). # diff --git a/tests/lasuite-drive/ops.py b/tests/lasuite-drive/ops.py index ca97673..2c6697c 100644 --- a/tests/lasuite-drive/ops.py +++ b/tests/lasuite-drive/ops.py @@ -5,6 +5,7 @@ in the `db` service. The backup path exercises the recipe's pg_backup.sh DB-dump backupbot-labelled).""" import os +import subprocess import sys import time @@ -12,6 +13,47 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner") from harness import lifecycle # noqa: E402 +def pre_install(domain, meta): + """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(".", "_") + 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"], + capture_output=True, + check=False, + ) + check = ( + 'mc alias set _c http://localhost:9000 "$(cat /run/secrets/minio_ru)" ' + '"$(cat /run/secrets/minio_rp)" >/dev/null 2>&1 && ' + "mc ls _c/drive-media-storage >/dev/null 2>&1" + ) + for i in range(30): + cid = subprocess.run( + ["docker", "ps", "-q", "-f", f"name={stack}_minio.1"], + capture_output=True, + text=True, + check=False, + ).stdout.split() + if cid and ( + subprocess.run( + ["docker", "exec", cid[0], "sh", "-c", check], capture_output=True, check=False + ).returncode + == 0 + ): + print( + f" pre_install: bucket drive-media-storage present after {i + 1} poll(s)", + flush=True, + ) + return + time.sleep(3) + raise AssertionError("minio-createbuckets one-shot did not create drive-media-storage in 90s") + + def _wait_collabora_ready(domain, timeout=420): """Gate the upgrade op on collabora being FULLY ready (WOPI discovery endpoint → 200), not just container 1/1 'running'. coolwsd takes ~2min to boot (pre-reads 1300+ l10n files + RSA keygen); diff --git a/tests/lasuite-drive/recipe_meta.py b/tests/lasuite-drive/recipe_meta.py index 615a51c..c084bdd 100644 --- a/tests/lasuite-drive/recipe_meta.py +++ b/tests/lasuite-drive/recipe_meta.py @@ -18,20 +18,17 @@ DEPLOY_TIMEOUT = 1800 HTTP_TIMEOUT = 900 # Base deploy/lifecycle proven cold-green @2026-05-28 (install: pass; 12 services incl. -# onlyoffice+collabora) once the Docker Hub rate limit was fixed. The keycloak SSO dep is now -# enabled: declaring DEPS triggers the orchestrator's setup_custom_tests step (deploy keycloak + -# provision realm/client/user + run tests/lasuite-drive/setup_custom_tests.sh to wire OIDC env + -# in-place redeploy). functional/test_oidc_with_keycloak.py then exercises the SSO flow. +# onlyoffice+collabora) once the Docker Hub rate limit was fixed. Declaring DEPS makes the +# orchestrator provision keycloak (realm/client/user) BEFORE the single deploy; +# functional/test_oidc_with_keycloak.py then exercises the SSO flow. DEPS = ["keycloak"] -# Q3.2a (plan-lasuite-drive-oidc-robustness.md Part A): wire OIDC at INSTALL time, not via a -# post-deploy in-place `--chaos` redeploy. The orchestrator provisions the per-run realm on the -# live-warm keycloak BEFORE the single `abra app deploy`, and tests/lasuite-drive/install_steps.sh -# writes the OIDC env + client secret into the .env that one deploy reads. This eliminates the flaky -# 12-service reconverge (collabora WOPI-discovery race; JOURNAL Step 0). Drive boots fine with OIDC -# env set because keycloak is live-warm (discovery reachable at boot). setup_custom_tests.sh now -# only triggers the post-deploy MinIO bucket one-shot. -OIDC_AT_INSTALL = True +# OIDC is wired at INSTALL time (the only deps mode since rcust P2b; Q3.2a pioneered it here): +# the orchestrator provisions the per-run realm on the live-warm keycloak BEFORE the single +# `abra app deploy`, and tests/lasuite-drive/install_steps.sh writes the OIDC env + client secret +# into the .env that one deploy reads. No post-deploy reconverge (the flaky 12-service collabora +# WOPI race is structurally gone). The post-deploy MinIO bucket one-shot lives in ops.py +# pre_install (the former setup_custom_tests.sh, deleted in P2b). def READY_PROBE(domain): diff --git a/tests/lasuite-drive/setup_custom_tests.sh b/tests/lasuite-drive/setup_custom_tests.sh deleted file mode 100755 index 65d84f2..0000000 --- a/tests/lasuite-drive/setup_custom_tests.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# lasuite-drive — POST-DEPLOY setup hook (Phase 2 Q3.2a). -# -# As of Q3.2a (plan-lasuite-drive-oidc-robustness.md Part A) OIDC is wired at INSTALL time by -# tests/lasuite-drive/install_steps.sh (before the single `abra app deploy`), so this hook NO LONGER -# does any OIDC env wiring or in-place redeploy — that eliminated the flaky 12-service reconverge -# (collabora WOPI race; see JOURNAL Step 0). What remains here is the ONE post-deploy step that -# genuinely needs the live stack: triggering the MinIO bucket-creation one-shot. The orchestrator -# runs this only on the install-time path AFTER the deploy is healthy (deps already provisioned). -# -# Env supplied by the orchestrator: -# CCCI_APP_DOMAIN — the running per-run lasuite-drive app domain -# CCCI_DEPS_FILE — JSON deps creds dict (unused here now; OIDC handled at install) -set -euo pipefail - -: "${CCCI_APP_DOMAIN:?missing}" - -# 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 create it here. `--detach` is REQUIRED: the job creates the bucket then EXITS 0, so it -# never holds a steady 1/1 replica; a blocking `docker service scale ...=1` would wait forever and -# hang the run. With `--detach` the scale just submits the one-run and returns; the poll loop below -# confirms the bucket was actually created. -STACK=$(printf '%s' "$CCCI_APP_DOMAIN" | tr '.' '_') -echo " setup: creating MinIO bucket via the minio-createbuckets one-shot (scale 0->1)" -docker service scale --detach "${STACK}_minio-createbuckets=1" >/dev/null 2>&1 || true -# Wait up to 90s for the one-shot to create the bucket (mc mb drive/drive-media-storage; exit 0). -# Poll by checking the bucket directly from the running minio replica container. -for i in $(seq 1 30); do - MC_CID=$(docker ps -q -f "name=${STACK}_minio.1" | head -1) - if [ -n "$MC_CID" ] && docker exec "$MC_CID" sh -c \ - 'mc alias set _c http://localhost:9000 "$(cat /run/secrets/minio_ru)" "$(cat /run/secrets/minio_rp)" >/dev/null 2>&1 && mc ls _c/drive-media-storage >/dev/null 2>&1'; then - echo " setup: bucket drive-media-storage present after ${i} poll(s)" - break - fi - sleep 3 -done - -echo " lasuite-drive setup_custom_tests: post-deploy MinIO bucket step complete (OIDC wired at install)" diff --git a/tests/lasuite-meet/functional/test_meeting_flow.py b/tests/lasuite-meet/functional/test_meeting_flow.py index 333f6a4..4e72c4b 100644 --- a/tests/lasuite-meet/functional/test_meeting_flow.py +++ b/tests/lasuite-meet/functional/test_meeting_flow.py @@ -36,8 +36,8 @@ def _b64url(seg: str) -> bytes: return base64.urlsafe_b64decode(seg + "=" * ((4 - len(seg) % 4) % 4)) -def _creds(deps_creds: dict) -> dict: - kc = deps_creds["keycloak"] +def _creds(deps: dict) -> dict: + kc = deps["keycloak"] return { "provider": "keycloak", "provider_domain": kc["domain"], @@ -55,10 +55,10 @@ def _creds(deps_creds: dict) -> dict: @pytest.mark.requires_deps -def test_create_room_get_livekit_token_and_read_back(live_app, deps_creds): - assert "keycloak" in deps_creds, f"keycloak creds missing; got {list(deps_creds.keys())}" +def test_create_room_get_livekit_token_and_read_back(live_app, deps): + assert "keycloak" in deps, f"keycloak creds missing; got {list(deps.keys())}" base = f"https://{live_app}" - token = sso.oidc_password_grant(_creds(deps_creds)) + token = sso.oidc_password_grant(_creds(deps)) assert isinstance(token, str) and token.count(".") == 2, "OIDC access token is not a JWT" auth = {"Authorization": f"Bearer {token}"} diff --git a/tests/lasuite-meet/functional/test_oidc_with_keycloak.py b/tests/lasuite-meet/functional/test_oidc_with_keycloak.py index 3335d2c..bac033d 100644 --- a/tests/lasuite-meet/functional/test_oidc_with_keycloak.py +++ b/tests/lasuite-meet/functional/test_oidc_with_keycloak.py @@ -3,12 +3,12 @@ Meet (La Suite Meet) is OIDC-required: login is gated by an external OpenID Connect provider. Mirrors the proven lasuite-docs SSO model: - The orchestrator deploys a per-run keycloak dep AFTER the generic tiers and provisions a fresh - realm/client/user via `harness.sso.setup_keycloak_realm`; `setup_custom_tests.sh` then wires the + realm/client/user via `harness.sso.setup_keycloak_realm`; `install_steps.sh` then wires the OIDC env + client secret into the running drive app and redeploys. Creds land in `$CCCI_DEPS_FILE` - (read here via the `deps_creds` fixture). + (read here via the `deps` fixture). - This test consumes those creds and exercises the real OIDC flow against the dep keycloak: discovery endpoint advertises the realm, and a password grant yields a valid JWT with the expected claims. -- Marked `@pytest.mark.requires_deps` so if setup_custom_tests failed the test SKIPs with a clear +- Marked `@pytest.mark.requires_deps` so if dep provisioning failed the test SKIPs with a clear `deps-not-ready` reason — and (per F2-11) the orchestrator then fails the run rather than going green on a skipped SSO test. @@ -36,13 +36,13 @@ def _b64url_decode(seg: str) -> bytes: @pytest.mark.requires_deps -def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds): +def test_oidc_password_grant_against_dep_keycloak(live_app, deps): """The dep keycloak issues a JWT for the pre-provisioned test user via OIDC password grant.""" - assert "keycloak" in deps_creds, ( - f"keycloak creds not in deps_creds; got {list(deps_creds.keys())}. " - "setup_custom_tests should have populated this." + assert "keycloak" in deps, ( + f"keycloak creds not in deps; got {list(deps.keys())}. " + "dep provisioning should have populated this." ) - kc = deps_creds["keycloak"] + kc = deps["keycloak"] # Creds shape. WC1: realm is per-run namespaced "-<6hex>"; client_id stays the parent. assert kc["domain"] diff --git a/tests/lasuite-meet/install_steps.sh b/tests/lasuite-meet/install_steps.sh index 8d310eb..3ea39e1 100755 --- a/tests/lasuite-meet/install_steps.sh +++ b/tests/lasuite-meet/install_steps.sh @@ -4,7 +4,8 @@ # Runs during the install tier AFTER `abra app new` + EXTRA_ENV + `abra app secret generate`, and # BEFORE the single `abra app deploy` (lifecycle.py::_run_install_steps). Writing OIDC env + the real # client secret HERE means the recipe deploys ONCE with OIDC already wired — no post-deploy reconverge -# (OIDC_AT_INSTALL). The orchestrator provisions the per-run realm/client on the live-warm keycloak +# (install-time deps wiring — the only mode since rcust P2b). The orchestrator provisions the +# per-run realm/client on the live-warm keycloak # BEFORE this hook and writes $CCCI_DEPS_FILE (the recipe→creds dict). # # Meet's OIDC is REQUIRED (recipe README). Same La Suite/impress env contract as drive, with meet's diff --git a/tests/lasuite-meet/recipe_meta.py b/tests/lasuite-meet/recipe_meta.py index b921e6b..63d0ab5 100644 --- a/tests/lasuite-meet/recipe_meta.py +++ b/tests/lasuite-meet/recipe_meta.py @@ -13,13 +13,12 @@ HEALTH_OK = (200, 301, 302) DEPLOY_TIMEOUT = 1200 HTTP_TIMEOUT = 600 -# SSO-dependent (recipe.toml requires=["keycloak"], [sso] provider=keycloak). Wire OIDC at INSTALL -# time against the live-warm keycloak — same machinery as lasuite-drive (Q3.2a): the orchestrator -# provisions the per-run realm BEFORE the single `abra app deploy`, and tests/lasuite-meet/ -# install_steps.sh writes the OIDC env + client secret into that one deploy (no post-deploy -# reconverge). Meet boots fine with OIDC env set because keycloak is live-warm. +# SSO-dependent (recipe.toml requires=["keycloak"], [sso] provider=keycloak). OIDC is wired at +# INSTALL time (the only deps mode since rcust P2b) against the live-warm keycloak: the +# orchestrator provisions the per-run realm BEFORE the single `abra app deploy`, and +# tests/lasuite-meet/install_steps.sh writes the OIDC env + client secret into that one deploy +# (no post-deploy reconverge). Meet boots fine with OIDC env set because keycloak is live-warm. DEPS = ["keycloak"] -OIDC_AT_INSTALL = True def EXTRA_ENV(domain): diff --git a/tests/unit/test_f211_sso_skip.py b/tests/unit/test_f211_sso_skip.py index 4df8d72..bca4297 100644 --- a/tests/unit/test_f211_sso_skip.py +++ b/tests/unit/test_f211_sso_skip.py @@ -30,7 +30,7 @@ def test_sso_dep_unverified_true_when_declared_notready_and_skipped(): def test_sso_dep_unverified_false_when_deps_ready(): - """deps ready (setup_custom_tests succeeded) → SSO tests actually ran → not a failure.""" + """deps ready (dep provisioning succeeded) → SSO tests actually ran → not a failure.""" assert not run_recipe_ci.sso_dep_unverified( ["keycloak"], deps_ready=True, requires_deps_skipped=0 ) diff --git a/tests/unit/test_meta.py b/tests/unit/test_meta.py index ea363c3..32573c9 100644 --- a/tests/unit/test_meta.py +++ b/tests/unit/test_meta.py @@ -73,13 +73,12 @@ def test_registry_field_set_matches_dataclass(): import dataclasses assert [f.name for f in dataclasses.fields(RecipeMeta)] == [k.name for k in KEYS] - # the 14 final keys + the 3 P2-deprecated ones, no more - assert len([k for k in KEYS if not k.deprecated]) == 14 - assert sorted(k.name for k in KEYS if k.deprecated) == [ - "CHAOS_BASE_DEPLOY", - "OIDC_AT_INSTALL", - "SKIP_GENERIC", - ] + # the 14 final keys, no more (the 3 P2-deleted legacy keys are gone from the registry, + # so any recipe_meta still setting them hard-fails the typo gate) + assert len(KEYS) == 14 + assert not [k for k in KEYS if k.deprecated] + for gone in ("CHAOS_BASE_DEPLOY", "OIDC_AT_INSTALL", "SKIP_GENERIC"): + assert gone not in {k.name for k in KEYS} # ---- validation hard errors (locked decision: fail fast at load) -------------------------------