diff --git a/runner/harness/abra.py b/runner/harness/abra.py index 035cd06..02cc837 100644 --- a/runner/harness/abra.py +++ b/runner/harness/abra.py @@ -126,6 +126,25 @@ def recipe_checkout(recipe: str, version: str) -> None: subprocess.run(["git", "-C", path, "checkout", "-f", "--quiet", version], check=True) +def ensure_secret_versions_from_recipe(recipe: str, domain: str) -> None: + """Fill missing secret versions after a pinned checkout. + + `app new ` creates its .env from whichever recipe tree was + checked out *before* it pins the version. A removed secret in the current + recipe can therefore be missing from the older base's .env. Keep existing + assignments, including versions intentionally changed by install hooks. + """ + sample = os.path.join(recipe_dir(recipe), ".env.sample") + if not os.path.isfile(sample): + return + pattern = re.compile(r"^\s*(SECRET_[A-Z0-9_]+_VERSION)=(v[A-Za-z0-9._-]+)") + with open(sample) as fh: + for line in fh: + match = pattern.match(line) + if match and env_get(domain, match.group(1)) is None: + env_set(domain, match.group(1), match.group(2)) + + def has_lightweight_version_tags(recipe: str) -> bool: """True if the recipe's local checkout has any LIGHTWEIGHT (non-annotated) version tag. diff --git a/runner/harness/lifecycle.py b/runner/harness/lifecycle.py index 1240b8d..f9ae686 100644 --- a/runner/harness/lifecycle.py +++ b/runner/harness/lifecycle.py @@ -444,6 +444,7 @@ def deploy_app( ) elif version: abra.recipe_checkout(recipe, version) + abra.ensure_secret_versions_from_recipe(recipe, domain) # A pinned (non-chaos) deploy runs `abra recipe lint`, which FATAs R014 ('only annotated # tags') if the upstream recipe ships a stray lightweight version tag (e.g. lasuite-meet's # 0.3.0+v1.16.0). In that case deploy the EXPLICITLY-checked-out pinned version with chaos: diff --git a/tests/lasuite-docs/install_steps.sh b/tests/lasuite-docs/install_steps.sh index 85e182e..dc469f0 100755 --- a/tests/lasuite-docs/install_steps.sh +++ b/tests/lasuite-docs/install_steps.sh @@ -20,6 +20,25 @@ set -euo pipefail : "${CCCI_APP_DOMAIN:?missing}" ENV_PATH="${CCCI_APP_ENV:?missing}" +# The v6 release requires two RSA PEM secrets with generate=false. CI must +# supply those operator-managed keys for the published base recipe; a PR head +# that provides its own keys still runs without these secrets after upgrade. +RECIPE_DIR="${ABRA_DIR:-$HOME/.abra}/recipes/lasuite-docs" +if grep -qE '^[[:space:]]{2}(jwt_pk|yhub_jwt_pk):' "$RECIPE_DIR/compose.yml"; then + STACK_NAME="${CCCI_APP_DOMAIN//./_}" + for SECRET_NAME in jwt_pk yhub_jwt_pk; do + ENV_NAME="SECRET_${SECRET_NAME^^}_VERSION" + VERSION=$(sed -n "s/^${ENV_NAME}=\([^ #]*\).*/\1/p" "$ENV_PATH" | tail -1) + : "${VERSION:?missing $ENV_NAME}" + DOCKER_SECRET="${STACK_NAME}_${SECRET_NAME}_${VERSION}" + if ! docker secret inspect "$DOCKER_SECRET" >/dev/null 2>&1; then + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null \ + | docker secret create "$DOCKER_SECRET" - >/dev/null + echo " install_steps: supplied ${SECRET_NAME} for the published base recipe" + fi + done +fi + # 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 diff --git a/tests/unit/test_abra_secret_versions.py b/tests/unit/test_abra_secret_versions.py new file mode 100644 index 0000000..4d5efa3 --- /dev/null +++ b/tests/unit/test_abra_secret_versions.py @@ -0,0 +1,30 @@ +"""Pinned base recipes need the secret versions declared by their own tag.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import abra # noqa: E402 + + +def test_pinned_base_adds_removed_secret_without_overwriting_existing_version( + monkeypatch, tmp_path +): + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("ABRA_DIR", str(tmp_path / ".abra")) + recipe = tmp_path / ".abra" / "recipes" / "lasuite-docs" + recipe.mkdir(parents=True) + (recipe / ".env.sample").write_text( + "SECRET_COLLAB_SS_VERSION=v1\n" + "SECRET_JWT_PK_VERSION=v1 # generate=false\n" + "# SECRET_UNUSED_VERSION=v1\n" + ) + env = tmp_path / ".abra" / "servers" / "default" / "test.example.env" + env.parent.mkdir(parents=True) + env.write_text("SECRET_JWT_PK_VERSION=v2\n") + + abra.ensure_secret_versions_from_recipe("lasuite-docs", "test.example") + + assert abra.env_get("test.example", "SECRET_COLLAB_SS_VERSION") == "v1" + assert abra.env_get("test.example", "SECRET_JWT_PK_VERSION") == "v2" + assert abra.env_get("test.example", "SECRET_UNUSED_VERSION") is None diff --git a/tests/unit/test_previous.py b/tests/unit/test_previous.py index 859db76..26ebbe1 100644 --- a/tests/unit/test_previous.py +++ b/tests/unit/test_previous.py @@ -127,7 +127,13 @@ def test_previous_overlay_deploys_checked_out_base_without_clean_tree_gate(monke "prepull_images", ): monkeypatch.setattr(lifecycle, name, lambda *a, **kw: None) - for name in ("app_config_remove", "app_new", "env_set", "secret_generate"): + for name in ( + "app_config_remove", + "app_new", + "env_set", + "secret_generate", + "ensure_secret_versions_from_recipe", + ): monkeypatch.setattr(lifecycle.abra, name, lambda *a, **kw: None) monkeypatch.setattr(lifecycle.meta_mod, "extra_env", lambda *a: {}) monkeypatch.setattr(lifecycle.meta_mod, "hook_ctx", lambda *a: None)