feat(admin-config-tool): drop ha/data/ entirely, match real client TLS

deploy-ha.sh no longer takes a <data-dir> argument or --build-config
flag. Every deploy now generates a fresh self-signed TLS cert/key
(openssl, RSA 2048, 10y validity) directly into the deploy dir's
certs/, and builds cluster.bin directly into controller/ctrl-1/conf/
via xceadmin -- no intermediate staging directory, nothing preserved
across runs. run-ha-soak.sh (/xce-test-ha) updated to match: dropped
its own XCE_HA_DATADIR-based precondition entirely. Updated 4 callers
(run-ha-soak.sh, test-heartbeat-tolerance.sh, test-commit-durability.sh,
both xce-deploy-ha/xce-test-ha SKILL.md docs).

Separately: xcecommon.wire.connect() was loading a client-side TLS
cert, diverging from the real reference client -- netio.c's
create_ssl_context() presents no certificate at all when acting as a
client (cert_file/key_file both NULL, bare TLS_client_method()).
Fixed connect() to match, and since nothing else consumed cert_dir
(verify_mode=CERT_NONE throughout, no CA check either), removed it
end-to-end: discovery.follow_redirects(), push.push_config()/
probe_config(), and the --cert-dir CLI flag.

Server-side cert loading (mock TLS test servers) is unaffected --
moved into a shared session-scoped tls_certs fixture (new
tests/conftest.py, openssl-generated) replacing 3 duplicated
REPO_HA_DATA_DIR/skipif blocks. Net result: those tests no longer skip
under a hermetic nix build sandbox (which never had ha/data/) --
verified 165/165 passed, 0 skipped, via both meson compile and a full
nix build --option sandbox false .#xcecore_debug rebuild (previously
128 passed / 37 skipped there).

ha/data/{xce.crt,xce.key} removed from git tracking. Requirements
2.3/2.4 and 8.3/8.4/8.5 marked superseded in requirements.md with
dated correction notes.
This commit is contained in:
Rongsong Shen 2026-07-22 14:50:51 +00:00
parent db2b5f17f0
commit ee8473bd99
20 changed files with 244 additions and 297 deletions

View file

@ -19,11 +19,6 @@ from click.testing import CliRunner
from xcecommon.schema import load_schema
from xceadmin.cli import cli
REPO_HA_DATA_DIR = Path(__file__).resolve().parents[3] / "ha" / "data"
CERT_FILE = REPO_HA_DATA_DIR / "xce.crt"
KEY_FILE = REPO_HA_DATA_DIR / "xce.key"
CERT_DIR = str(REPO_HA_DATA_DIR)
def test_cli_help_exits_zero() -> None:
"""`xceadmin --help` must run cleanly and print usage text."""
@ -138,15 +133,17 @@ def test_config_build_missing_configs_dir_does_not_raw_traceback(tmp_path):
# ---------------------------------------------------------------------------
def _server_context() -> ssl.SSLContext:
def _server_context(cert_file: Path, key_file: Path) -> ssl.SSLContext:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=str(CERT_FILE), keyfile=str(KEY_FILE))
context.load_cert_chain(certfile=str(cert_file), keyfile=str(key_file))
return context
class _MockController:
def __init__(self, handler):
def __init__(self, handler, cert_file: Path, key_file: Path):
self._handler = handler
self._cert_file = cert_file
self._key_file = key_file
self._listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._listener.bind(("127.0.0.1", 0))
self._listener.listen(1)
@ -160,7 +157,9 @@ class _MockController:
except OSError:
return
try:
tls_conn = _server_context().wrap_socket(raw_conn, server_side=True)
tls_conn = _server_context(self._cert_file, self._key_file).wrap_socket(
raw_conn, server_side=True
)
except (ssl.SSLError, OSError):
raw_conn.close()
return
@ -176,18 +175,12 @@ class _MockController:
self._thread.join(timeout=5)
pytestmark = pytest.mark.skipif(
not (CERT_FILE.is_file() and KEY_FILE.is_file()),
reason="ha/data/xce.crt or ha/data/xce.key not present",
)
@pytest.fixture
def mock_controller():
def mock_controller(tls_certs):
servers: list[_MockController] = []
def _make(handler) -> _MockController:
srv = _MockController(handler)
srv = _MockController(handler, tls_certs.cert_file, tls_certs.key_file)
servers.append(srv)
return srv
@ -223,7 +216,7 @@ def test_config_push_success_exits_zero(mock_controller, cluster_bin):
runner = CliRunner()
result = runner.invoke(
cli,
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}", "--cert-dir", CERT_DIR],
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}"],
)
assert result.exit_code == 0, result.output
@ -251,7 +244,7 @@ def test_config_push_zero_acked_count_exits_nonzero(mock_controller, cluster_bin
runner = CliRunner()
result = runner.invoke(
cli,
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}", "--cert-dir", CERT_DIR],
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}"],
)
assert result.exit_code != 0
@ -267,7 +260,7 @@ def test_config_push_connection_refused_exits_nonzero_not_raw_traceback(cluster_
runner = CliRunner()
result = runner.invoke(
cli,
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{port}", "--cert-dir", CERT_DIR],
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{port}"],
)
assert result.exit_code != 0
@ -297,8 +290,6 @@ def test_config_push_dry_run_reports_generations(mock_controller, cluster_bin):
cluster_bin,
"--controller",
f"127.0.0.1:{srv.port}",
"--cert-dir",
CERT_DIR,
],
)