sovereign 0.14.2__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 +17 -78
  2. sovereign/app.py +74 -59
  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 +271 -100
  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 +61 -0
  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 +123 -28
  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 +18 -0
  45. sovereign/utils/mock.py +60 -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 +52 -67
  67. sovereign/views/healthchecks.py +107 -20
  68. sovereign/views/interface.py +173 -117
  69. sovereign/worker.py +193 -0
  70. {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +81 -73
  71. sovereign-1.0.0a4.dist-info/RECORD +85 -0
  72. {sovereign-0.14.2.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 -715
  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 -298
  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 -64
  96. sovereign/views/admin.py +0 -120
  97. sovereign-0.14.2.dist-info/LICENSE.txt +0 -13
  98. sovereign-0.14.2.dist-info/RECORD +0 -45
  99. sovereign-0.14.2.dist-info/entry_points.txt +0 -10
sovereign/schemas.py DELETED
@@ -1,715 +0,0 @@
1
- import warnings
2
- import multiprocessing
3
- from collections import defaultdict
4
- from enum import Enum
5
- from pydantic import (
6
- BaseModel,
7
- Field,
8
- BaseSettings,
9
- SecretStr,
10
- validator,
11
- root_validator,
12
- )
13
- from typing import List, Any, Dict, Union, Optional, Tuple, Type
14
- from types import ModuleType
15
- from jinja2 import meta, Template
16
- from fastapi.responses import JSONResponse
17
- from sovereign.config_loader import jinja_env, Serialization, Protocol, Loadable
18
- from sovereign.utils.version_info import compute_hash
19
- from croniter import croniter, CroniterBadCronError
20
-
21
- missing_arguments = {"missing", "positional", "arguments:"}
22
-
23
- JsonResponseClass: Type[JSONResponse] = JSONResponse
24
- # pylint: disable=unused-import
25
- try:
26
- import orjson
27
- from fastapi.responses import ORJSONResponse
28
-
29
- JsonResponseClass = ORJSONResponse
30
- except ImportError:
31
- try:
32
- import ujson
33
- from fastapi.responses import UJSONResponse
34
-
35
- JsonResponseClass = UJSONResponse
36
- except ImportError:
37
- pass
38
-
39
-
40
- class CacheStrategy(str, Enum):
41
- context = "context"
42
- content = "content"
43
-
44
-
45
- class SourceData(BaseModel):
46
- scopes: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
47
-
48
-
49
- class ConfiguredSource(BaseModel):
50
- type: str
51
- config: Dict[str, Any]
52
- scope: str = "default" # backward compatibility
53
-
54
-
55
- class StatsdConfig(BaseModel):
56
- host: str = "127.0.0.1"
57
- port: int = 8125
58
- tags: Dict[str, Union[Loadable, str]] = dict()
59
- namespace: str = "sovereign"
60
- enabled: bool = False
61
- use_ms: bool = True
62
-
63
- @validator("tags", pre=True)
64
- def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
65
- ret = dict()
66
- for key, value in v.items():
67
- if isinstance(value, dict):
68
- ret[key] = Loadable(**value).load()
69
- elif isinstance(value, str):
70
- ret[key] = Loadable.from_legacy_fmt(value).load()
71
- else:
72
- raise ValueError(f"Received an invalid tag for statsd: {value}")
73
- return ret
74
-
75
-
76
- class XdsTemplate:
77
- def __init__(self, path: Union[str, Loadable]) -> None:
78
- if isinstance(path, str):
79
- self.loadable: Loadable = Loadable.from_legacy_fmt(path)
80
- elif isinstance(path, Loadable):
81
- self.loadable = path
82
- self.is_python_source = self.loadable.protocol == Protocol.python
83
- self.source = self.load_source()
84
- template_ast = jinja_env.parse(self.source)
85
- self.jinja_variables = meta.find_undeclared_variables(template_ast)
86
-
87
- def __call__(
88
- self, *args: Any, **kwargs: Any
89
- ) -> Optional[Union[Dict[str, Any], str]]:
90
- if not hasattr(self, "code"):
91
- self.code: Union[Template, ModuleType] = self.loadable.load()
92
- if isinstance(self.code, ModuleType):
93
- try:
94
- return {"resources": list(self.code.call(*args, **kwargs))} # type: ignore
95
- except TypeError as e:
96
- if not set(str(e).split()).issuperset(missing_arguments):
97
- raise e
98
- message_start = str(e).find(":")
99
- missing_args = str(e)[message_start + 2 :]
100
- supplied_args = list(kwargs.keys())
101
- raise TypeError(
102
- f"Tried to render a template using partial arguments. "
103
- f"Missing args: {missing_args}. Supplied args: {args} "
104
- f"Supplied keyword args: {supplied_args}"
105
- )
106
- else:
107
- return self.code.render(*args, **kwargs)
108
-
109
- def load_source(self) -> str:
110
- if self.loadable.serialization in (Serialization.jinja, Serialization.jinja2):
111
- # The Jinja2 template serializer does not properly set a name
112
- # for the loaded template.
113
- # The repr for the template prints out as the memory address
114
- # This makes it really hard to generate a consistent version_info string
115
- # in rendered configuration.
116
- # For this reason, we re-load the template as a string instead, and create a checksum.
117
- old_serialization = self.loadable.serialization
118
- self.loadable.serialization = Serialization("string")
119
- ret = self.loadable.load()
120
- self.loadable.serialization = old_serialization
121
- return str(ret)
122
- elif self.is_python_source:
123
- # If the template specified is a python source file,
124
- # we can simply read and return the source of it.
125
- old_protocol = self.loadable.protocol
126
- old_serialization = self.loadable.serialization
127
- self.loadable.protocol = Protocol("inline")
128
- self.loadable.serialization = Serialization("string")
129
- ret = self.loadable.load()
130
- self.loadable.protocol = old_protocol
131
- self.loadable.serialization = old_serialization
132
- return str(ret)
133
- ret = self.loadable.load()
134
- return str(ret)
135
-
136
- def __repr__(self) -> str:
137
- return f"XdsTemplate({self.loadable=}, {self.is_python_source=}, {self.source=}, {self.jinja_variables=})"
138
-
139
-
140
- class ProcessedTemplate:
141
- def __init__(
142
- self,
143
- resources: List[Dict[str, Any]],
144
- version_info: Optional[str],
145
- ) -> None:
146
- self.resources = resources
147
- self.version_info = version_info
148
- self._rendered: Optional[bytes] = None
149
-
150
- @property
151
- def version(self) -> str:
152
- return self.version_info or compute_hash(self.resources)
153
-
154
- @property
155
- def rendered(self) -> bytes:
156
- if self._rendered is None:
157
- result = JsonResponseClass(content="").render(
158
- content={
159
- "version_info": self.version,
160
- "resources": self.resources,
161
- }
162
- )
163
- self._rendered = result
164
- return self._rendered
165
-
166
- def deserialize_resources(self) -> List[Dict[str, Any]]:
167
- return self.resources
168
-
169
-
170
- class Locality(BaseModel):
171
- region: str = Field(None)
172
- zone: str = Field(None)
173
- sub_zone: str = Field(None)
174
-
175
-
176
- class SemanticVersion(BaseModel):
177
- major_number: int = 0
178
- minor_number: int = 0
179
- patch: int = 0
180
-
181
- def __str__(self) -> str:
182
- return f"{self.major_number}.{self.minor_number}.{self.patch}"
183
-
184
-
185
- class BuildVersion(BaseModel):
186
- version: SemanticVersion = SemanticVersion()
187
- metadata: Dict[str, Any] = {}
188
-
189
-
190
- class Extension(BaseModel):
191
- name: Optional[str] = None
192
- category: Optional[str] = None
193
- version: Optional[BuildVersion] = None
194
- disabled: Optional[bool] = None
195
-
196
-
197
- class Node(BaseModel):
198
- id: str = Field("-", title="Hostname")
199
- cluster: str = Field(
200
- ...,
201
- title="Envoy service-cluster",
202
- description="The ``--service-cluster`` configured by the Envoy client",
203
- )
204
- metadata: Dict[str, Any] = Field(default_factory=dict, title="Key:value metadata")
205
- locality: Locality = Field(Locality(), title="Locality")
206
- build_version: str = Field(
207
- None, # Optional in the v3 Envoy API
208
- title="Envoy build/release version string",
209
- description="Used to identify what version of Envoy the "
210
- "client is running, and what config to provide in response",
211
- )
212
- user_agent_name: str = "envoy"
213
- user_agent_version: str = ""
214
- user_agent_build_version: BuildVersion = BuildVersion()
215
- extensions: List[Extension] = []
216
- client_features: List[str] = []
217
-
218
- @property
219
- def common(self) -> Tuple[str, str, str, BuildVersion, Locality]:
220
- """
221
- Returns fields that are the same in adjacent proxies
222
- ie. proxies that are part of the same logical group
223
- """
224
- return (
225
- self.cluster,
226
- self.build_version,
227
- self.user_agent_version,
228
- self.user_agent_build_version,
229
- self.locality,
230
- )
231
-
232
-
233
- class Resources(List[str]):
234
- """
235
- Acts like a regular list except it returns True
236
- for all membership tests when empty.
237
- """
238
-
239
- def __contains__(self, item: object) -> bool:
240
- if len(self) == 0:
241
- return True
242
- return item in list(self)
243
-
244
-
245
- class DiscoveryRequest(BaseModel):
246
- node: Node = Field(..., title="Node information about the envoy proxy")
247
- version_info: str = Field(
248
- "0", title="The version of the envoy clients current configuration"
249
- )
250
- resource_names: Resources = Field(
251
- Resources(), title="List of requested resource names"
252
- )
253
- hide_private_keys: bool = False
254
- type_url: Optional[str] = Field(
255
- None, title="The corresponding type_url for the requested resource"
256
- )
257
- desired_controlplane: str = Field(
258
- None, title="The host header provided in the Discovery Request"
259
- )
260
-
261
- @property
262
- def envoy_version(self) -> str:
263
- try:
264
- version = str(self.node.user_agent_build_version.version)
265
- assert version != "0.0.0"
266
- except AssertionError:
267
- try:
268
- build_version = self.node.build_version
269
- _, version, *_ = build_version.split("/")
270
- except (AttributeError, ValueError):
271
- # TODO: log/metric this?
272
- return "default"
273
- return version
274
-
275
- @property
276
- def resources(self) -> Resources:
277
- return Resources(self.resource_names)
278
-
279
- @property
280
- def uid(self) -> str:
281
- return compute_hash(
282
- self.resources,
283
- self.node.common,
284
- self.desired_controlplane,
285
- )
286
-
287
-
288
- class DiscoveryResponse(BaseModel):
289
- version_info: str = Field(
290
- ..., title="The version of the configuration in the response"
291
- )
292
- resources: List[Any] = Field(..., title="The requested configuration resources")
293
-
294
-
295
- class SovereignAsgiConfig(BaseSettings):
296
- host: str = "0.0.0.0"
297
- port: int = 8080
298
- keepalive: int = 5
299
- workers: int = multiprocessing.cpu_count() * 2 + 1
300
- threads: int = 1
301
- reuse_port: bool = True
302
- preload_app: bool = False
303
- log_level: str = "warning"
304
- worker_class: str = "uvicorn.workers.UvicornWorker"
305
- worker_timeout: int = 30
306
-
307
- class Config:
308
- fields = {
309
- "host": {"env": "SOVEREIGN_HOST"},
310
- "port": {"env": "SOVEREIGN_PORT"},
311
- "keepalive": {"env": "SOVEREIGN_KEEPALIVE"},
312
- "workers": {"env": "SOVEREIGN_WORKERS"},
313
- "threads": {"env": "SOVEREIGN_THREADS"},
314
- "preload_app": {"env": "SOVEREIGN_PRELOAD"},
315
- "worker_timeout": {"env": "SOVEREIGN_WORKER_TIMEOUT"},
316
- }
317
-
318
- def as_gunicorn_conf(self) -> Dict[str, Any]:
319
- return {
320
- "bind": ":".join(map(str, [self.host, self.port])),
321
- "keepalive": self.keepalive,
322
- "reuse_port": self.reuse_port,
323
- "preload_app": self.preload_app,
324
- "loglevel": self.log_level,
325
- "timeout": self.worker_timeout,
326
- "threads": self.threads,
327
- "workers": self.workers,
328
- "worker_class": self.worker_class,
329
- }
330
-
331
-
332
- class SovereignConfig(BaseSettings):
333
- sources: List[ConfiguredSource]
334
- templates: Dict[str, Dict[str, Union[str, Loadable]]]
335
- template_context: Dict[str, Any] = {}
336
- eds_priority_matrix: Dict[str, Dict[str, str]] = {}
337
- modifiers: List[str] = []
338
- global_modifiers: List[str] = []
339
- regions: List[str] = []
340
- statsd: StatsdConfig = StatsdConfig()
341
- auth_enabled: bool = False
342
- auth_passwords: str = ""
343
- encryption_key: str = ""
344
- environment: str = "local"
345
- debug_enabled: bool = False
346
- sentry_dsn: str = ""
347
- node_match_key: str = "cluster"
348
- node_matching: bool = True
349
- source_match_key: str = "service_clusters"
350
- sources_refresh_rate: int = 30
351
- cache_strategy: str = "context"
352
- refresh_context: bool = False
353
- context_refresh_rate: Optional[int]
354
- context_refresh_cron: Optional[str]
355
- dns_hard_fail: bool = False
356
- enable_application_logs: bool = False
357
- enable_access_logs: bool = True
358
- log_fmt: Optional[str] = ""
359
- ignore_empty_log_fields: bool = False
360
-
361
- class Config:
362
- fields = {
363
- "auth_enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
364
- "auth_passwords": {"env": "SOVEREIGN_AUTH_PASSWORDS"},
365
- "encryption_key": {"env": "SOVEREIGN_ENCRYPTION_KEY"},
366
- "environment": {"env": "SOVEREIGN_ENVIRONMENT"},
367
- "debug_enabled": {"env": "SOVEREIGN_DEBUG_ENABLED"},
368
- "sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
369
- "node_match_key": {"env": "SOVEREIGN_NODE_MATCH_KEY"},
370
- "node_matching": {"env": "SOVEREIGN_NODE_MATCHING"},
371
- "source_match_key": {"env": "SOVEREIGN_SOURCE_MATCH_KEY"},
372
- "sources_refresh_rate": {"env": "SOVEREIGN_SOURCES_REFRESH_RATE"},
373
- "cache_strategy": {"env": "SOVEREIGN_CACHE_STRATEGY"},
374
- "refresh_context": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
375
- "context_refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
376
- "context_refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
377
- "dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
378
- "enable_application_logs": {"env": "SOVEREIGN_ENABLE_APPLICATION_LOGS"},
379
- "enable_access_logs": {"env": "SOVEREIGN_ENABLE_ACCESS_LOGS"},
380
- "log_fmt": {"env": "SOVEREIGN_LOG_FORMAT"},
381
- "ignore_empty_fields": {"env": "SOVEREIGN_LOG_IGNORE_EMPTY"},
382
- }
383
-
384
- @property
385
- def passwords(self) -> List[str]:
386
- return self.auth_passwords.split(",") or []
387
-
388
- def xds_templates(self) -> Dict[str, Dict[str, XdsTemplate]]:
389
- ret: Dict[str, Dict[str, XdsTemplate]] = {
390
- "__any__": {}
391
- } # Special key to hold templates from all versions
392
- for version, templates in self.templates.items():
393
- loaded_templates = {
394
- _type: XdsTemplate(path=path) for _type, path in templates.items()
395
- }
396
- ret[str(version)] = loaded_templates
397
- ret["__any__"].update(loaded_templates)
398
- return ret
399
-
400
- def __str__(self) -> str:
401
- return self.__repr__()
402
-
403
- def __repr__(self) -> str:
404
- kwargs = [f"{k}={v}" for k, v in self.show().items()]
405
- return f"SovereignConfig({kwargs})"
406
-
407
- def show(self) -> Dict[str, Any]:
408
- safe_items = dict()
409
- for key, value in self.__dict__.items():
410
- if key in ["auth_passwords", "encryption_key", "passwords", "sentry_dsn"]:
411
- value = "redacted"
412
- safe_items[key] = value
413
- return safe_items
414
-
415
-
416
- class TemplateSpecification(BaseModel):
417
- type: str
418
- spec: Loadable
419
-
420
-
421
- class NodeMatching(BaseSettings):
422
- enabled: bool = True
423
- source_key: str = "service_clusters"
424
- node_key: str = "cluster"
425
-
426
- class Config:
427
- fields = {
428
- "enabled": {"env": "SOVEREIGN_NODE_MATCHING_ENABLED"},
429
- "source_key": {"env": "SOVEREIGN_SOURCE_MATCH_KEY"},
430
- "node_key": {"env": "SOVEREIGN_NODE_MATCH_KEY"},
431
- }
432
-
433
-
434
- class AuthConfiguration(BaseSettings):
435
- enabled: bool = False
436
- auth_passwords: SecretStr = SecretStr("")
437
- encryption_key: SecretStr = SecretStr("")
438
-
439
- class Config:
440
- fields = {
441
- "enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
442
- "auth_passwords": {"env": "SOVEREIGN_AUTH_PASSWORDS"},
443
- "encryption_key": {"env": "SOVEREIGN_ENCRYPTION_KEY"},
444
- }
445
-
446
-
447
- class ApplicationLogConfiguration(BaseSettings):
448
- enabled: bool = False
449
- # currently only support /dev/stdout as JSON
450
-
451
- class Config:
452
- fields = {
453
- "enabled": {"env": "SOVEREIGN_ENABLE_APPLICATION_LOGS"},
454
- }
455
-
456
-
457
- class AccessLogConfiguration(BaseSettings):
458
- enabled: bool = True
459
- log_fmt: Optional[str] = None
460
- ignore_empty_fields: bool = False
461
-
462
- class Config:
463
- fields = {
464
- "enabled": {"env": "SOVEREIGN_ENABLE_ACCESS_LOGS"},
465
- "log_fmt": {"env": "SOVEREIGN_LOG_FORMAT"},
466
- "ignore_empty_fields": {"env": "SOVEREIGN_LOG_IGNORE_EMPTY"},
467
- }
468
-
469
-
470
- class LoggingConfiguration(BaseSettings):
471
- application_logs: ApplicationLogConfiguration = ApplicationLogConfiguration()
472
- access_logs: AccessLogConfiguration = AccessLogConfiguration()
473
-
474
-
475
- class ContextConfiguration(BaseSettings):
476
- context: Dict[str, Loadable] = {}
477
- refresh: bool = False
478
- refresh_rate: Optional[int] = None
479
- refresh_cron: Optional[str] = None
480
-
481
- @staticmethod
482
- def context_from_legacy(context: Dict[str, str]) -> Dict[str, Loadable]:
483
- ret = dict()
484
- for key, value in context.items():
485
- ret[key] = Loadable.from_legacy_fmt(value)
486
- return ret
487
-
488
- @root_validator(pre=False)
489
- def validate_single_use_refresh_method(
490
- cls, values: Dict[str, Any]
491
- ) -> Dict[str, Any]:
492
- refresh_rate = values.get("refresh_rate")
493
- refresh_cron = values.get("refresh_cron")
494
-
495
- if (refresh_rate is not None) and (refresh_cron is not None):
496
- raise RuntimeError(
497
- f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {refresh_rate=} and {refresh_cron=}"
498
- )
499
- return values
500
-
501
- @root_validator
502
- def set_default_refresh_rate(cls, values: Dict[str, Any]) -> Dict[str, Any]:
503
- refresh_rate = values.get("refresh_rate")
504
- refresh_cron = values.get("refresh_cron")
505
-
506
- if (refresh_rate is None) and (refresh_cron is None):
507
- values["refresh_rate"] = 3600
508
- return values
509
-
510
- @validator("refresh_cron")
511
- def validate_refresh_cron(cls, v: Optional[str]) -> Optional[str]:
512
- if v is None:
513
- return v
514
- if not croniter.is_valid(v):
515
- raise CroniterBadCronError(f"'{v}' is not a valid cron expression")
516
- return v
517
-
518
- class Config:
519
- fields = {
520
- "refresh": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
521
- "refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
522
- "refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
523
- }
524
-
525
-
526
- class SourcesConfiguration(BaseSettings):
527
- refresh_rate: int = 30
528
- cache_strategy: CacheStrategy = CacheStrategy.context
529
-
530
- class Config:
531
- fields = {
532
- "refresh_rate": {"env": "SOVEREIGN_SOURCES_REFRESH_RATE"},
533
- "cache_strategy": {"env": "SOVEREIGN_CACHE_STRATEGY"},
534
- }
535
-
536
-
537
- class LegacyConfig(BaseSettings):
538
- regions: Optional[List[str]] = None
539
- eds_priority_matrix: Optional[Dict[str, Dict[str, str]]] = None
540
- dns_hard_fail: Optional[bool] = None
541
- environment: Optional[str] = None
542
-
543
- @validator("regions")
544
- def regions_is_set(cls, v: Optional[List[str]]) -> List[str]:
545
- if v is not None:
546
- warnings.warn(
547
- "Setting regions via config is deprecated. "
548
- "It is suggested to use a modifier or template "
549
- "logic in order to achieve the same goal.",
550
- DeprecationWarning,
551
- )
552
- return v
553
- else:
554
- return []
555
-
556
- @validator("eds_priority_matrix")
557
- def eds_priority_matrix_is_set(
558
- cls, v: Optional[Dict[str, Dict[str, Any]]]
559
- ) -> Dict[str, Dict[str, Any]]:
560
- if v is not None:
561
- warnings.warn(
562
- "Setting eds_priority_matrix via config is deprecated. "
563
- "It is suggested to use a modifier or template "
564
- "logic in order to achieve the same goal.",
565
- DeprecationWarning,
566
- )
567
- return v
568
- else:
569
- return {}
570
-
571
- @validator("dns_hard_fail")
572
- def dns_hard_fail_is_set(cls, v: Optional[bool]) -> bool:
573
- if v is not None:
574
- warnings.warn(
575
- "Setting dns_hard_fail via config is deprecated. "
576
- "It is suggested to supply a module that can perform "
577
- "dns resolution to template_context, so that it can "
578
- "be used via templates instead.",
579
- DeprecationWarning,
580
- )
581
- return v
582
- else:
583
- return False
584
-
585
- @validator("environment")
586
- def environment_is_set(cls, v: Optional[str]) -> Optional[str]:
587
- if v is not None:
588
- warnings.warn(
589
- "Setting environment via config is deprecated. "
590
- "It is suggested to configure this value through log_fmt "
591
- "instead.",
592
- DeprecationWarning,
593
- )
594
- return v
595
- else:
596
- return None
597
-
598
- class Config:
599
- fields = {
600
- "dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
601
- "environment": {"env": "SOVEREIGN_ENVIRONMENT"},
602
- }
603
-
604
-
605
- class SovereignConfigv2(BaseSettings):
606
- sources: List[ConfiguredSource]
607
- templates: Dict[str, List[TemplateSpecification]]
608
- source_config: SourcesConfiguration = SourcesConfiguration()
609
- modifiers: List[str] = []
610
- global_modifiers: List[str] = []
611
- template_context: ContextConfiguration = ContextConfiguration()
612
- matching: NodeMatching = NodeMatching()
613
- authentication: AuthConfiguration = AuthConfiguration()
614
- logging: LoggingConfiguration = LoggingConfiguration()
615
- statsd: StatsdConfig = StatsdConfig()
616
- sentry_dsn: SecretStr = SecretStr("")
617
- debug: bool = False
618
- legacy_fields: LegacyConfig = LegacyConfig()
619
-
620
- class Config:
621
- fields = {
622
- "sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
623
- "debug": {"env": "SOVEREIGN_DEBUG"},
624
- }
625
-
626
- @property
627
- def passwords(self) -> List[str]:
628
- return self.authentication.auth_passwords.get_secret_value().split(",") or []
629
-
630
- def xds_templates(self) -> Dict[str, Dict[str, XdsTemplate]]:
631
- ret: Dict[str, Dict[str, XdsTemplate]] = {
632
- "__any__": {}
633
- } # Special key to hold templates from all versions
634
- for version, template_specs in self.templates.items():
635
- loaded_templates = {
636
- template.type: XdsTemplate(path=template.spec)
637
- for template in template_specs
638
- }
639
- ret[str(version)] = loaded_templates
640
- ret["__any__"].update(loaded_templates)
641
- return ret
642
-
643
- def __str__(self) -> str:
644
- return self.__repr__()
645
-
646
- def __repr__(self) -> str:
647
- return f"SovereignConfigv2({self.dict()})"
648
-
649
- def show(self) -> Dict[str, Any]:
650
- return self.dict()
651
-
652
- @staticmethod
653
- def from_legacy_config(other: SovereignConfig) -> "SovereignConfigv2":
654
- new_templates = dict()
655
- for version, templates in other.templates.items():
656
- specs = list()
657
- for type, path in templates.items():
658
- if isinstance(path, str):
659
- specs.append(
660
- TemplateSpecification(
661
- type=type, spec=Loadable.from_legacy_fmt(path)
662
- )
663
- )
664
- else:
665
- # Just in case? Although this shouldn't happen
666
- specs.append(TemplateSpecification(type=type, spec=path))
667
- new_templates[str(version)] = specs
668
-
669
- return SovereignConfigv2(
670
- sources=other.sources,
671
- templates=new_templates,
672
- source_config=SourcesConfiguration(
673
- refresh_rate=other.sources_refresh_rate,
674
- cache_strategy=CacheStrategy(other.cache_strategy),
675
- ),
676
- modifiers=other.modifiers,
677
- global_modifiers=other.global_modifiers,
678
- template_context=ContextConfiguration(
679
- context=ContextConfiguration.context_from_legacy(
680
- other.template_context
681
- ),
682
- refresh=other.refresh_context,
683
- refresh_rate=other.context_refresh_rate,
684
- refresh_cron=other.context_refresh_cron,
685
- ),
686
- matching=NodeMatching(
687
- enabled=other.node_matching,
688
- source_key=other.source_match_key,
689
- node_key=other.node_match_key,
690
- ),
691
- authentication=AuthConfiguration(
692
- enabled=other.auth_enabled,
693
- auth_passwords=SecretStr(other.auth_passwords),
694
- encryption_key=SecretStr(other.encryption_key),
695
- ),
696
- logging=LoggingConfiguration(
697
- application_logs=ApplicationLogConfiguration(
698
- enabled=other.enable_application_logs,
699
- ),
700
- access_logs=AccessLogConfiguration(
701
- enabled=other.enable_access_logs,
702
- log_fmt=other.log_fmt,
703
- ignore_empty_fields=other.ignore_empty_log_fields,
704
- ),
705
- ),
706
- statsd=other.statsd,
707
- sentry_dsn=SecretStr(other.sentry_dsn),
708
- debug=other.debug_enabled,
709
- legacy_fields=LegacyConfig(
710
- regions=other.regions,
711
- eds_priority_matrix=other.eds_priority_matrix,
712
- dns_hard_fail=other.dns_hard_fail,
713
- environment=other.environment,
714
- ),
715
- )