feat(mumble F2-14c): drop cc-ci compose.host-ports.yml fork; deploy 0.2.0 base minimally, add native host-ports on upgrade-to-latest via new UPGRADE_EXTRA_ENV harness hook + COMPOSE_FILE-aware READY_PROBE/install skip

This commit is contained in:
autonomic-bot
2026-05-31 05:07:44 +00:00
parent e3720bedf3
commit 4bf9e1d43d
8 changed files with 101 additions and 80 deletions

View File

@ -81,8 +81,8 @@ def recipe_checkout(recipe: str, version: str) -> None:
path = os.path.expanduser(f"~/.abra/recipes/{recipe}")
# -f (force): the version-pinning checkout must yield the EXACT ref tree. Without it, a cc-ci
# install_steps-provided overlay (e.g. mumble's compose.host-ports.yml, copied into a version that
# predates it) is an UNTRACKED file that collides with the same path TRACKED in a later ref, and
# install_steps-provided overlay (e.g. discourse's compose.ccci.yml, copied into the pinned base)
# is an UNTRACKED file that collides with the same path TRACKED in a later ref, and
# `git checkout <ref>` aborts ("untracked working tree files would be overwritten"). Force resolves
# it by writing the ref's tracked version. Safe: we never want local recipe-tree state preserved
# across a version switch (and chaos deploys re-provide the overlay via install_steps when needed).
@ -137,6 +137,25 @@ def env_set(domain: str, key: str, value: str) -> None:
fh.write("\n".join(out) + "\n")
def env_get(domain: str, key: str) -> str | None:
"""Read a key from the app's .env (last uncommented assignment wins). None if absent. Symmetric
with env_set; abra has no getter. Strips surrounding quotes from the value."""
import os
import re
path = os.path.expanduser(f"~/.abra/servers/default/{domain}.env")
if not os.path.exists(path):
return None
pat = re.compile(rf"^\s*{re.escape(key)}=(.*)$")
val = None
with open(path) as fh:
for ln in fh.read().splitlines():
m = pat.match(ln)
if m:
val = m.group(1).strip().strip('"').strip("'")
return val
def secret_generate(domain: str, timeout: int = 300) -> None:
# -m avoids the TTY/table (ioctl) path; output (which contains the generated values) is
# captured by _run and never logged. -C -o keep the recipe at the PR checkout (without -o it

View File

@ -18,7 +18,7 @@ import socket
import ssl
import time
from . import lifecycle
from . import abra, lifecycle
# A recipe is backup-capable iff a compose file carries a truthy backupbot.backup label.
_BACKUPBOT_RE = re.compile(r"backupbot\.backup\b[^\n]*\btrue\b", re.IGNORECASE)
@ -244,6 +244,17 @@ def perform_upgrade(
before = lifecycle.deployed_identity(domain)
if head_ref:
lifecycle.recipe_checkout_ref(recipe, head_ref)
# UPGRADE_EXTRA_ENV (F2-14c): a recipe may need different app .env for the upgrade-TARGET deploy
# than for the base — e.g. mumble's `compose.host-ports.yml` overlay exists ONLY in the newer
# (target) version, so the base deploys minimally WITHOUT it and the upgrade adds it to COMPOSE_FILE
# here, after the PR-head checkout (which ships the overlay) and before the chaos redeploy that
# picks up the new .env. Dict or callable(domain)->dict. No-op for recipes without it.
upgrade_env = meta.get("UPGRADE_EXTRA_ENV") or {}
if callable(upgrade_env):
upgrade_env = upgrade_env(domain) or {}
for k, v in upgrade_env.items():
print(f" upgrade-env: {k}={v}", flush=True)
abra.env_set(domain, k, v)
# HQ1: warm the NEW-version image set before the chaos redeploy (the head_ref checkout's pinned
# tags) so a pull failure is a clear pre-deploy error and convergence isn't pull-bound.
lifecycle.prepull_images(recipe, domain)

View File

@ -231,10 +231,10 @@ def deploy_app(
flush=True,
)
chaos = True
# A recipe may force a chaos base deploy via recipe_meta CHAOS_BASE_DEPLOY=True when cc-ci adds
# an untracked compose overlay to the recipe checkout (e.g. mumble's host-ports.yml, provided
# by install_steps for older versions that predate it). The untracked file makes abra's
# pinned-deploy clean-tree check FATA ('has locally unstaged changes'); chaos skips lint +
# 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 _recipe_meta_flag(recipe, "CHAOS_BASE_DEPLOY"):

View File

@ -194,7 +194,7 @@ def _load_meta(recipe: str) -> dict:
ns: dict = {}
with open(path) as fh:
exec(compile(fh.read(), path, "exec"), ns) # noqa: S102 (trusted, in-repo)
for k in list(meta) + ["BACKUP_CAPABLE", "SKIP_GENERIC", "OIDC_AT_INSTALL", "READY_PROBE", "UPGRADE_BASE_VERSION", "BACKUP_VERIFY"]:
for k in list(meta) + ["BACKUP_CAPABLE", "SKIP_GENERIC", "OIDC_AT_INSTALL", "READY_PROBE", "UPGRADE_BASE_VERSION", "BACKUP_VERIFY", "UPGRADE_EXTRA_ENV"]:
if k in ns:
meta[k] = ns[k]
return meta

View File

@ -1,19 +0,0 @@
---
# cc-ci-owned copy of the upstream mumble `compose.host-ports.yml` overlay (identical content).
# Provided to the recipe checkout by tests/mumble/install_steps.sh so that 64738 is published on the
# cc-ci host for EVERY version under test — the upstream overlay only exists from recipe version
# 1.0.0+, but the upgrade tier's base deploy is the previous published version (0.2.0+), which
# predates it. On-host tests (cc-ci-run) reach the voice server at 127.0.0.1:64738 via this publish.
version: "3.8"
services:
app:
ports:
- target: 64738
published: 64738
protocol: tcp
mode: host
- target: 64738
published: 64738
protocol: udp
mode: host

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# mumble — INSTALL-TIME hook (Phase 2 Q4.2). 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 / CCCI_APP_ENV in env.
#
# Purpose: guarantee `compose.host-ports.yml` exists in the recipe checkout for EVERY version under
# test. mumble's voice server speaks a non-HTTP TLS protocol on 64738; cc-ci's tests run on-host
# (cc-ci-run) and reach it at 127.0.0.1:64738 via a host-published port. The upstream recipe ships
# compose.host-ports.yml only from version 1.0.0+, but the upgrade tier's base deploy is the previous
# published version (0.2.0+), which predates it — so EXTRA_ENV's COMPOSE_FILE (which references the
# overlay) would fail to resolve on that base deploy. We provide an identical overlay here so the
# overlay is present whether the checked-out version ships it natively (no-op) or not (copied).
set -euo pipefail
: "${CCCI_RECIPE:?missing CCCI_RECIPE}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RECIPE_DIR="${HOME}/.abra/recipes/${CCCI_RECIPE}"
if [ ! -d "$RECIPE_DIR" ]; then
echo " mumble install_steps: recipe dir $RECIPE_DIR missing — cannot provide host-ports overlay" >&2
exit 1
fi
if [ -f "$RECIPE_DIR/compose.host-ports.yml" ]; then
echo " mumble install_steps: compose.host-ports.yml already present (native to this version)"
else
cp "$SCRIPT_DIR/compose.host-ports.yml" "$RECIPE_DIR/compose.host-ports.yml"
echo " mumble install_steps: provided compose.host-ports.yml to recipe checkout (${CCCI_RECIPE})"
fi

View File

@ -1,28 +1,33 @@
# Per-recipe harness config for mumble (Phase 2 Q4.2 — a TCP/voice recipe, not HTTP-native).
#
# Mumble's voice server speaks its own TLS protocol on 64738 (no HTTP API). To fit cc-ci's
# HTTP-readiness + on-host test model we deploy two recipe overlays:
# HTTP-readiness + on-host test model we deploy upstream recipe overlays:
# - compose.mumbleweb.yml -> a mumble-web HTTP client routed through Traefik on the app domain,
# giving the generic harness a real HTTP readiness/serving signal (HEALTH_PATH "/") AND the
# web_client.py parity surface.
# - compose.host-ports.yml -> publishes 64738 (tcp+udp) directly on the cc-ci host (mode: host).
# Tests run on-host (cc-ci-run), so the protocol tests connect to 127.0.0.1:64738.
# Both overlays are shipped by the upstream recipe; this is a documented deployment mode, not a fork.
# web_client.py parity surface. Shipped upstream from 0.1.0 (present on the 0.2.0 base too).
# - compose.host-ports.yml -> publishes 64738 (tcp+udp, mode:host) on the cc-ci host so on-host
# tests (cc-ci-run) reach the voice server at 127.0.0.1:64738. Shipped upstream ONLY from 1.0.0.
#
# F2-14c disposition (Adversary REVIEW-2 @16:22:07Z VETO): the upgrade tier's base is the previous
# published version 0.2.0+v1.6.870-0, which PREDATES compose.host-ports.yml (added in 1.0.0). We do
# NOT carry a cc-ci copy of that upstream overlay (no fork). Instead:
# - the BASE 0.2.0 deploys MINIMALLY with `compose.yml:compose.mumbleweb.yml` (HTTP health via
# mumble-web works; the voice port is NOT host-published on 0.2.0), and the on-host voice/protocol
# custom tests are SKIPPED on 0.2.0 (they run in the CUSTOM tier, which executes once on the
# post-upgrade LATEST);
# - the UPGRADE to latest (1.0.0+, which ships compose.host-ports.yml NATIVELY) adds host-ports to
# COMPOSE_FILE via UPGRADE_EXTRA_ENV (applied by generic.perform_upgrade after the PR-head
# checkout, before the chaos redeploy), so the voice port IS host-published on latest and the
# voice tests run there. The current version's native overlay is untouched (no cc-ci fork).
#
# Distinctive config markers (read back by the recipe-specific functional tests, proving our config
# actually propagated into the running server — version-independent, not hard-coded upstream values):
# WELCOME_TEXT -> MUMBLE_CONFIG_WELCOMETEXT, surfaced in the ServerSync welcome_text.
# USERS -> MUMBLE_CONFIG_USERS (max users), surfaced in the ServerConfig.max_users.
HEALTH_PATH = "/" # mumble-web client UI
HEALTH_PATH = "/" # mumble-web client UI (present on both 0.2.0 base and 1.0.0 latest)
HEALTH_OK = (200,)
# install_steps.sh provides compose.host-ports.yml to recipe versions that predate it (the upgrade
# tier's base deploy is the previous published version, 0.2.0+, which lacks the upstream overlay).
# That untracked file makes abra's PINNED base-deploy clean-tree check FATA, so deploy the
# explicitly-checked-out pinned version with chaos (skips lint/clean-tree; deploys the version, not
# LATEST). No-op for the upgrade tier (already a PR-head chaos redeploy). See DECISIONS.md.
CHAOS_BASE_DEPLOY = True
DEPLOY_TIMEOUT = 900 # two images to pull (mumble-server + mumble-web) on a cold node
HTTP_TIMEOUT = 300
@ -31,19 +36,34 @@ WELCOME_TEXT_MARKER = "cc-ci-mumble-welcome-7f3a9c"
# A distinctive max-users value (not the recipe default 100) the server_config test asserts.
MAX_USERS = 42
# BASE deploy (0.2.0): mumble-web only — NO host-ports (0.2.0 predates it). The voice-config env is
# set here and persists across the upgrade so it takes effect on the latest (where the custom config
# round-trip tests assert it).
EXTRA_ENV = {
"COMPOSE_FILE": "compose.yml:compose.mumbleweb.yml:compose.host-ports.yml",
"COMPOSE_FILE": "compose.yml:compose.mumbleweb.yml",
"WELCOME_TEXT": WELCOME_TEXT_MARKER,
"USERS": str(MAX_USERS),
}
# UPGRADE-target deploy (latest 1.0.0+): add the NATIVE compose.host-ports.yml so 64738 is
# host-published and the on-host voice/protocol custom tests can run on latest.
UPGRADE_EXTRA_ENV = {
"COMPOSE_FILE": "compose.yml:compose.mumbleweb.yml:compose.host-ports.yml",
}
def READY_PROBE(domain):
# HEALTH_PATH "/" only proves the mumble-web HTTP sidecar; it does NOT reflect the voice server.
# After a chaos upgrade redeploy the host-mode 64738 port must be released by the old task and
# rebound by the new one — a window where the app (voice) container isn't yet serving while
# mumble-web still returns 200. backup-bot then execs its sqlite pre-hook into a not-running app
# container → 409. Gate readiness on the voice port being STABLY listening (3 consecutive
# connects) before the harness proceeds to the backup tier. The port is host-published
# (compose.host-ports.yml), so we probe it on the cc-ci host where the run executes.
return [{"tcp_host": "127.0.0.1", "tcp_port": 64738, "stable": 3}]
# The voice server on 64738 is testable on-host ONLY when compose.host-ports.yml is active — i.e.
# the post-upgrade LATEST, not the minimal 0.2.0 base. Read the live COMPOSE_FILE to decide, so the
# SAME probe fn is correct in both phases: the post-install probe (base, no host-ports) returns []
# (HTTP health alone gates the base), the post-upgrade probe (latest, host-ports) gates readiness
# on the voice port being STABLY listening (3 consecutive connects) before the harness proceeds to
# backup — after the chaos upgrade redeploy the host-mode 64738 must be released by the old task and
# rebound by the new one (a window where mumble-web 200s while the voice container isn't yet up, and
# backup-bot would then exec into a not-running app container -> 409).
from harness import abra # lazy: recipe_meta is exec'd with `harness` importable at call time
cf = abra.env_get(domain, "COMPOSE_FILE") or ""
if "compose.host-ports.yml" in cf:
return [{"tcp_host": "127.0.0.1", "tcp_port": 64738, "stable": 3}]
return []

View File

@ -2,16 +2,35 @@
install tier (which proves the mumble-web HTTP sidecar serves over Traefik — the readiness signal).
This overlay ADDS the assertion that mumble's actual purpose — the voice server — is up: the murmur
control channel accepts a TLS connection on the host-published 64738 right after install. (The full
protocol handshake + channel presence is exercised in the custom tier; here we assert the install
produced a listening voice server, not only a web UI.)
control channel accepts a TLS connection on the host-published 64738.
F2-14c: the install tier runs against the upgrade BASE, which is the previous published version
0.2.0+v1.6.870-0. That version PREDATES compose.host-ports.yml (added upstream in 1.0.0), so the base
deploys minimally without it and the voice port is NOT host-published — this on-host voice check is then
not applicable on the base and is SKIPPED (recorded). The voice server is asserted listening on the
post-upgrade LATEST via the READY_PROBE (tcp 3x, gates backup) and the custom-tier full TLS protocol
handshake. When this overlay runs against a host-ports deploy (latest), it asserts the listening server.
"""
import os
import socket
import sys
import time
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import abra # noqa: E402
def test_voice_server_listening(live_app):
cf = abra.env_get(live_app, "COMPOSE_FILE") or ""
if "compose.host-ports.yml" not in cf:
pytest.skip(
"upgrade base (0.2.0) predates compose.host-ports.yml (added in 1.0.0) → voice port not "
"host-published; voice listening asserted on post-upgrade latest (READY_PROBE tcp 3x + "
"custom-tier protocol handshake)"
)
deadline = time.time() + 120
last_err = None
while time.time() < deadline: