sovereign 0.26.0__tar.gz → 0.27.0__tar.gz

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 (61) hide show
  1. {sovereign-0.26.0 → sovereign-0.27.0}/PKG-INFO +4 -2
  2. {sovereign-0.26.0 → sovereign-0.27.0}/pyproject.toml +10 -2
  3. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/__init__.py +1 -1
  4. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/configuration.py +1 -1
  5. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/schemas.py +184 -187
  6. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/crypto/crypto.py +2 -0
  7. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/views/admin.py +2 -2
  8. {sovereign-0.26.0 → sovereign-0.27.0}/LICENSE.txt +0 -0
  9. {sovereign-0.26.0 → sovereign-0.27.0}/README.md +0 -0
  10. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/app.py +0 -0
  11. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/config_loader.py +0 -0
  12. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/constants.py +0 -0
  13. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/context.py +0 -0
  14. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/discovery.py +0 -0
  15. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/error_info.py +0 -0
  16. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/logging/access_logger.py +0 -0
  17. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/logging/application_logger.py +0 -0
  18. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/logging/base_logger.py +0 -0
  19. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/logging/bootstrapper.py +0 -0
  20. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/logging/types.py +0 -0
  21. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/middlewares.py +0 -0
  22. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/modifiers/__init__.py +0 -0
  23. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/modifiers/lib.py +0 -0
  24. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/response_class.py +0 -0
  25. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/server.py +0 -0
  26. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/sources/__init__.py +0 -0
  27. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/sources/file.py +0 -0
  28. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/sources/inline.py +0 -0
  29. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/sources/lib.py +0 -0
  30. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/sources/poller.py +0 -0
  31. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/static/sass/style.scss +0 -0
  32. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/static/style.css +0 -0
  33. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/statistics.py +0 -0
  34. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/templates/base.html +0 -0
  35. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/templates/err.html +0 -0
  36. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/templates/resources.html +0 -0
  37. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/templates/ul_filter.html +0 -0
  38. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/testing/loaders.py +0 -0
  39. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/testing/modifiers.py +0 -0
  40. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/__init__.py +0 -0
  41. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/auth.py +0 -0
  42. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/crypto/__init__.py +0 -0
  43. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
  44. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
  45. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
  46. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
  47. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
  48. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/dictupdate.py +0 -0
  49. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/eds.py +0 -0
  50. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/entry_point_loader.py +0 -0
  51. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/mock.py +0 -0
  52. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/resources.py +0 -0
  53. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/templates.py +0 -0
  54. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/timer.py +0 -0
  55. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/version_info.py +0 -0
  56. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/utils/weighted_clusters.py +0 -0
  57. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/views/__init__.py +0 -0
  58. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/views/crypto.py +0 -0
  59. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/views/discovery.py +0 -0
  60. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/views/healthchecks.py +0 -0
  61. {sovereign-0.26.0 → sovereign-0.27.0}/src/sovereign/views/interface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: Envoy Proxy control-plane written in Python
5
5
  Home-page: https://pypi.org/project/sovereign/
6
6
  License: Apache-2.0
@@ -44,13 +44,15 @@ Requires-Dist: glom (>=23.3.0,<24.0.0)
44
44
  Requires-Dist: gunicorn (>=22.0.0,<23.0.0)
45
45
  Requires-Dist: httptools (>=0.6.0,<0.7.0) ; extra == "httptools"
46
46
  Requires-Dist: orjson (>=3.9.15,<4.0.0) ; extra == "orjson"
47
+ Requires-Dist: pydantic (>=2.7.2,<3.0.0)
48
+ Requires-Dist: pydantic-settings (>=2.3.1,<3.0.0)
47
49
  Requires-Dist: redis (<=5.0.0)
48
50
  Requires-Dist: requests (>=2.31.0,<3.0.0)
49
51
  Requires-Dist: sentry-sdk (>=1.23.1,<2.0.0) ; extra == "sentry"
50
52
  Requires-Dist: structlog (>=23.1.0,<24.0.0)
51
53
  Requires-Dist: ujson (>=5.8.0,<6.0.0) ; extra == "ujson"
52
54
  Requires-Dist: uvicorn (>=0.23.2,<0.24.0)
53
- Requires-Dist: uvloop (>=0.17.0,<0.18.0)
55
+ Requires-Dist: uvloop (>=0.19.0,<0.20.0)
54
56
  Project-URL: Documentation, https://vsyrakis.bitbucket.io/sovereign/docs/
55
57
  Project-URL: Repository, https://bitbucket.org/atlassian/sovereign/src/master/
56
58
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sovereign"
3
- version = "0.26.0"
3
+ version = "0.27.0"
4
4
  description = "Envoy Proxy control-plane written in Python"
5
5
  license = "Apache-2.0"
6
6
  packages = [
@@ -55,7 +55,7 @@ cachelib = "^0.10.2"
55
55
  glom = "^23.3.0"
56
56
  cryptography = "^42.0.5"
57
57
  fastapi = "^0.110.0"
58
- uvloop = "^0.17.0"
58
+ uvloop = "^0.19.0"
59
59
  sentry-sdk = "^1.23.1"
60
60
  boto3 = {version = "^1.28.62", optional = true}
61
61
  datadog = {version = "^0.47.0", optional = true}
@@ -66,6 +66,8 @@ cashews = {extras = ["redis"], version = "^6.3.0", optional = true}
66
66
  redis = {version = "<= 5.0.0", optional = true}
67
67
  httptools = {version = "^0.6.0", optional = true}
68
68
  cachetools = "^5.3.2"
69
+ pydantic = "^2.7.2"
70
+ pydantic-settings = "^2.3.1"
69
71
 
70
72
  [tool.poetry.extras]
71
73
  sentry = ["sentry-sdk"]
@@ -114,6 +116,12 @@ lint = { cmd = "pylint src/sovereign", help = "Run linter checks" }
114
116
  [tool.black]
115
117
  target-version = ['py311']
116
118
 
119
+ [tool.mypy]
120
+ plugins = [
121
+ "pydantic.mypy"
122
+ ]
123
+ ignore_missing_imports = true
124
+
117
125
  [tool.coverage.run]
118
126
  omit = ["test/*"]
119
127
 
@@ -4,7 +4,7 @@ from importlib.metadata import version
4
4
  from typing import Any, Mapping, Type
5
5
 
6
6
  from fastapi.responses import JSONResponse
7
- from pydantic.error_wrappers import ValidationError
7
+ from pydantic import ValidationError
8
8
  from starlette.templating import Jinja2Templates
9
9
 
10
10
  from sovereign import config_loader
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from typing import Any, Mapping
3
3
 
4
- from pydantic.error_wrappers import ValidationError
4
+ from pydantic import ValidationError
5
5
 
6
6
  from sovereign import config_loader
7
7
  from sovereign.context import TemplateContext
@@ -5,19 +5,20 @@ from dataclasses import dataclass
5
5
  from enum import Enum
6
6
  from os import getenv
7
7
  from types import ModuleType
8
- from typing import Any, Dict, List, Optional, Tuple, Type, Union
8
+ from typing import Any, Dict, List, Optional, Self, Tuple, Type, Union
9
9
 
10
10
  from croniter import CroniterBadCronError, croniter
11
11
  from fastapi.responses import JSONResponse
12
12
  from jinja2 import Template, meta
13
13
  from pydantic import (
14
14
  BaseModel,
15
- BaseSettings,
15
+ ConfigDict,
16
16
  Field,
17
17
  SecretStr,
18
- root_validator,
19
- validator,
18
+ model_validator,
19
+ field_validator,
20
20
  )
21
+ from pydantic_settings import BaseSettings, SettingsConfigDict
21
22
 
22
23
  from sovereign.config_loader import Loadable, Serialization, jinja_env
23
24
  from sovereign.utils.crypto.suites import EncryptionType
@@ -65,11 +66,13 @@ class StatsdConfig(BaseModel):
65
66
  enabled: bool = False
66
67
  use_ms: bool = True
67
68
 
68
- @validator("host", pre=True)
69
+ @field_validator("host", mode="before")
70
+ @classmethod
69
71
  def load_host(cls, v: str) -> Any:
70
72
  return Loadable.from_legacy_fmt(v).load()
71
73
 
72
- @validator("port", pre=True)
74
+ @field_validator("port", mode="before")
75
+ @classmethod
73
76
  def load_port(cls, v: Union[int, str]) -> Any:
74
77
  if isinstance(v, int):
75
78
  return v
@@ -78,7 +81,8 @@ class StatsdConfig(BaseModel):
78
81
  else:
79
82
  raise ValueError(f"Received an invalid port: {v}")
80
83
 
81
- @validator("tags", pre=True)
84
+ @field_validator("tags", mode="before")
85
+ @classmethod
82
86
  def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
83
87
  ret = dict()
84
88
  for key, value in v.items():
@@ -108,14 +112,14 @@ class DiscoveryCacheConfig(BaseModel):
108
112
  socket_keepalive: bool = True # Try to keep connections to redis around.
109
113
  ttl: int = 60
110
114
 
111
- @root_validator
112
- def set_default_protocol(cls, values: Dict[str, Any]) -> Dict[str, Any]:
113
- secure = values.get("secure")
114
- if secure:
115
- values["protocol"] = "rediss://"
116
- return values
115
+ @model_validator(mode="after")
116
+ def set_default_protocol(self) -> Self:
117
+ if self.secure:
118
+ self.protocol = "rediss://"
119
+ return self
117
120
 
118
- @root_validator
121
+ @model_validator(mode="before")
122
+ @classmethod
119
123
  def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
120
124
  if host := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_HOST"):
121
125
  values["host"] = host
@@ -221,9 +225,9 @@ class ProcessedTemplate:
221
225
 
222
226
 
223
227
  class Locality(BaseModel):
224
- region: str = Field(None)
225
- zone: str = Field(None)
226
- sub_zone: str = Field(None)
228
+ region: Optional[str] = Field(None)
229
+ zone: Optional[str] = Field(None)
230
+ sub_zone: Optional[str] = Field(None)
227
231
 
228
232
 
229
233
  class SemanticVersion(BaseModel):
@@ -255,8 +259,8 @@ class Node(BaseModel):
255
259
  description="The ``--service-cluster`` configured by the Envoy client",
256
260
  )
257
261
  metadata: Dict[str, Any] = Field(default_factory=dict, title="Key:value metadata")
258
- locality: Locality = Field(Locality(), title="Locality") # type: ignore
259
- build_version: str = Field(
262
+ locality: Locality = Field(Locality(), title="Locality")
263
+ build_version: Optional[str] = Field(
260
264
  None, # Optional in the v3 Envoy API
261
265
  title="Envoy build/release version string",
262
266
  description="Used to identify what version of Envoy the "
@@ -269,7 +273,7 @@ class Node(BaseModel):
269
273
  client_features: List[str] = []
270
274
 
271
275
  @property
272
- def common(self) -> Tuple[str, str, str, BuildVersion, Locality]:
276
+ def common(self) -> Tuple[str, Optional[str], str, BuildVersion, Locality]:
273
277
  """
274
278
  Returns fields that are the same in adjacent proxies
275
279
  ie. proxies that are part of the same logical group
@@ -292,7 +296,7 @@ class Resources(List[str]):
292
296
  def __contains__(self, item: object) -> bool:
293
297
  if len(self) == 0:
294
298
  return True
295
- return item in list(self)
299
+ return super().__contains__(item)
296
300
 
297
301
 
298
302
  class Status(BaseModel):
@@ -306,19 +310,20 @@ class DiscoveryRequest(BaseModel):
306
310
  version_info: str = Field(
307
311
  "0", title="The version of the envoy clients current configuration"
308
312
  )
309
- resource_names: Resources = Field(
313
+ resource_names: list[str] | Resources = Field(
310
314
  Resources(), title="List of requested resource names"
311
315
  )
312
316
  hide_private_keys: bool = False
313
317
  type_url: Optional[str] = Field(
314
318
  None, title="The corresponding type_url for the requested resource"
315
319
  )
316
- desired_controlplane: str = Field(
320
+ desired_controlplane: Optional[str] = Field(
317
321
  None, title="The host header provided in the Discovery Request"
318
322
  )
319
- error_detail: Status = Field(
323
+ error_detail: Optional[Status] = Field(
320
324
  None, title="Error details from the previous xDS request"
321
325
  )
326
+ model_config = ConfigDict(arbitrary_types_allowed=True)
322
327
 
323
328
  @property
324
329
  def envoy_version(self) -> str:
@@ -328,6 +333,8 @@ class DiscoveryRequest(BaseModel):
328
333
  except AssertionError:
329
334
  try:
330
335
  build_version = self.node.build_version
336
+ if build_version is None:
337
+ return "default"
331
338
  _, version, *_ = build_version.split("/")
332
339
  except (AttributeError, ValueError):
333
340
  # TODO: log/metric this?
@@ -355,33 +362,35 @@ class DiscoveryResponse(BaseModel):
355
362
 
356
363
 
357
364
  class SovereignAsgiConfig(BaseSettings):
358
- host: str = "0.0.0.0"
359
- port: int = 8080
360
- keepalive: int = 5
361
- workers: int = multiprocessing.cpu_count() * 2 + 1
362
- threads: int = 1
365
+ host: str = Field("0.0.0.0", alias="SOVEREIGN_HOST")
366
+ port: int = Field(8080, alias="SOVEREIGN_PORT")
367
+ keepalive: int = Field(5, alias="SOVEREIGN_KEEPALIVE")
368
+ workers: int = Field(
369
+ default_factory=lambda: multiprocessing.cpu_count() * 2 + 1,
370
+ alias="SOVEREIGN_WORKERS",
371
+ )
372
+ threads: int = Field(1, alias="SOVEREIGN_THREADS")
363
373
  reuse_port: bool = True
364
- preload_app: bool = True
374
+ preload_app: bool = Field(True, alias="SOVEREIGN_PRELOAD")
365
375
  log_level: str = "warning"
366
376
  worker_class: str = "uvicorn.workers.UvicornWorker"
367
- worker_timeout: int = 30
377
+ worker_timeout: int = Field(30, alias="SOVEREIGN_WORKER_TIMEOUT")
368
378
  worker_tmp_dir: str = "/dev/shm"
369
- graceful_timeout: int = worker_timeout * 2
370
- max_requests: int = 0
371
- max_requests_jitter: int = 0
372
-
373
- class Config:
374
- fields = {
375
- "host": {"env": "SOVEREIGN_HOST"},
376
- "port": {"env": "SOVEREIGN_PORT"},
377
- "keepalive": {"env": "SOVEREIGN_KEEPALIVE"},
378
- "workers": {"env": "SOVEREIGN_WORKERS"},
379
- "threads": {"env": "SOVEREIGN_THREADS"},
380
- "preload_app": {"env": "SOVEREIGN_PRELOAD"},
381
- "worker_timeout": {"env": "SOVEREIGN_WORKER_TIMEOUT"},
382
- "max_requests": {"env": "SOVEREIGN_MAX_REQUESTS"},
383
- "max_requests_jitter": {"env": "SOVEREIGN_MAX_REQUESTS_JITTER"},
384
- }
379
+ graceful_timeout: Optional[int] = Field(None)
380
+ max_requests: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS")
381
+ max_requests_jitter: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS_JITTER")
382
+ model_config = SettingsConfigDict(
383
+ env_file=".env",
384
+ extra="ignore",
385
+ env_file_encoding="utf-8",
386
+ populate_by_name=True,
387
+ )
388
+
389
+ @model_validator(mode="after")
390
+ def validate_graceful_timeout(self) -> Self:
391
+ if self.graceful_timeout is None:
392
+ self.graceful_timeout = self.worker_timeout * 2
393
+ return self
385
394
 
386
395
  def as_gunicorn_conf(self) -> Dict[str, Any]:
387
396
  return {
@@ -405,54 +414,45 @@ class SovereignConfig(BaseSettings):
405
414
  sources: List[ConfiguredSource]
406
415
  templates: Dict[str, Dict[str, Union[str, Loadable]]]
407
416
  template_context: Dict[str, Any] = {}
408
- eds_priority_matrix: Dict[str, Dict[str, str]] = {}
417
+ eds_priority_matrix: Dict[str, Dict[str, int]] = {}
409
418
  modifiers: List[str] = []
410
419
  global_modifiers: List[str] = []
411
420
  regions: List[str] = []
412
421
  statsd: StatsdConfig = StatsdConfig()
413
- auth_enabled: bool = False
414
- auth_passwords: str = ""
415
- encryption_key: str = ""
416
- environment: str = "local"
417
- debug_enabled: bool = False
418
- sentry_dsn: str = ""
419
- node_match_key: str = "cluster"
420
- node_matching: bool = True
421
- source_match_key: str = "service_clusters"
422
- sources_refresh_rate: int = 30
423
- cache_strategy: str = "context"
424
- refresh_context: bool = False
425
- context_refresh_rate: Optional[int]
426
- context_refresh_cron: Optional[str]
427
- dns_hard_fail: bool = False
428
- enable_application_logs: bool = True
429
- enable_access_logs: bool = True
430
- log_fmt: Optional[str] = ""
431
- ignore_empty_log_fields: bool = False
422
+ auth_enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
423
+ auth_passwords: str = Field("", alias="SOVEREIGN_AUTH_PASSWORDS")
424
+ encryption_key: str = Field("", alias="SOVEREIGN_ENCRYPTION_KEY")
425
+ environment: str = Field("local", alias="SOVEREIGN_ENVIRONMENT")
426
+ debug_enabled: bool = Field(False, alias="SOVEREIGN_DEBUG_ENABLED")
427
+ sentry_dsn: str = Field("", alias="SOVEREIGN_SENTRY_DSN")
428
+ node_match_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
429
+ node_matching: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING")
430
+ source_match_key: str = Field(
431
+ "service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY"
432
+ )
433
+ sources_refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
434
+ cache_strategy: str = Field("context", alias="SOVEREIGN_CACHE_STRATEGY")
435
+ refresh_context: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
436
+ context_refresh_rate: Optional[int] = Field(
437
+ None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE"
438
+ )
439
+ context_refresh_cron: Optional[str] = Field(
440
+ None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON"
441
+ )
442
+ dns_hard_fail: bool = Field(False, alias="SOVEREIGN_DNS_HARD_FAIL")
443
+ enable_application_logs: bool = Field(
444
+ True, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS"
445
+ )
446
+ enable_access_logs: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
447
+ log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
448
+ ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
432
449
  discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
433
-
434
- class Config:
435
- fields = {
436
- "auth_enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
437
- "auth_passwords": {"env": "SOVEREIGN_AUTH_PASSWORDS"},
438
- "encryption_key": {"env": "SOVEREIGN_ENCRYPTION_KEY"},
439
- "environment": {"env": "SOVEREIGN_ENVIRONMENT"},
440
- "debug_enabled": {"env": "SOVEREIGN_DEBUG_ENABLED"},
441
- "sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
442
- "node_match_key": {"env": "SOVEREIGN_NODE_MATCH_KEY"},
443
- "node_matching": {"env": "SOVEREIGN_NODE_MATCHING"},
444
- "source_match_key": {"env": "SOVEREIGN_SOURCE_MATCH_KEY"},
445
- "sources_refresh_rate": {"env": "SOVEREIGN_SOURCES_REFRESH_RATE"},
446
- "cache_strategy": {"env": "SOVEREIGN_CACHE_STRATEGY"},
447
- "refresh_context": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
448
- "context_refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
449
- "context_refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
450
- "dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
451
- "enable_application_logs": {"env": "SOVEREIGN_ENABLE_APPLICATION_LOGS"},
452
- "enable_access_logs": {"env": "SOVEREIGN_ENABLE_ACCESS_LOGS"},
453
- "log_fmt": {"env": "SOVEREIGN_LOG_FORMAT"},
454
- "ignore_empty_fields": {"env": "SOVEREIGN_LOG_IGNORE_EMPTY"},
455
- }
450
+ model_config = SettingsConfigDict(
451
+ env_file=".env",
452
+ extra="ignore",
453
+ env_file_encoding="utf-8",
454
+ populate_by_name=True,
455
+ )
456
456
 
457
457
  @property
458
458
  def passwords(self) -> List[str]:
@@ -492,16 +492,15 @@ class TemplateSpecification(BaseModel):
492
492
 
493
493
 
494
494
  class NodeMatching(BaseSettings):
495
- enabled: bool = True
496
- source_key: str = "service_clusters"
497
- node_key: str = "cluster"
498
-
499
- class Config:
500
- fields = {
501
- "enabled": {"env": "SOVEREIGN_NODE_MATCHING_ENABLED"},
502
- "source_key": {"env": "SOVEREIGN_SOURCE_MATCH_KEY"},
503
- "node_key": {"env": "SOVEREIGN_NODE_MATCH_KEY"},
504
- }
495
+ enabled: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING_ENABLED")
496
+ source_key: str = Field("service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY")
497
+ node_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
498
+ model_config = SettingsConfigDict(
499
+ env_file=".env",
500
+ extra="ignore",
501
+ env_file_encoding="utf-8",
502
+ populate_by_name=True,
503
+ )
505
504
 
506
505
 
507
506
  @dataclass
@@ -511,9 +510,15 @@ class EncryptionConfig:
511
510
 
512
511
 
513
512
  class AuthConfiguration(BaseSettings):
514
- enabled: bool = False
515
- auth_passwords: SecretStr = SecretStr("")
516
- encryption_key: SecretStr = SecretStr("")
513
+ enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
514
+ auth_passwords: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_AUTH_PASSWORDS")
515
+ encryption_key: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_ENCRYPTION_KEY")
516
+ model_config = SettingsConfigDict(
517
+ env_file=".env",
518
+ extra="ignore",
519
+ env_file_encoding="utf-8",
520
+ populate_by_name=True,
521
+ )
517
522
 
518
523
  @staticmethod
519
524
  def _create_encryption_config(encryption_key_setting: str) -> EncryptionConfig:
@@ -534,37 +539,29 @@ class AuthConfiguration(BaseSettings):
534
539
  )
535
540
  return configs
536
541
 
537
- class Config:
538
- fields = {
539
- "enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
540
- "auth_passwords": {"env": "SOVEREIGN_AUTH_PASSWORDS"},
541
- "encryption_key": {"env": "SOVEREIGN_ENCRYPTION_KEY"},
542
- }
543
-
544
542
 
545
543
  class ApplicationLogConfiguration(BaseSettings):
546
- enabled: bool = False
547
- log_fmt: Optional[str] = None
544
+ enabled: bool = Field(False, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS")
545
+ log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_APPLICATION_LOG_FORMAT")
548
546
  # currently only support /dev/stdout as JSON
549
-
550
- class Config:
551
- fields = {
552
- "enabled": {"env": "SOVEREIGN_ENABLE_APPLICATION_LOGS"},
553
- "log_fmt": {"env": "SOVEREIGN_APPLICATION_LOG_FORMAT"},
554
- }
547
+ model_config = SettingsConfigDict(
548
+ env_file=".env",
549
+ extra="ignore",
550
+ env_file_encoding="utf-8",
551
+ populate_by_name=True,
552
+ )
555
553
 
556
554
 
557
555
  class AccessLogConfiguration(BaseSettings):
558
- enabled: bool = True
559
- log_fmt: Optional[str] = None
560
- ignore_empty_fields: bool = False
561
-
562
- class Config:
563
- fields = {
564
- "enabled": {"env": "SOVEREIGN_ENABLE_ACCESS_LOGS"},
565
- "log_fmt": {"env": "SOVEREIGN_LOG_FORMAT"},
566
- "ignore_empty_fields": {"env": "SOVEREIGN_LOG_IGNORE_EMPTY"},
567
- }
556
+ enabled: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
557
+ log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_LOG_FORMAT")
558
+ ignore_empty_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
559
+ model_config = SettingsConfigDict(
560
+ env_file=".env",
561
+ extra="ignore",
562
+ env_file_encoding="utf-8",
563
+ populate_by_name=True,
564
+ )
568
565
 
569
566
 
570
567
  class LoggingConfiguration(BaseSettings):
@@ -574,11 +571,19 @@ class LoggingConfiguration(BaseSettings):
574
571
 
575
572
  class ContextConfiguration(BaseSettings):
576
573
  context: Dict[str, Loadable] = {}
577
- refresh: bool = False
578
- refresh_rate: Optional[int] = None
579
- refresh_cron: Optional[str] = None
580
- refresh_num_retries: int = 3
581
- refresh_retry_interval_secs: int = 10
574
+ refresh: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
575
+ refresh_rate: Optional[int] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE")
576
+ refresh_cron: Optional[str] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON")
577
+ refresh_num_retries: int = Field(3, alias="SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES")
578
+ refresh_retry_interval_secs: int = Field(
579
+ 10, alias="SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
580
+ )
581
+ model_config = SettingsConfigDict(
582
+ env_file=".env",
583
+ extra="ignore",
584
+ env_file_encoding="utf-8",
585
+ populate_by_name=True,
586
+ )
582
587
 
583
588
  @staticmethod
584
589
  def context_from_legacy(context: Dict[str, str]) -> Dict[str, Loadable]:
@@ -587,20 +592,16 @@ class ContextConfiguration(BaseSettings):
587
592
  ret[key] = Loadable.from_legacy_fmt(value)
588
593
  return ret
589
594
 
590
- @root_validator(pre=False)
591
- def validate_single_use_refresh_method(
592
- cls, values: Dict[str, Any]
593
- ) -> Dict[str, Any]:
594
- refresh_rate = values.get("refresh_rate")
595
- refresh_cron = values.get("refresh_cron")
596
-
597
- if (refresh_rate is not None) and (refresh_cron is not None):
595
+ @model_validator(mode="after")
596
+ def validate_single_use_refresh_method(self) -> Self:
597
+ if (self.refresh_rate is not None) and (self.refresh_cron is not None):
598
598
  raise RuntimeError(
599
- f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {refresh_rate=} and {refresh_cron=}"
599
+ f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {self.refresh_rate=} and {self.refresh_cron=}"
600
600
  )
601
- return values
601
+ return self
602
602
 
603
- @root_validator
603
+ @model_validator(mode="before")
604
+ @classmethod
604
605
  def set_default_refresh_rate(cls, values: Dict[str, Any]) -> Dict[str, Any]:
605
606
  refresh_rate = values.get("refresh_rate")
606
607
  refresh_cron = values.get("refresh_cron")
@@ -609,7 +610,8 @@ class ContextConfiguration(BaseSettings):
609
610
  values["refresh_rate"] = 3600
610
611
  return values
611
612
 
612
- @validator("refresh_cron")
613
+ @field_validator("refresh_cron")
614
+ @classmethod
613
615
  def validate_refresh_cron(cls, v: Optional[str]) -> Optional[str]:
614
616
  if v is None:
615
617
  return v
@@ -617,36 +619,34 @@ class ContextConfiguration(BaseSettings):
617
619
  raise CroniterBadCronError(f"'{v}' is not a valid cron expression")
618
620
  return v
619
621
 
620
- class Config:
621
- fields = {
622
- "refresh": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
623
- "refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
624
- "refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
625
- "refresh_num_retries": {"env": "SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES"},
626
- "refresh_retry_interval_secs": {
627
- "env": "SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
628
- },
629
- }
630
-
631
622
 
632
623
  class SourcesConfiguration(BaseSettings):
633
- refresh_rate: int = 30
634
- cache_strategy: CacheStrategy = CacheStrategy.context
635
-
636
- class Config:
637
- fields = {
638
- "refresh_rate": {"env": "SOVEREIGN_SOURCES_REFRESH_RATE"},
639
- "cache_strategy": {"env": "SOVEREIGN_CACHE_STRATEGY"},
640
- }
624
+ refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
625
+ cache_strategy: CacheStrategy = Field(
626
+ CacheStrategy.context, alias="SOVEREIGN_CACHE_STRATEGY"
627
+ )
628
+ model_config = SettingsConfigDict(
629
+ env_file=".env",
630
+ extra="ignore",
631
+ env_file_encoding="utf-8",
632
+ populate_by_name=True,
633
+ )
641
634
 
642
635
 
643
636
  class LegacyConfig(BaseSettings):
644
637
  regions: Optional[List[str]] = None
645
- eds_priority_matrix: Optional[Dict[str, Dict[str, str]]] = None
646
- dns_hard_fail: Optional[bool] = None
647
- environment: Optional[str] = None
638
+ eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
639
+ dns_hard_fail: Optional[bool] = Field(None, alias="SOVEREIGN_DNS_HARD_FAIL")
640
+ environment: Optional[str] = Field(None, alias="SOVEREIGN_ENVIRONMENT")
641
+ model_config = SettingsConfigDict(
642
+ env_file=".env",
643
+ extra="ignore",
644
+ env_file_encoding="utf-8",
645
+ populate_by_name=True,
646
+ )
648
647
 
649
- @validator("regions")
648
+ @field_validator("regions")
649
+ @classmethod
650
650
  def regions_is_set(cls, v: Optional[List[str]]) -> List[str]:
651
651
  if v is not None:
652
652
  warnings.warn(
@@ -659,7 +659,8 @@ class LegacyConfig(BaseSettings):
659
659
  else:
660
660
  return []
661
661
 
662
- @validator("eds_priority_matrix")
662
+ @field_validator("eds_priority_matrix")
663
+ @classmethod
663
664
  def eds_priority_matrix_is_set(
664
665
  cls, v: Optional[Dict[str, Dict[str, Any]]]
665
666
  ) -> Dict[str, Dict[str, Any]]:
@@ -674,7 +675,8 @@ class LegacyConfig(BaseSettings):
674
675
  else:
675
676
  return {}
676
677
 
677
- @validator("dns_hard_fail")
678
+ @field_validator("dns_hard_fail")
679
+ @classmethod
678
680
  def dns_hard_fail_is_set(cls, v: Optional[bool]) -> bool:
679
681
  if v is not None:
680
682
  warnings.warn(
@@ -688,7 +690,8 @@ class LegacyConfig(BaseSettings):
688
690
  else:
689
691
  return False
690
692
 
691
- @validator("environment")
693
+ @field_validator("environment")
694
+ @classmethod
692
695
  def environment_is_set(cls, v: Optional[str]) -> Optional[str]:
693
696
  if v is not None:
694
697
  warnings.warn(
@@ -701,12 +704,6 @@ class LegacyConfig(BaseSettings):
701
704
  else:
702
705
  return None
703
706
 
704
- class Config:
705
- fields = {
706
- "dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
707
- "environment": {"env": "SOVEREIGN_ENVIRONMENT"},
708
- }
709
-
710
707
 
711
708
  class SovereignConfigv2(BaseSettings):
712
709
  sources: List[ConfiguredSource]
@@ -719,16 +716,16 @@ class SovereignConfigv2(BaseSettings):
719
716
  authentication: AuthConfiguration = AuthConfiguration()
720
717
  logging: LoggingConfiguration = LoggingConfiguration()
721
718
  statsd: StatsdConfig = StatsdConfig()
722
- sentry_dsn: SecretStr = SecretStr("")
723
- debug: bool = False
719
+ sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
720
+ debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
724
721
  legacy_fields: LegacyConfig = LegacyConfig()
725
722
  discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
726
-
727
- class Config:
728
- fields = {
729
- "sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
730
- "debug": {"env": "SOVEREIGN_DEBUG"},
731
- }
723
+ model_config = SettingsConfigDict(
724
+ env_file=".env",
725
+ extra="ignore",
726
+ env_file_encoding="utf-8",
727
+ populate_by_name=True,
728
+ )
732
729
 
733
730
  @property
734
731
  def passwords(self) -> List[str]:
@@ -751,10 +748,10 @@ class SovereignConfigv2(BaseSettings):
751
748
  return self.__repr__()
752
749
 
753
750
  def __repr__(self) -> str:
754
- return f"SovereignConfigv2({self.dict()})"
751
+ return f"SovereignConfigv2({self.model_dump()})"
755
752
 
756
753
  def show(self) -> Dict[str, Any]:
757
- return self.dict()
754
+ return self.model_dump()
758
755
 
759
756
  @staticmethod
760
757
  def from_legacy_config(other: SovereignConfig) -> "SovereignConfigv2":
@@ -79,6 +79,8 @@ class CipherContainer:
79
79
 
80
80
  @property
81
81
  def key_available(self) -> bool:
82
+ if not self.suites:
83
+ return False
82
84
  return self.suites[0].key_available
83
85
 
84
86
  AVAILABLE_CIPHERS: dict[EncryptionType | Literal["default"], type[CipherSuite]] = {
@@ -16,7 +16,7 @@ router = APIRouter()
16
16
  @router.get("/xds_dump", summary="Displays all xDS resources as JSON")
17
17
  async def display_config(
18
18
  xds_type: str = Query(
19
- ..., title="xDS type", description="The type of request", example="clusters"
19
+ ..., title="xDS type", description="The type of request", examples=["clusters"]
20
20
  ),
21
21
  service_cluster: str = Query(
22
22
  "*", title="The clients service cluster to emulate in this XDS request"
@@ -48,7 +48,7 @@ async def display_config(
48
48
  )
49
49
  async def debug_template(
50
50
  xds_type: str = Query(
51
- ..., title="xDS type", description="The type of request", example="clusters"
51
+ ..., title="xDS type", description="The type of request", examples=["clusters"]
52
52
  ),
53
53
  service_cluster: str = Query(
54
54
  "*", title="The clients service cluster to emulate in this XDS request"
File without changes
File without changes