sovereign 0.19.3__py3-none-any.whl → 1.0.0b148__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sovereign might be problematic. Click here for more details.
- sovereign/__init__.py +13 -81
- sovereign/app.py +59 -48
- sovereign/cache/__init__.py +172 -0
- sovereign/cache/backends/__init__.py +110 -0
- sovereign/cache/backends/s3.py +143 -0
- sovereign/cache/filesystem.py +73 -0
- sovereign/cache/types.py +15 -0
- sovereign/configuration.py +573 -0
- sovereign/constants.py +1 -0
- sovereign/context.py +271 -104
- sovereign/dynamic_config/__init__.py +113 -0
- sovereign/dynamic_config/deser.py +78 -0
- sovereign/dynamic_config/loaders.py +120 -0
- sovereign/events.py +49 -0
- sovereign/logging/access_logger.py +85 -0
- sovereign/logging/application_logger.py +54 -0
- sovereign/logging/base_logger.py +41 -0
- sovereign/logging/bootstrapper.py +36 -0
- sovereign/logging/types.py +10 -0
- sovereign/middlewares.py +8 -7
- sovereign/modifiers/lib.py +1 -0
- sovereign/rendering.py +192 -0
- sovereign/response_class.py +18 -0
- sovereign/server.py +93 -35
- sovereign/sources/file.py +1 -1
- sovereign/sources/inline.py +1 -0
- sovereign/sources/lib.py +1 -0
- sovereign/sources/poller.py +296 -53
- sovereign/statistics.py +17 -20
- sovereign/templates/base.html +59 -46
- sovereign/templates/resources.html +203 -102
- sovereign/testing/loaders.py +8 -0
- sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
- sovereign/tracing.py +102 -0
- sovereign/types.py +299 -0
- sovereign/utils/auth.py +26 -13
- sovereign/utils/crypto/__init__.py +0 -0
- sovereign/utils/crypto/crypto.py +135 -0
- sovereign/utils/crypto/suites/__init__.py +21 -0
- sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
- sovereign/utils/crypto/suites/base_cipher.py +21 -0
- sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
- sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
- sovereign/utils/dictupdate.py +2 -1
- sovereign/utils/eds.py +37 -21
- sovereign/utils/mock.py +54 -16
- sovereign/utils/resources.py +17 -0
- sovereign/utils/version_info.py +8 -0
- sovereign/views/__init__.py +4 -0
- sovereign/views/api.py +61 -0
- sovereign/views/crypto.py +46 -15
- sovereign/views/discovery.py +37 -116
- sovereign/views/healthchecks.py +87 -18
- sovereign/views/interface.py +112 -112
- sovereign/worker.py +204 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +79 -76
- sovereign-1.0.0b148.dist-info/RECORD +77 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/WHEEL +1 -1
- sovereign-1.0.0b148.dist-info/entry_points.txt +38 -0
- sovereign_files/__init__.py +0 -0
- sovereign_files/static/darkmode.js +51 -0
- sovereign_files/static/node_expression.js +42 -0
- sovereign_files/static/panel.js +76 -0
- sovereign_files/static/resources.css +246 -0
- sovereign_files/static/resources.js +642 -0
- sovereign_files/static/sass/style.scss +33 -0
- sovereign_files/static/style.css +16143 -0
- sovereign_files/static/style.css.map +1 -0
- sovereign/config_loader.py +0 -225
- sovereign/discovery.py +0 -175
- sovereign/logs.py +0 -131
- sovereign/schemas.py +0 -780
- sovereign/static/sass/style.scss +0 -27
- sovereign/static/style.css +0 -13553
- sovereign/templates/ul_filter.html +0 -22
- sovereign/utils/crypto.py +0 -103
- sovereign/views/admin.py +0 -120
- sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
- sovereign-0.19.3.dist-info/RECORD +0 -47
- sovereign-0.19.3.dist-info/entry_points.txt +0 -10
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from typing_extensions import final
|
|
6
|
+
from cachelib import FileSystemCache
|
|
7
|
+
|
|
8
|
+
from sovereign.types import DiscoveryRequest
|
|
9
|
+
from sovereign.configuration import config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
INIT = """
|
|
13
|
+
CREATE TABLE IF NOT EXISTS registered_clients (
|
|
14
|
+
client_id TEXT PRIMARY KEY,
|
|
15
|
+
discovery_request TEXT NOT NULL
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
INSERT = "INSERT OR IGNORE INTO registered_clients (client_id, discovery_request) VALUES (?, ?)"
|
|
19
|
+
LIST = "SELECT client_id, discovery_request FROM registered_clients"
|
|
20
|
+
SEARCH = "SELECT 1 FROM registered_clients WHERE client_id = ?"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@final
|
|
24
|
+
class FilesystemCache:
|
|
25
|
+
def __init__(self, cache_path: str | None = None, default_timeout: int = 0):
|
|
26
|
+
self.cache_path = cache_path or config.cache.local_fs_path
|
|
27
|
+
self.default_timeout = default_timeout
|
|
28
|
+
self._cache = FileSystemCache(
|
|
29
|
+
cache_dir=self.cache_path,
|
|
30
|
+
default_timeout=self.default_timeout,
|
|
31
|
+
hash_method=sha256,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Initialize SQLite for client registration
|
|
35
|
+
Path(self.cache_path).mkdir(parents=True, exist_ok=True)
|
|
36
|
+
self._db_path = Path(self.cache_path) / "clients.db"
|
|
37
|
+
self._init_db()
|
|
38
|
+
|
|
39
|
+
def _init_db(self):
|
|
40
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
41
|
+
_ = conn.execute(INIT)
|
|
42
|
+
|
|
43
|
+
def get(self, key):
|
|
44
|
+
return self._cache.get(key)
|
|
45
|
+
|
|
46
|
+
def set(self, key, value, timeout=None):
|
|
47
|
+
return self._cache.set(key, value, timeout)
|
|
48
|
+
|
|
49
|
+
def delete(self, key):
|
|
50
|
+
return self._cache.delete(key)
|
|
51
|
+
|
|
52
|
+
def clear(self):
|
|
53
|
+
return self._cache.clear()
|
|
54
|
+
|
|
55
|
+
def register(self, id: str, req: DiscoveryRequest) -> None:
|
|
56
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
57
|
+
_ = conn.execute(INSERT, (id, json.dumps(req.model_dump())))
|
|
58
|
+
|
|
59
|
+
def registered(self, id: str) -> bool:
|
|
60
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
61
|
+
cursor = conn.execute(SEARCH, (id,))
|
|
62
|
+
return cursor.fetchone() is not None
|
|
63
|
+
|
|
64
|
+
def get_registered_clients(self) -> list[tuple[str, DiscoveryRequest]]:
|
|
65
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
66
|
+
cursor = conn.execute(LIST)
|
|
67
|
+
rows = cursor.fetchall()
|
|
68
|
+
|
|
69
|
+
result = []
|
|
70
|
+
for client_id, req_json in rows:
|
|
71
|
+
req = DiscoveryRequest.model_validate(json.loads(req_json))
|
|
72
|
+
result.append((client_id, req))
|
|
73
|
+
return result
|
sovereign/cache/types.py
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
import importlib
|
|
5
|
+
import multiprocessing
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from os import getenv
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Dict,
|
|
13
|
+
Mapping,
|
|
14
|
+
Optional,
|
|
15
|
+
Self,
|
|
16
|
+
Union,
|
|
17
|
+
Callable,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from croniter import CroniterBadCronError, croniter
|
|
21
|
+
from pydantic import (
|
|
22
|
+
BaseModel,
|
|
23
|
+
Field,
|
|
24
|
+
SecretStr,
|
|
25
|
+
model_validator,
|
|
26
|
+
field_validator,
|
|
27
|
+
)
|
|
28
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
29
|
+
|
|
30
|
+
from sovereign.dynamic_config import Loadable
|
|
31
|
+
from sovereign.types import XdsTemplate
|
|
32
|
+
from sovereign.utils.crypto.suites import EncryptionType
|
|
33
|
+
from sovereign.utils import dictupdate
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CacheStrategy(str, Enum):
|
|
37
|
+
context = "context"
|
|
38
|
+
content = "content"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SourceData(BaseModel):
|
|
42
|
+
scopes: Dict[str, list[Dict[str, Any]]] = Field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConfiguredSource(BaseModel):
|
|
46
|
+
type: str
|
|
47
|
+
config: Dict[str, Any]
|
|
48
|
+
scope: str = "default" # backward compatibility
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class StatsdConfig(BaseModel):
|
|
52
|
+
host: str = "127.0.0.1"
|
|
53
|
+
port: Union[int, str] = 8125
|
|
54
|
+
tags: Dict[str, Union[Loadable, str]] = dict()
|
|
55
|
+
namespace: str = "sovereign"
|
|
56
|
+
enabled: bool = False
|
|
57
|
+
use_ms: bool = True
|
|
58
|
+
|
|
59
|
+
@field_validator("host", mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def load_host(cls, v: str) -> Any:
|
|
62
|
+
return Loadable.from_legacy_fmt(v).load()
|
|
63
|
+
|
|
64
|
+
@field_validator("port", mode="before")
|
|
65
|
+
@classmethod
|
|
66
|
+
def load_port(cls, v: Union[int, str]) -> Any:
|
|
67
|
+
if isinstance(v, int):
|
|
68
|
+
return v
|
|
69
|
+
elif isinstance(v, str):
|
|
70
|
+
return Loadable.from_legacy_fmt(v).load()
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(f"Received an invalid port: {v}")
|
|
73
|
+
|
|
74
|
+
@field_validator("tags", mode="before")
|
|
75
|
+
@classmethod
|
|
76
|
+
def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
|
|
77
|
+
ret = dict()
|
|
78
|
+
for key, value in v.items():
|
|
79
|
+
if isinstance(value, dict):
|
|
80
|
+
ret[key] = Loadable(**value).load()
|
|
81
|
+
elif isinstance(value, str):
|
|
82
|
+
ret[key] = Loadable.from_legacy_fmt(value).load()
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError(f"Received an invalid tag for statsd: {value}")
|
|
85
|
+
return ret
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SovereignAsgiConfig(BaseSettings):
|
|
89
|
+
user: Optional[str] = Field(None, alias="SOVEREIGN_USER")
|
|
90
|
+
host: str = Field("0.0.0.0", alias="SOVEREIGN_HOST")
|
|
91
|
+
port: int = Field(8080, alias="SOVEREIGN_PORT")
|
|
92
|
+
keepalive: int = Field(5, alias="SOVEREIGN_KEEPALIVE")
|
|
93
|
+
workers: int = Field(
|
|
94
|
+
default_factory=lambda: multiprocessing.cpu_count() * 2 + 1,
|
|
95
|
+
alias="SOVEREIGN_WORKERS",
|
|
96
|
+
)
|
|
97
|
+
threads: int = Field(1, alias="SOVEREIGN_THREADS")
|
|
98
|
+
reuse_port: bool = True
|
|
99
|
+
preload_app: bool = Field(True, alias="SOVEREIGN_PRELOAD")
|
|
100
|
+
log_level: str = "warning"
|
|
101
|
+
worker_class: str = "uvicorn.workers.UvicornWorker"
|
|
102
|
+
worker_timeout: int = Field(30, alias="SOVEREIGN_WORKER_TIMEOUT")
|
|
103
|
+
worker_tmp_dir: Optional[str] = Field(None, alias="SOVEREIGN_WORKER_TMP_DIR")
|
|
104
|
+
graceful_timeout: Optional[int] = Field(None)
|
|
105
|
+
max_requests: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS")
|
|
106
|
+
max_requests_jitter: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS_JITTER")
|
|
107
|
+
model_config = SettingsConfigDict(
|
|
108
|
+
env_file=".env",
|
|
109
|
+
extra="ignore",
|
|
110
|
+
env_file_encoding="utf-8",
|
|
111
|
+
populate_by_name=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@model_validator(mode="after")
|
|
115
|
+
def validate_graceful_timeout(self) -> Self:
|
|
116
|
+
if self.graceful_timeout is None:
|
|
117
|
+
self.graceful_timeout = self.worker_timeout * 2
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def as_gunicorn_conf(self) -> Dict[str, Any]:
|
|
121
|
+
ret = {
|
|
122
|
+
"bind": ":".join(map(str, [self.host, self.port])),
|
|
123
|
+
"keepalive": self.keepalive,
|
|
124
|
+
"reuse_port": self.reuse_port,
|
|
125
|
+
"preload_app": self.preload_app,
|
|
126
|
+
"loglevel": self.log_level,
|
|
127
|
+
"timeout": self.worker_timeout,
|
|
128
|
+
"threads": self.threads,
|
|
129
|
+
"workers": self.workers,
|
|
130
|
+
"worker_class": self.worker_class,
|
|
131
|
+
"graceful_timeout": self.graceful_timeout,
|
|
132
|
+
"max_requests": self.max_requests,
|
|
133
|
+
"max_requests_jitter": self.max_requests_jitter,
|
|
134
|
+
}
|
|
135
|
+
if self.worker_tmp_dir is not None:
|
|
136
|
+
ret["worker_tmp_dir"] = self.worker_tmp_dir
|
|
137
|
+
return ret
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TemplateSpecification(BaseModel):
|
|
141
|
+
type: str
|
|
142
|
+
spec: Loadable
|
|
143
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class NodeMatching(BaseSettings):
|
|
147
|
+
enabled: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING_ENABLED")
|
|
148
|
+
source_key: str = Field("service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY")
|
|
149
|
+
node_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
|
|
150
|
+
model_config = SettingsConfigDict(
|
|
151
|
+
env_file=".env",
|
|
152
|
+
extra="ignore",
|
|
153
|
+
env_file_encoding="utf-8",
|
|
154
|
+
populate_by_name=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class EncryptionConfig:
|
|
160
|
+
encryption_key: str
|
|
161
|
+
encryption_type: EncryptionType
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class AuthConfiguration(BaseSettings):
|
|
165
|
+
enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
|
|
166
|
+
auth_passwords: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_AUTH_PASSWORDS")
|
|
167
|
+
encryption_key: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_ENCRYPTION_KEY")
|
|
168
|
+
model_config = SettingsConfigDict(
|
|
169
|
+
env_file=".env",
|
|
170
|
+
extra="ignore",
|
|
171
|
+
env_file_encoding="utf-8",
|
|
172
|
+
populate_by_name=True,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _create_encryption_config(encryption_key_setting: str) -> EncryptionConfig:
|
|
177
|
+
encryption_key, _, encryption_type_raw = encryption_key_setting.partition(":")
|
|
178
|
+
if encryption_type_raw:
|
|
179
|
+
encryption_type = EncryptionType(encryption_type_raw)
|
|
180
|
+
else:
|
|
181
|
+
encryption_type = EncryptionType.FERNET
|
|
182
|
+
return EncryptionConfig(encryption_key, encryption_type)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def encryption_configs(self) -> tuple[EncryptionConfig, ...]:
|
|
186
|
+
secret_values = self.encryption_key.get_secret_value().split()
|
|
187
|
+
|
|
188
|
+
configs = tuple(
|
|
189
|
+
self._create_encryption_config(encryption_key_setting)
|
|
190
|
+
for encryption_key_setting in secret_values
|
|
191
|
+
)
|
|
192
|
+
return configs
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ApplicationLogConfiguration(BaseSettings):
|
|
196
|
+
enabled: bool = Field(False, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS")
|
|
197
|
+
log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_APPLICATION_LOG_FORMAT")
|
|
198
|
+
# currently only support /dev/stdout as JSON
|
|
199
|
+
model_config = SettingsConfigDict(
|
|
200
|
+
env_file=".env",
|
|
201
|
+
extra="ignore",
|
|
202
|
+
env_file_encoding="utf-8",
|
|
203
|
+
populate_by_name=True,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class AccessLogConfiguration(BaseSettings):
|
|
208
|
+
enabled: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
|
|
209
|
+
log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_LOG_FORMAT")
|
|
210
|
+
ignore_empty_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
|
|
211
|
+
model_config = SettingsConfigDict(
|
|
212
|
+
env_file=".env",
|
|
213
|
+
extra="ignore",
|
|
214
|
+
env_file_encoding="utf-8",
|
|
215
|
+
populate_by_name=True,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class LoggingConfiguration(BaseSettings):
|
|
220
|
+
application_logs: ApplicationLogConfiguration = ApplicationLogConfiguration()
|
|
221
|
+
access_logs: AccessLogConfiguration = AccessLogConfiguration()
|
|
222
|
+
log_source_diffs: bool = False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class ContextFileCache(BaseSettings):
|
|
226
|
+
file_path: str = ".sovereign_context_cache"
|
|
227
|
+
algo: Optional[str] = None
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def path(self) -> Path:
|
|
231
|
+
return Path(self.file_path)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def hasher(self) -> Callable[[Any], Any]:
|
|
235
|
+
lib = importlib.import_module("hashlib")
|
|
236
|
+
return getattr(lib, self.algo or "sha256")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ContextConfiguration(BaseSettings):
|
|
240
|
+
context: Dict[str, Loadable] = {}
|
|
241
|
+
cache: ContextFileCache = ContextFileCache()
|
|
242
|
+
refresh: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
|
|
243
|
+
refresh_rate: Optional[int] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE")
|
|
244
|
+
refresh_cron: Optional[str] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON")
|
|
245
|
+
refresh_num_retries: int = Field(3, alias="SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES")
|
|
246
|
+
refresh_retry_interval_secs: int = Field(
|
|
247
|
+
10, alias="SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
|
|
248
|
+
)
|
|
249
|
+
cooldown: int = Field(15, alias="SOVEREIGN_CONTEXT_REFRESH_COOLDOWN")
|
|
250
|
+
model_config = SettingsConfigDict(
|
|
251
|
+
env_file=".env",
|
|
252
|
+
extra="ignore",
|
|
253
|
+
env_file_encoding="utf-8",
|
|
254
|
+
populate_by_name=True,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def context_from_legacy(context: Dict[str, str]) -> Dict[str, Loadable]:
|
|
259
|
+
ret = dict()
|
|
260
|
+
for key, value in context.items():
|
|
261
|
+
ret[key] = Loadable.from_legacy_fmt(value)
|
|
262
|
+
return ret
|
|
263
|
+
|
|
264
|
+
@model_validator(mode="after")
|
|
265
|
+
def validate_single_use_refresh_method(self) -> Self:
|
|
266
|
+
if (self.refresh_rate is not None) and (self.refresh_cron is not None):
|
|
267
|
+
raise RuntimeError(
|
|
268
|
+
f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {self.refresh_rate=} and {self.refresh_cron=}"
|
|
269
|
+
)
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
@model_validator(mode="before")
|
|
273
|
+
@classmethod
|
|
274
|
+
def set_default_refresh_rate(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
275
|
+
refresh_rate = values.get("refresh_rate")
|
|
276
|
+
refresh_cron = values.get("refresh_cron")
|
|
277
|
+
|
|
278
|
+
if (refresh_rate is None) and (refresh_cron is None):
|
|
279
|
+
values["refresh_rate"] = 3600
|
|
280
|
+
return values
|
|
281
|
+
|
|
282
|
+
@field_validator("refresh_cron")
|
|
283
|
+
@classmethod
|
|
284
|
+
def validate_refresh_cron(cls, v: Optional[str]) -> Optional[str]:
|
|
285
|
+
if v is None:
|
|
286
|
+
return v
|
|
287
|
+
if not croniter.is_valid(v):
|
|
288
|
+
raise CroniterBadCronError(f"'{v}' is not a valid cron expression")
|
|
289
|
+
return v
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class SourcesConfiguration(BaseSettings):
|
|
293
|
+
refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
|
|
294
|
+
max_retries: int = Field(3, alias="SOVEREIGN_SOURCES_MAX_RETRIES")
|
|
295
|
+
retry_delay: int = Field(1, alias="SOVEREIGN_SOURCES_RETRY_DELAY")
|
|
296
|
+
model_config = SettingsConfigDict(
|
|
297
|
+
env_file=".env",
|
|
298
|
+
extra="ignore",
|
|
299
|
+
env_file_encoding="utf-8",
|
|
300
|
+
populate_by_name=True,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class TracingConfig(BaseSettings):
|
|
305
|
+
enabled: bool = Field(False)
|
|
306
|
+
collector: str = Field("notset")
|
|
307
|
+
endpoint: str = Field("/v2/api/spans")
|
|
308
|
+
trace_id_128bit: bool = Field(True)
|
|
309
|
+
tags: Dict[str, Union[Loadable, str]] = dict()
|
|
310
|
+
model_config = SettingsConfigDict(
|
|
311
|
+
env_file=".env",
|
|
312
|
+
extra="ignore",
|
|
313
|
+
env_file_encoding="utf-8",
|
|
314
|
+
populate_by_name=True,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
@field_validator("tags", mode="before")
|
|
318
|
+
@classmethod
|
|
319
|
+
def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
|
|
320
|
+
ret = dict()
|
|
321
|
+
for key, value in v.items():
|
|
322
|
+
if isinstance(value, dict):
|
|
323
|
+
ret[key] = Loadable(**value).load()
|
|
324
|
+
elif isinstance(value, str):
|
|
325
|
+
ret[key] = Loadable.from_legacy_fmt(value).load()
|
|
326
|
+
else:
|
|
327
|
+
raise ValueError(f"Received an invalid tag for tracing: {value}")
|
|
328
|
+
return ret
|
|
329
|
+
|
|
330
|
+
@model_validator(mode="before")
|
|
331
|
+
@classmethod
|
|
332
|
+
def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
333
|
+
if enabled := getenv("SOVEREIGN_TRACING_ENABLED"):
|
|
334
|
+
values["enabled"] = enabled
|
|
335
|
+
if collector := getenv("SOVEREIGN_TRACING_COLLECTOR"):
|
|
336
|
+
values["collector"] = collector
|
|
337
|
+
if endpoint := getenv("SOVEREIGN_TRACING_ENDPOINT"):
|
|
338
|
+
values["endpoint"] = endpoint
|
|
339
|
+
if trace_id_128bit := getenv("SOVEREIGN_TRACING_TRACE_ID_128BIT"):
|
|
340
|
+
values["trace_id_128bit"] = trace_id_128bit
|
|
341
|
+
return values
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class SupervisordConfig(BaseSettings):
|
|
345
|
+
nodaemon: bool = Field(True, alias="SOVEREIGN_SUPERVISORD_NODAEMON")
|
|
346
|
+
loglevel: str = Field("error", alias="SOVEREIGN_SUPERVISORD_LOGLEVEL")
|
|
347
|
+
pidfile: str = Field("/tmp/supervisord.pid", alias="SOVEREIGN_SUPERVISORD_PIDFILE")
|
|
348
|
+
logfile: str = Field("/tmp/supervisord.log", alias="SOVEREIGN_SUPERVISORD_LOGFILE")
|
|
349
|
+
directory: str = Field("%(here)s", alias="SOVEREIGN_SUPERVISORD_DIRECTORY")
|
|
350
|
+
model_config = SettingsConfigDict(
|
|
351
|
+
env_file=".env",
|
|
352
|
+
extra="ignore",
|
|
353
|
+
env_file_encoding="utf-8",
|
|
354
|
+
populate_by_name=True,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class LegacyConfig(BaseSettings):
|
|
359
|
+
regions: Optional[list[str]] = None
|
|
360
|
+
eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
|
|
361
|
+
dns_hard_fail: Optional[bool] = Field(None, alias="SOVEREIGN_DNS_HARD_FAIL")
|
|
362
|
+
environment: Optional[str] = Field(None, alias="SOVEREIGN_ENVIRONMENT")
|
|
363
|
+
model_config = SettingsConfigDict(
|
|
364
|
+
env_file=".env",
|
|
365
|
+
extra="ignore",
|
|
366
|
+
env_file_encoding="utf-8",
|
|
367
|
+
populate_by_name=True,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@field_validator("regions")
|
|
371
|
+
@classmethod
|
|
372
|
+
def regions_is_set(cls, v: Optional[list[str]]) -> list[str]:
|
|
373
|
+
if v is not None:
|
|
374
|
+
warnings.warn(
|
|
375
|
+
"Setting regions via config is deprecated. "
|
|
376
|
+
"It is suggested to use a modifier or template "
|
|
377
|
+
"logic in order to achieve the same goal.",
|
|
378
|
+
DeprecationWarning,
|
|
379
|
+
)
|
|
380
|
+
return v
|
|
381
|
+
else:
|
|
382
|
+
return []
|
|
383
|
+
|
|
384
|
+
@field_validator("eds_priority_matrix")
|
|
385
|
+
@classmethod
|
|
386
|
+
def eds_priority_matrix_is_set(
|
|
387
|
+
cls, v: Optional[Dict[str, Dict[str, Any]]]
|
|
388
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
389
|
+
if v is not None:
|
|
390
|
+
warnings.warn(
|
|
391
|
+
"Setting eds_priority_matrix via config is deprecated. "
|
|
392
|
+
"It is suggested to use a modifier or template "
|
|
393
|
+
"logic in order to achieve the same goal.",
|
|
394
|
+
DeprecationWarning,
|
|
395
|
+
)
|
|
396
|
+
return v
|
|
397
|
+
else:
|
|
398
|
+
return {}
|
|
399
|
+
|
|
400
|
+
@field_validator("dns_hard_fail")
|
|
401
|
+
@classmethod
|
|
402
|
+
def dns_hard_fail_is_set(cls, v: Optional[bool]) -> bool:
|
|
403
|
+
if v is not None:
|
|
404
|
+
warnings.warn(
|
|
405
|
+
"Setting dns_hard_fail via config is deprecated. "
|
|
406
|
+
"It is suggested to supply a module that can perform "
|
|
407
|
+
"dns resolution to template_context, so that it can "
|
|
408
|
+
"be used via templates instead.",
|
|
409
|
+
DeprecationWarning,
|
|
410
|
+
)
|
|
411
|
+
return v
|
|
412
|
+
else:
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
@field_validator("environment")
|
|
416
|
+
@classmethod
|
|
417
|
+
def environment_is_set(cls, v: Optional[str]) -> Optional[str]:
|
|
418
|
+
if v is not None:
|
|
419
|
+
warnings.warn(
|
|
420
|
+
"Setting environment via config is deprecated. "
|
|
421
|
+
"It is suggested to configure this value through log_fmt "
|
|
422
|
+
"instead.",
|
|
423
|
+
DeprecationWarning,
|
|
424
|
+
)
|
|
425
|
+
return v
|
|
426
|
+
else:
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class TemplateConfiguration(BaseModel):
|
|
431
|
+
default: list[TemplateSpecification]
|
|
432
|
+
versions: dict[str, list[TemplateSpecification]] = Field(default_factory=dict)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def default_hash_rules():
|
|
436
|
+
return [
|
|
437
|
+
# Sovereign internal fields
|
|
438
|
+
"template.version",
|
|
439
|
+
"is_internal_request",
|
|
440
|
+
"desired_controlplane",
|
|
441
|
+
"resource_type",
|
|
442
|
+
"api_version",
|
|
443
|
+
"envoy_version",
|
|
444
|
+
# Envoy fields from the real Discovery Request
|
|
445
|
+
"resource_names",
|
|
446
|
+
"node.cluster",
|
|
447
|
+
"node.locality",
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class CacheBackendConfig(BaseModel):
|
|
452
|
+
type: str = Field(..., description="Cache backend type (e.g., 'redis', 's3')")
|
|
453
|
+
config: dict[str, Any] = Field(
|
|
454
|
+
default_factory=dict, description="Backend-specific configuration"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class CacheConfiguration(BaseModel):
|
|
459
|
+
hash_rules: list[str] = Field(
|
|
460
|
+
default_factory=default_hash_rules,
|
|
461
|
+
description="The set of JMES expressions against incoming Discovery Requests used to form a cache key.",
|
|
462
|
+
)
|
|
463
|
+
read_timeout: float = Field(
|
|
464
|
+
5.0,
|
|
465
|
+
description="How long to block when trying to read from the cache before giving up",
|
|
466
|
+
)
|
|
467
|
+
local_fs_path: str = Field(
|
|
468
|
+
"/var/run/sovereign_cache",
|
|
469
|
+
description="Local filesystem cache path. Used to provide fast responses to clients and reduce hits against remote cache backend.",
|
|
470
|
+
)
|
|
471
|
+
remote_backend: CacheBackendConfig | None = Field(
|
|
472
|
+
None, description="Remote cache backend configuration"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class SovereignConfigv2(BaseSettings):
|
|
477
|
+
# Config generation
|
|
478
|
+
templates: TemplateConfiguration
|
|
479
|
+
template_context: ContextConfiguration = ContextConfiguration()
|
|
480
|
+
|
|
481
|
+
# Web/Discovery
|
|
482
|
+
authentication: AuthConfiguration = AuthConfiguration()
|
|
483
|
+
|
|
484
|
+
# Cache
|
|
485
|
+
cache: CacheConfiguration = CacheConfiguration()
|
|
486
|
+
|
|
487
|
+
# Worker
|
|
488
|
+
worker_host: Optional[str] = Field("localhost", alias="SOVEREIGN_WORKER_HOST")
|
|
489
|
+
worker_port: Optional[int] = Field(9080, alias="SOVEREIGN_WORKER_PORT")
|
|
490
|
+
|
|
491
|
+
# Supervisord settings
|
|
492
|
+
supervisord: SupervisordConfig = SupervisordConfig()
|
|
493
|
+
|
|
494
|
+
# Misc
|
|
495
|
+
tracing: Optional[TracingConfig] = Field(default_factory=TracingConfig)
|
|
496
|
+
debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
|
|
497
|
+
logging: LoggingConfiguration = LoggingConfiguration()
|
|
498
|
+
statsd: StatsdConfig = StatsdConfig()
|
|
499
|
+
sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
|
|
500
|
+
|
|
501
|
+
# Planned for removal/deprecated/blocked by circular context usage internally
|
|
502
|
+
sources: Optional[list[ConfiguredSource]] = Field(None, deprecated=True)
|
|
503
|
+
source_config: SourcesConfiguration = Field(
|
|
504
|
+
default_factory=SourcesConfiguration, deprecated=True
|
|
505
|
+
)
|
|
506
|
+
matching: Optional[NodeMatching] = Field(
|
|
507
|
+
default_factory=NodeMatching, deprecated=True
|
|
508
|
+
)
|
|
509
|
+
modifiers: list[str] = Field(default_factory=list, deprecated=True)
|
|
510
|
+
global_modifiers: list[str] = Field(default_factory=list, deprecated=True)
|
|
511
|
+
|
|
512
|
+
# Deprecated, need to migrate off internally
|
|
513
|
+
legacy_fields: LegacyConfig = Field(default_factory=LegacyConfig, deprecated=True)
|
|
514
|
+
|
|
515
|
+
model_config = SettingsConfigDict(
|
|
516
|
+
env_file=".env",
|
|
517
|
+
extra="ignore",
|
|
518
|
+
env_file_encoding="utf-8",
|
|
519
|
+
populate_by_name=True,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def passwords(self) -> list[str]:
|
|
524
|
+
return self.authentication.auth_passwords.get_secret_value().split(",") or []
|
|
525
|
+
|
|
526
|
+
def xds_templates(self) -> dict[str, dict[str, XdsTemplate]]:
|
|
527
|
+
ret: dict[str, dict[str, XdsTemplate]] = defaultdict(dict)
|
|
528
|
+
for template in self.templates.default:
|
|
529
|
+
ret["default"][template.type] = XdsTemplate(
|
|
530
|
+
path=template.spec,
|
|
531
|
+
resource_type=template.type,
|
|
532
|
+
depends_on=template.depends_on,
|
|
533
|
+
)
|
|
534
|
+
for version, templates in self.templates.versions.items():
|
|
535
|
+
for template in templates:
|
|
536
|
+
loaded = XdsTemplate(
|
|
537
|
+
path=template.spec,
|
|
538
|
+
resource_type=template.type,
|
|
539
|
+
depends_on=template.depends_on,
|
|
540
|
+
)
|
|
541
|
+
ret[version][template.type] = loaded
|
|
542
|
+
ret["__any__"][template.type] = loaded
|
|
543
|
+
ret["__any__"].update(ret["default"])
|
|
544
|
+
return ret
|
|
545
|
+
|
|
546
|
+
def __str__(self) -> str:
|
|
547
|
+
return self.__repr__()
|
|
548
|
+
|
|
549
|
+
def __repr__(self) -> str:
|
|
550
|
+
return f"SovereignConfigv2({self.model_dump()})"
|
|
551
|
+
|
|
552
|
+
def show(self) -> Dict[str, Any]:
|
|
553
|
+
return self.model_dump()
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
|
|
557
|
+
ret: Mapping[Any, Any] = dict()
|
|
558
|
+
for p in path.split(","):
|
|
559
|
+
spec = Loadable.from_legacy_fmt(p)
|
|
560
|
+
ret = dictupdate.merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
|
|
561
|
+
return ret
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
config_path = os.getenv("SOVEREIGN_CONFIG", "file:///etc/sovereign.yaml")
|
|
565
|
+
config = SovereignConfigv2(**parse_raw_configuration(config_path))
|
|
566
|
+
|
|
567
|
+
XDS_TEMPLATES = config.xds_templates()
|
|
568
|
+
|
|
569
|
+
# Create an enum that bases all the available discovery types off what has been configured
|
|
570
|
+
discovery_types = (_type for _type in sorted(XDS_TEMPLATES["__any__"].keys()))
|
|
571
|
+
discovery_types_base: Dict[str, str] = {t: t for t in discovery_types}
|
|
572
|
+
# TODO: this needs to be typed somehow, but I have no idea how
|
|
573
|
+
ConfiguredResourceTypes = Enum("DiscoveryTypes", discovery_types_base) # type: ignore
|
sovereign/constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TEMPLATE_CTX_PATH = ".sovereign_context.json"
|