-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
152 lines (124 loc) · 6.53 KB
/
Copy pathconfig.py
File metadata and controls
152 lines (124 loc) · 6.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""Configuration loaded from environment / .env file.
Mirrors hivemind-core's pydantic-settings pattern so two repos look like
siblings to anyone reading both. All values come from `TEESQL_EXAMPLE_*`
env vars; see `.env.example` for the full list with comments.
Validation is intentional: a misconfigured cluster identity should fail at
process start, not at connect time. Five seconds saved on startup is five
seconds your users wait when something is broken — fail loud instead.
"""
from __future__ import annotations
import re
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
_HEX10 = re.compile(r"^[0-9a-fA-F]{10}$")
_HEX_ADDR = re.compile(r"^(?:0x)?[0-9a-fA-F]{40}$")
_HEX_SECRET = re.compile(r"^[0-9a-fA-F]{64}$")
class Settings(BaseSettings):
"""Customer-side configuration for connecting to a teesql cluster.
Required at construction:
cluster_uuid, manifest_signer, database, cluster_secret
Optional (sensible defaults):
role, dstack_sock, allow_simulator, log_level, tick_seconds
"""
model_config = SettingsConfigDict(
env_prefix="TEESQL_EXAMPLE_",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── Cluster identity ─────────────────────────────────────────────────
cluster_uuid: str = Field(..., description="10-hex on-chain cluster UUID")
manifest_signer: str = Field(..., description="20-byte hex address of dns-controller signer")
database: str = Field(..., description="Postgres database name on the cluster's primary")
# ── Auth ─────────────────────────────────────────────────────────────
cluster_secret: str = Field(..., description="64-hex cluster_secret (NOT the postgres password)")
primary_role: str = Field(
"teesql_readwrite",
description="Postgres role for the primary (write) connection",
)
secondary_role: str = Field(
"teesql_read",
description="Postgres role for the secondary (read-only poller) connection",
)
# ── Demo polling cadence ─────────────────────────────────────────────
# The secondary connection polls `events WHERE id > last_seen` on a
# fixed interval and broadcasts new rows over WebSocket. 50ms is the
# default — enough to feel real-time in the browser, well above the
# mutual-RA-TLS round-trip cost (the connection is held open for the
# process lifetime, NOT re-handshaken per poll). Bump if your cluster
# is geographically distant; values <20ms start hitting psycopg's
# per-query overhead before they hit network.
poll_interval_ms: int = Field(
50,
ge=10,
le=10_000,
description="Secondary poll interval in milliseconds (10..10_000)",
)
# ── HTTP/WebSocket bind ──────────────────────────────────────────────
host: str = Field("0.0.0.0", description="HTTP bind host")
port: int = Field(8080, ge=1, le=65535, description="HTTP bind port")
# ── dstack guest agent ───────────────────────────────────────────────
dstack_sock: str = Field(
"/var/run/dstack.sock",
description="Path to dstack guest-agent socket (or simulator)",
)
allow_simulator: bool = Field(
False,
description="Allow running against the dstack simulator. NEVER true in production.",
)
# ── Operational ──────────────────────────────────────────────────────
log_level: str = Field("INFO", description="logging level: DEBUG | INFO | WARN | ERROR")
# ── Validators ───────────────────────────────────────────────────────
@field_validator("cluster_uuid")
@classmethod
def _check_uuid(cls, v: str) -> str:
if not _HEX10.match(v):
raise ValueError(
f"cluster_uuid must be exactly 10 hex chars, got {v!r}. "
"The operator gives this to you; it matches the on-chain "
"TeeSqlClusterApp.clusterId() string."
)
return v.lower()
@field_validator("manifest_signer")
@classmethod
def _check_signer(cls, v: str) -> str:
if not _HEX_ADDR.match(v):
raise ValueError(
f"manifest_signer must be a 40-hex (20-byte) address, got {v!r}"
)
return v.lower().removeprefix("0x")
@field_validator("cluster_secret")
@classmethod
def _check_secret(cls, v: str) -> str:
if not _HEX_SECRET.match(v):
raise ValueError(
"cluster_secret must be 64 hex chars (32 raw bytes). "
"Generate with `openssl rand -hex 32` if the operator told "
"you to pick one yourself."
)
return v.lower()
@field_validator("log_level")
@classmethod
def _check_level(cls, v: str) -> str:
v = v.upper()
if v not in {"DEBUG", "INFO", "WARNING", "WARN", "ERROR", "CRITICAL"}:
raise ValueError(f"unknown log level: {v}")
return "WARNING" if v == "WARN" else v
# ── Derived properties ───────────────────────────────────────────────
@property
def cluster_domain(self) -> str:
"""The domain the manifest TXT record is published under."""
return f"{self.cluster_uuid}.teesql.com"
@property
def manifest_signer_bytes(self) -> bytes:
"""20-byte signer address ready for ra-tls-verify."""
return bytes.fromhex(self.manifest_signer)
def load_settings(env_file: str | None = None) -> Settings:
"""Construct Settings, optionally pointing at a non-default .env path.
Useful for tests that want to load a fixture .env without polluting the
process environment.
"""
if env_file is not None:
return Settings(_env_file=env_file) # type: ignore[call-arg]
return Settings()