Files
OPNexus/updater/test_cli_refresh.py
T
Julian Boldt b020e38d0a
Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
Release v1.31.0
- **Regel-Bereinigung: Entfernen, Bestätigen, Löschen, Wiederherstellen:** Im Tab **Bereinigung** (Regelwerk › Regel-Analyse) lassen sich Kandidaten jetzt auswählen
  (Checkbox, höchstens so viele Regeln je Lauf wie in den Einstellungen; „vermutlich“ nur mit einer ausdrücklichen Einzelbestätigung je Regel, „unbekannt“ nie).
  **Vorschau** zeigt je Firewall (im Cluster zuerst der Backup) die Regeln mit Befund, Sicherheit und Schutzprüfung; **Ausrollen** deaktiviert die Regeln und wendet an.
  Danach läuft ein Countdown: **Bestätigen** heißt „ich erreiche die Firewall noch“, ohne Bestätigung setzt OPNexus **automatisch zurück**; bis dahin geht auch
  **Jetzt zurücksetzen**. Bestätigte Regeln lassen sich nach der Wartezeit (einstellbar, Standard 7 Tage) **löschen**; vorher sichert OPNexus den Regelinhalt, und
  unter **Verlauf der Bereinigungen** kommt die Regel mit **Wiederherstellen** deaktiviert und möglichst an alter Position zurück (sonst mit Hinweis am Ende der Gruppe);
  **Wieder aktivieren** hat wieder ein Bestätigungsfenster. Wichtig: **Ausrollen, Wiederholen und Wiederherstellen darf nur ein Administrator**, eine Vorschau gehört
  dem, der sie erstellt hat (ein Admin öffnet selbst eine neue); **Bestätigen geht nur in der Browser-Sitzung**, nie per API-Token. **Die Bereinigung bleibt gesperrt,
  bis unter Einstellungen die Verwaltungsquellen** (IP-Adressen oder Netze, von denen aus verwaltet wird) **eingetragen sind**: Regeln, die diesen Zugang treffen
  könnten, deaktiviert OPNexus nie. Dort stehen auch die Obergrenze je Lauf, die Wartezeit bis zum Löschen und **weitere Verwaltungsports** (zusätzlich zu
  API-Port, 443, 80, 22). Tragen Sie die Adresse ein, mit der OPNexus selbst bei den Firewalls ankommt (im Cluster für jeden Knoten). Quellen, Ports und
  Firewall-Adressen sehen nur Administratoren. Der Hinweis „Entfernen folgt“ entfällt.
- **Update: neuere CLI aus dem Zielrelease:** `opnexus update` (auch per Klick im Webinterface) holt sich nach dem Pull die CLI aus dem Updater-Image des
  Zielreleases und startet sich damit neu, damit Korrekturen am Update-Ablauf sofort greifen statt erst beim übernächsten Update. Das geschieht nur, wenn
  `cosign` und `cosign.pub` vorhanden sind und die Signatur des Image-**Digests** in diesem Lauf geprüft wurde (ausgeführt wird genau dieser Digest);
  sonst und bei jedem Fehler läuft das Update mit der vorhandenen CLI weiter. Abschalten: `OPNEXUS_NO_CLI_REFRESH=1`. Wirksam wird es ab dem Update
  *von* dieser Version.
2026-10-22 21:04:32 +02:00

147 lines
7.3 KiB
Python

"""refresh_cli in `opnexus` (B8): the CLI takes the newer copy from the target's updater image and restarts itself, but
only with a cosign check of the image DIGEST that succeeded in this run; every other failure continues with the present
CLI. Runs the real function with fake `docker` and `cosign` in front of PATH.
Run: cd updater && python3 -m unittest test_cli_refresh"""
import os
import shutil
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).with_name("opnexus").resolve()
DIGEST = "registry.example/opnexus/opnexus-updater@sha256:" + "a" * 64
FAKE_DOCKER = """#!/bin/sh
echo "$@" >> "$FAKE_LOG"
case "$1" in
pull) [ "${FAKE_PULL_RC:-0}" = 0 ] || exit 1 ;;
image) [ -z "${FAKE_NO_DIGEST:-}" ] && echo "$FAKE_DIGEST" ;;
run) [ "${FAKE_RC:-0}" = 0 ] || exit "$FAKE_RC"; cat "$FAKE_CLI" 2>/dev/null ;;
esac
"""
FAKE_COSIGN = """#!/bin/sh
echo "cosign $*" >> "$FAKE_LOG"
[ "${FAKE_COSIGN_RC:-0}" = 0 ]
"""
NEW_CLI = '#!/usr/bin/env bash\\nrefresh_cli() {\\n :\\n}\\necho "NEW-CLI $*"\\n'.replace("\\n", "\n")
def _exe(path: Path, text: str):
path.write_text(text)
path.chmod(path.stat().st_mode | stat.S_IXUSR)
class RefreshCli(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
(self.tmp / "bin").mkdir()
_exe(self.tmp / "bin" / "docker", FAKE_DOCKER)
_exe(self.tmp / "bin" / "cosign", FAKE_COSIGN)
(self.tmp / ".env").write_text("OPNEXUS_IMAGE_REPO=registry.example/opnexus\n")
(self.tmp / "cosign.pub").write_text("key")
self.log = self.tmp / "calls.log"
self.cli = self.tmp / "new-cli"
def run_refresh(self, new_cli=NEW_CLI, *, env_extra=None, env_file=None, pre="", no_cosign=False, no_pub=False):
self.cli.write_text(new_cli)
if env_file is not None:
(self.tmp / ".env").write_text(env_file)
if no_pub:
(self.tmp / "cosign.pub").unlink()
if no_cosign:
(self.tmp / "bin" / "cosign").unlink()
path = f"{self.tmp / 'bin'}:/usr/bin:/bin" if no_cosign else f"{self.tmp / 'bin'}:{os.environ['PATH']}" # a real cosign must not leak in
env = {**os.environ, "PATH": path, "OPNEXUS_DIR": str(self.tmp),
"FAKE_LOG": str(self.log), "FAKE_CLI": str(self.cli), "FAKE_DIGEST": DIGEST, "TMPDIR": str(self.tmp)}
for name in ("OPNEXUS_NO_CLI_REFRESH", "CLI_REFRESHED"):
env.pop(name, None)
env.update(env_extra or {})
body = f'source <(sed "/^case \\"\\${{1:-}}\\" in/,\\$d" "{SCRIPT}"); {pre} refresh_cli 9.9.9; echo CONTINUED'
return subprocess.run(["bash", "-c", body, str(SCRIPT)], env=env, capture_output=True, text=True, timeout=30)
def calls(self):
return self.log.read_text().splitlines() if self.log.exists() else []
def test_verified_newer_cli_runs_by_digest_with_the_internal_marker_argument(self):
r = self.run_refresh()
self.assertIn("NEW-CLI __refreshed update 9.9.9", r.stdout)
self.assertNotIn("CONTINUED", r.stdout) # exec: the old script does not go on
calls = self.calls()
self.assertTrue(any(c.startswith("cosign verify") and c.endswith(DIGEST) for c in calls)) # signature of the DIGEST
run = next(c for c in calls if c.startswith("run "))
self.assertTrue(run.endswith(f"{DIGEST} /usr/local/bin/opnexus")) # that same digest is executed
self.assertLess([c.split()[0] for c in calls].index("cosign"), [c.split()[0] for c in calls].index("run"))
def test_identical_cli_continues_and_removes_the_image_it_pulled(self):
r = self.run_refresh(SCRIPT.read_text())
self.assertIn("CONTINUED", r.stdout)
self.assertTrue(any(c.startswith("rmi registry.example/opnexus/opnexus-updater:9.9.9") for c in self.calls()))
def test_image_stays_when_the_updater_profile_is_enabled(self):
self.run_refresh(SCRIPT.read_text(), env_file="OPNEXUS_IMAGE_REPO=registry.example/opnexus\nCOMPOSE_PROFILES=updater\n")
self.assertFalse(any(c.startswith("rmi") for c in self.calls()))
def test_nothing_runs_without_a_real_signature_check(self):
for label, kwargs in {"no cosign": {"no_cosign": True}, "no cosign.pub": {"no_pub": True}}.items():
with self.subTest(label):
self.log.unlink(missing_ok=True)
r = self.run_refresh(**kwargs)
self.assertIn("CONTINUED", r.stdout)
self.assertFalse(any(c.startswith(("pull", "run")) for c in self.calls()))
def test_invalid_signature_aborts_the_update_and_runs_nothing(self):
r = self.run_refresh(env_extra={"FAKE_COSIGN_RC": "1"})
self.assertNotEqual(r.returncode, 0)
self.assertNotIn("CONTINUED", r.stdout)
self.assertNotIn("NEW-CLI", r.stdout)
self.assertFalse(any(c.startswith("run ") for c in self.calls()))
def test_pull_failure_never_falls_back_to_a_local_image(self):
r = self.run_refresh(env_extra={"FAKE_PULL_RC": "1"})
self.assertIn("CONTINUED", r.stdout)
self.assertFalse(any(c.startswith(("run ", "cosign")) for c in self.calls()))
def test_unknown_digest_missing_marker_broken_or_empty_copies_are_skipped(self):
for label, kwargs in {
"no digest": {"env_extra": {"FAKE_NO_DIGEST": "1"}},
"old CLI without refresh_cli": {"new_cli": "#!/usr/bin/env bash\necho OLD-CLI\n"},
"syntax error": {"new_cli": "refresh_cli() {\nif then fi (\n"},
"empty": {"new_cli": ""},
"docker run fails": {"env_extra": {"FAKE_RC": "1"}},
}.items():
with self.subTest(label):
r = self.run_refresh(**kwargs)
self.assertIn("CONTINUED", r.stdout)
self.assertNotIn("NEW-CLI", r.stdout)
self.assertNotIn("OLD-CLI", r.stdout)
def test_opt_out_and_missing_repo_do_not_even_ask_docker(self):
for label, kwargs in {"opt-out": {"env_extra": {"OPNEXUS_NO_CLI_REFRESH": "1"}},
"no repo": {"env_file": "POSTGRES_PASSWORD=x\n"}}.items():
with self.subTest(label):
self.log.unlink(missing_ok=True)
r = self.run_refresh(**kwargs)
self.assertIn("CONTINUED", r.stdout)
self.assertEqual(self.calls(), [])
def test_restart_guard_is_a_shell_variable_an_inherited_environment_value_cannot_switch_it_off(self):
inherited = self.run_refresh(env_extra={"CLI_REFRESHED": "1"})
self.assertIn("NEW-CLI __refreshed", inherited.stdout) # inheriting the name changes nothing
self.log.unlink(missing_ok=True)
again = self.run_refresh(pre="CLI_REFRESHED=1;") # the refreshed copy itself does not refresh again
self.assertIn("CONTINUED", again.stdout)
self.assertEqual(self.calls(), [])
def test_refreshed_copy_removes_its_own_temp_file(self):
copy = self.tmp / "opnexus-cli.abc123"
shutil.copy(SCRIPT, copy)
env = {**os.environ, "OPNEXUS_DIR": str(self.tmp)}
subprocess.run(["bash", str(copy), "__refreshed", "bogus"], env=env, capture_output=True, text=True, timeout=30)
self.assertFalse(copy.exists())
if __name__ == "__main__":
unittest.main()