feat(harness): P2 — delete legacy customization keys & paths (rcust)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
a) compose.ccci.yml is FIRST-CLASS: the harness auto-copies tests/<recipe>/ compose.ccci.yml into the run's recipe checkout (ABRA_DIR-aware, lifecycle. provide_ccci_overlay) and auto-chaoses the pinned base deploy on its presence (kills the R7 implicit coupling). ghost/discourse install_steps.sh (copy-only boilerplate) deleted; CHAOS_BASE_DEPLOY removed from both metas + the registry. b) install-time deps wiring is the ONLY mode: deps with DEPS provision BEFORE the single deploy; legacy post-deploy provisioning + the setup_custom_tests.sh invocation machinery deleted. lasuite-docs migrated to install_steps.sh OIDC wiring (same env names/values as the old hook — only the timing moved); lasuite-drive's remaining post-deploy MinIO bucket one-shot moved to ops.py pre_install; both setup_custom_tests.sh files deleted; OIDC_AT_INSTALL removed from drive/meet metas + the registry. c) SKIP_GENERIC meta key deleted (zero users). Env form CCCI_SKIP_GENERIC* stays as the documented dev-only escape hatch; when active in a drone CI run the orchestrator prints a loud !! warning (manifest embedding lands in P5). d) conftest cleanup: dead pre-deploy-once fixtures deployed/deployed_app deleted (zero users), app_domain + _short + _wait_healthy dropped (only users were the deleted fixtures); deps_apps+deps_creds consolidated into ONE deps fixture (entries expose .domain etc. as attributes; dict access intact); the 6 lasuite test files renamed deps_creds->deps (fixture name only — assertions and flows byte-identical). requires_deps marker + F2-11 skip-report plumbing unchanged. Registry is now exactly the 14 final keys; docs §4 table regenerated. Stale setup_custom_tests/OIDC_AT_INSTALL prose in docstrings/comments/assert MESSAGES updated (no assert logic or expected value touched). Verified on cc-ci: cc-ci-run -m pytest tests/unit -q -> 175 passed; scripts/lint.sh -> PASS.
This commit is contained in:
@ -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/<recipe>/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. |
|
||||
|
||||
<!-- META-TABLE-END -->
|
||||
|
||||
|
||||
@ -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": "<dep-recipe>", "domain": "<dep-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": "<dep>", "domain": "<d>"}, ...]` (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):**
|
||||
`{"<dep_recipe>": {"recipe": "<dep>", "domain": "<d>", "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
|
||||
|
||||
@ -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/<recipe>/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/<recipe>/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/<recipe>/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/<recipe>/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)
|
||||
|
||||
@ -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/<recipe>/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}
|
||||
|
||||
@ -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_<OP>, 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_<OP>. 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/<recipe>/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/<recipe>/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,
|
||||
|
||||
@ -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 = <stackname>_<res>_<ver> 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 <recipe[:4]>-<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: <captured-err>` when the orchestrator's setup_custom_tests step failed
|
||||
`deps-not-ready: <captured-err>` 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)
|
||||
|
||||
@ -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 <head>`
|
||||
# (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})"
|
||||
@ -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)
|
||||
|
||||
@ -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 <head>` (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})"
|
||||
@ -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",
|
||||
|
||||
@ -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 <jwt>` to create a new doc with a
|
||||
unique title; capture the returned `id`.
|
||||
3. GET `/api/v1.0/documents/<id>/` 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(
|
||||
|
||||
@ -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 <jwt>` → 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/")
|
||||
|
||||
@ -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"]
|
||||
|
||||
74
tests/lasuite-docs/install_steps.sh
Executable file
74
tests/lasuite-docs/install_steps.sh
Executable file
@ -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)"
|
||||
@ -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"
|
||||
@ -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 "<parent>-<6hex>"; client_id stays the parent.
|
||||
assert kc["domain"]
|
||||
|
||||
@ -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).
|
||||
#
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)"
|
||||
@ -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}"}
|
||||
|
||||
|
||||
@ -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 "<parent>-<6hex>"; client_id stays the parent.
|
||||
assert kc["domain"]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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) -------------------------------
|
||||
|
||||
Reference in New Issue
Block a user