mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-28 06:44:51 -06:00
c0b5952573
TLSClient class for service nodes — discovers console via services table, fetches CA cert, requests cert via ACME, provides SSL contexts: - Console self-registers in services table for discovery - Services auto-discover console URL from DB (no extra config) - Initial cert request over plain HTTP (ACME provides integrity) - Auto-renewal via RenewalManager in server lifespan - Unauthenticated /acme/ca.pem endpoint for node bootstrapping RenewalManager fix (was passing client=None): - Console creates loopback ACME client for self-renewal - Proper async lifecycle (aenter/aexit) with clean shutdown Integration points wired: - Server: TLS init before uvicorn, temp PEM files (0o600, atexit cleanup), auto-renewal in lifespan - Bridge: tls_verify + tls_cert params on all 3 httpx clients - Console collector: tls_verify + tls_cert params on httpx client - Console proxy: mTLS context from TLSManager on proxy clients - Channel gateway: optional SSL params on uvicorn.Config - Console main(): reads tls.enabled, creates TLSManager, passes to create_app with console_url 6 tests for TLSClient (discovery, defaults, backward compat)
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
"""Tests for TLSClient — service node certificate provisioning."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from turnstone.core.storage import get_storage, init_storage, reset_storage
|
|
|
|
lacme = pytest.importorskip("lacme")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _storage(tmp_path):
|
|
"""Initialize ephemeral SQLite storage for each test."""
|
|
reset_storage()
|
|
db = str(tmp_path / "test.db")
|
|
init_storage("sqlite", path=db)
|
|
yield
|
|
reset_storage()
|
|
|
|
|
|
# ── Console URL discovery ─────────────────────────────────────────────────────
|
|
|
|
|
|
def test_discover_console_url():
|
|
"""TLSClient discovers console URL from services table."""
|
|
from turnstone.core.tls import TLSClient
|
|
|
|
storage = get_storage()
|
|
storage.register_service("console", "console", "http://console:8080")
|
|
|
|
client = TLSClient(storage=storage, hostnames=["node-1"])
|
|
url = client._discover_console_url()
|
|
assert url == "http://console:8080"
|
|
|
|
|
|
def test_discover_console_url_missing():
|
|
"""TLSClient raises if no console registered."""
|
|
from turnstone.core.tls import TLSClient
|
|
|
|
client = TLSClient(storage=get_storage(), hostnames=["node-1"])
|
|
with pytest.raises(RuntimeError, match="No console service found"):
|
|
client._discover_console_url()
|
|
|
|
|
|
def test_explicit_console_url_skips_discovery():
|
|
"""When console_url is provided, discovery is skipped."""
|
|
from turnstone.core.tls import TLSClient
|
|
|
|
client = TLSClient(
|
|
storage=get_storage(),
|
|
console_url="http://explicit:9090",
|
|
hostnames=["node-1"],
|
|
)
|
|
assert client._console_url == "http://explicit:9090"
|
|
|
|
|
|
# ── SSL context construction ─────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_ssl_contexts_none_before_init():
|
|
"""SSL contexts are None before init()."""
|
|
from turnstone.core.tls import TLSClient
|
|
|
|
client = TLSClient(
|
|
storage=get_storage(),
|
|
console_url="http://localhost:8080",
|
|
hostnames=["node-1"],
|
|
)
|
|
assert client.get_server_ssl_context() is None
|
|
assert client.get_client_ssl_context() is None
|
|
assert not client.initialized
|
|
|
|
|
|
# ── Backward compatibility ───────────────────────────────────────────────────
|
|
|
|
|
|
def test_bridge_tls_defaults():
|
|
"""Bridge with default TLS params works without changes."""
|
|
from turnstone.mq.bridge import Bridge
|
|
|
|
# Default: tls_verify=True, tls_cert=None — no mTLS
|
|
bridge = Bridge(server_url="http://localhost:8080")
|
|
assert bridge._tls_verify is True
|
|
assert bridge._tls_cert is None
|
|
|
|
|
|
def test_collector_tls_defaults():
|
|
"""Collector with default TLS params works without changes."""
|
|
from turnstone.console.collector import ClusterCollector
|
|
|
|
broker_mock = MagicMock()
|
|
collector = ClusterCollector(broker=broker_mock)
|
|
# Should create httpx client without errors
|
|
assert collector._http_client is not None
|