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