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.

Files changed (80) hide show
  1. sovereign/__init__.py +13 -81
  2. sovereign/app.py +59 -48
  3. sovereign/cache/__init__.py +172 -0
  4. sovereign/cache/backends/__init__.py +110 -0
  5. sovereign/cache/backends/s3.py +143 -0
  6. sovereign/cache/filesystem.py +73 -0
  7. sovereign/cache/types.py +15 -0
  8. sovereign/configuration.py +573 -0
  9. sovereign/constants.py +1 -0
  10. sovereign/context.py +271 -104
  11. sovereign/dynamic_config/__init__.py +113 -0
  12. sovereign/dynamic_config/deser.py +78 -0
  13. sovereign/dynamic_config/loaders.py +120 -0
  14. sovereign/events.py +49 -0
  15. sovereign/logging/access_logger.py +85 -0
  16. sovereign/logging/application_logger.py +54 -0
  17. sovereign/logging/base_logger.py +41 -0
  18. sovereign/logging/bootstrapper.py +36 -0
  19. sovereign/logging/types.py +10 -0
  20. sovereign/middlewares.py +8 -7
  21. sovereign/modifiers/lib.py +1 -0
  22. sovereign/rendering.py +192 -0
  23. sovereign/response_class.py +18 -0
  24. sovereign/server.py +93 -35
  25. sovereign/sources/file.py +1 -1
  26. sovereign/sources/inline.py +1 -0
  27. sovereign/sources/lib.py +1 -0
  28. sovereign/sources/poller.py +296 -53
  29. sovereign/statistics.py +17 -20
  30. sovereign/templates/base.html +59 -46
  31. sovereign/templates/resources.html +203 -102
  32. sovereign/testing/loaders.py +8 -0
  33. sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
  34. sovereign/tracing.py +102 -0
  35. sovereign/types.py +299 -0
  36. sovereign/utils/auth.py +26 -13
  37. sovereign/utils/crypto/__init__.py +0 -0
  38. sovereign/utils/crypto/crypto.py +135 -0
  39. sovereign/utils/crypto/suites/__init__.py +21 -0
  40. sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
  41. sovereign/utils/crypto/suites/base_cipher.py +21 -0
  42. sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
  43. sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
  44. sovereign/utils/dictupdate.py +2 -1
  45. sovereign/utils/eds.py +37 -21
  46. sovereign/utils/mock.py +54 -16
  47. sovereign/utils/resources.py +17 -0
  48. sovereign/utils/version_info.py +8 -0
  49. sovereign/views/__init__.py +4 -0
  50. sovereign/views/api.py +61 -0
  51. sovereign/views/crypto.py +46 -15
  52. sovereign/views/discovery.py +37 -116
  53. sovereign/views/healthchecks.py +87 -18
  54. sovereign/views/interface.py +112 -112
  55. sovereign/worker.py +204 -0
  56. {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +79 -76
  57. sovereign-1.0.0b148.dist-info/RECORD +77 -0
  58. {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/WHEEL +1 -1
  59. sovereign-1.0.0b148.dist-info/entry_points.txt +38 -0
  60. sovereign_files/__init__.py +0 -0
  61. sovereign_files/static/darkmode.js +51 -0
  62. sovereign_files/static/node_expression.js +42 -0
  63. sovereign_files/static/panel.js +76 -0
  64. sovereign_files/static/resources.css +246 -0
  65. sovereign_files/static/resources.js +642 -0
  66. sovereign_files/static/sass/style.scss +33 -0
  67. sovereign_files/static/style.css +16143 -0
  68. sovereign_files/static/style.css.map +1 -0
  69. sovereign/config_loader.py +0 -225
  70. sovereign/discovery.py +0 -175
  71. sovereign/logs.py +0 -131
  72. sovereign/schemas.py +0 -780
  73. sovereign/static/sass/style.scss +0 -27
  74. sovereign/static/style.css +0 -13553
  75. sovereign/templates/ul_filter.html +0 -22
  76. sovereign/utils/crypto.py +0 -103
  77. sovereign/views/admin.py +0 -120
  78. sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
  79. sovereign-0.19.3.dist-info/RECORD +0 -47
  80. 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
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+ from pydantic import BaseModel
3
+ from sovereign.types import Node
4
+
5
+
6
+ class CacheResult(BaseModel):
7
+ value: Any
8
+ from_remote: bool
9
+
10
+
11
+ class Entry(BaseModel):
12
+ text: str
13
+ len: int
14
+ version: str
15
+ node: Node
@@ -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"