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