sovereign 0.26.0__tar.gz → 0.28.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.28.0}/PKG-INFO +4 -2
  2. {sovereign-0.26.0 → sovereign-0.28.0}/pyproject.toml +10 -2
  3. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/__init__.py +1 -1
  4. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/configuration.py +1 -1
  5. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/discovery.py +2 -0
  6. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/schemas.py +196 -188
  7. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/templates/resources.html +16 -6
  8. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/crypto/crypto.py +2 -0
  9. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/views/admin.py +2 -2
  10. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/views/interface.py +2 -2
  11. {sovereign-0.26.0 → sovereign-0.28.0}/LICENSE.txt +0 -0
  12. {sovereign-0.26.0 → sovereign-0.28.0}/README.md +0 -0
  13. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/app.py +0 -0
  14. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/config_loader.py +0 -0
  15. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/constants.py +0 -0
  16. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/context.py +0 -0
  17. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/error_info.py +0 -0
  18. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/logging/access_logger.py +0 -0
  19. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/logging/application_logger.py +0 -0
  20. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/logging/base_logger.py +0 -0
  21. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/logging/bootstrapper.py +0 -0
  22. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/logging/types.py +0 -0
  23. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/middlewares.py +0 -0
  24. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/modifiers/__init__.py +0 -0
  25. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/modifiers/lib.py +0 -0
  26. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/response_class.py +0 -0
  27. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/server.py +0 -0
  28. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/sources/__init__.py +0 -0
  29. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/sources/file.py +0 -0
  30. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/sources/inline.py +0 -0
  31. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/sources/lib.py +0 -0
  32. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/sources/poller.py +0 -0
  33. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/static/sass/style.scss +0 -0
  34. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/static/style.css +0 -0
  35. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/statistics.py +0 -0
  36. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/templates/base.html +0 -0
  37. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/templates/err.html +0 -0
  38. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/templates/ul_filter.html +0 -0
  39. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/testing/loaders.py +0 -0
  40. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/testing/modifiers.py +0 -0
  41. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/__init__.py +0 -0
  42. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/auth.py +0 -0
  43. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/crypto/__init__.py +0 -0
  44. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
  45. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
  46. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
  47. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
  48. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
  49. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/dictupdate.py +0 -0
  50. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/eds.py +0 -0
  51. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/entry_point_loader.py +0 -0
  52. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/mock.py +0 -0
  53. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/resources.py +0 -0
  54. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/templates.py +0 -0
  55. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/timer.py +0 -0
  56. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/version_info.py +0 -0
  57. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/utils/weighted_clusters.py +0 -0
  58. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/views/__init__.py +0 -0
  59. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/views/crypto.py +0 -0
  60. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/views/discovery.py +0 -0
  61. {sovereign-0.26.0 → sovereign-0.28.0}/src/sovereign/views/healthchecks.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.28.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.28.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
@@ -163,6 +163,8 @@ def filter_resources(
163
163
  that does not match the name of the resource.
164
164
  If Envoy did not specifically request anything, every resource is retained.
165
165
  """
166
+ if len(requested) == 0:
167
+ return generated
166
168
  return [resource for resource in generated if resource_name(resource) in requested]
167
169
 
168
170
 
@@ -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
@@ -290,9 +294,11 @@ class Resources(List[str]):
290
294
  """
291
295
 
292
296
  def __contains__(self, item: object) -> bool:
293
- if len(self) == 0:
297
+ if (
298
+ len(self) == 0
299
+ ): # TODO: refactor to remove overriding __contains__; its being used in multiple places
294
300
  return True
295
- return item in list(self)
301
+ return super().__contains__(item)
296
302
 
297
303
 
298
304
  class Status(BaseModel):
@@ -301,24 +307,29 @@ class Status(BaseModel):
301
307
  details: List[Any]
302
308
 
303
309
 
310
+ def resources_factory() -> Resources:
311
+ return Resources()
312
+
313
+
304
314
  class DiscoveryRequest(BaseModel):
305
315
  node: Node = Field(..., title="Node information about the envoy proxy")
306
316
  version_info: str = Field(
307
317
  "0", title="The version of the envoy clients current configuration"
308
318
  )
309
319
  resource_names: Resources = Field(
310
- Resources(), title="List of requested resource names"
320
+ default_factory=resources_factory, title="List of requested resource names"
311
321
  )
312
322
  hide_private_keys: bool = False
313
323
  type_url: Optional[str] = Field(
314
324
  None, title="The corresponding type_url for the requested resource"
315
325
  )
316
- desired_controlplane: str = Field(
326
+ desired_controlplane: Optional[str] = Field(
317
327
  None, title="The host header provided in the Discovery Request"
318
328
  )
319
- error_detail: Status = Field(
329
+ error_detail: Optional[Status] = Field(
320
330
  None, title="Error details from the previous xDS request"
321
331
  )
332
+ model_config = ConfigDict(arbitrary_types_allowed=True)
322
333
 
323
334
  @property
324
335
  def envoy_version(self) -> str:
@@ -328,6 +339,8 @@ class DiscoveryRequest(BaseModel):
328
339
  except AssertionError:
329
340
  try:
330
341
  build_version = self.node.build_version
342
+ if build_version is None:
343
+ return "default"
331
344
  _, version, *_ = build_version.split("/")
332
345
  except (AttributeError, ValueError):
333
346
  # TODO: log/metric this?
@@ -346,6 +359,11 @@ class DiscoveryRequest(BaseModel):
346
359
  self.desired_controlplane,
347
360
  )
348
361
 
362
+ @field_validator("resource_names", mode="before")
363
+ @classmethod
364
+ def validate_resources(cls, v: Union[Resources, List[str]]) -> Resources:
365
+ return Resources(v)
366
+
349
367
 
350
368
  class DiscoveryResponse(BaseModel):
351
369
  version_info: str = Field(
@@ -355,33 +373,35 @@ class DiscoveryResponse(BaseModel):
355
373
 
356
374
 
357
375
  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
376
+ host: str = Field("0.0.0.0", alias="SOVEREIGN_HOST")
377
+ port: int = Field(8080, alias="SOVEREIGN_PORT")
378
+ keepalive: int = Field(5, alias="SOVEREIGN_KEEPALIVE")
379
+ workers: int = Field(
380
+ default_factory=lambda: multiprocessing.cpu_count() * 2 + 1,
381
+ alias="SOVEREIGN_WORKERS",
382
+ )
383
+ threads: int = Field(1, alias="SOVEREIGN_THREADS")
363
384
  reuse_port: bool = True
364
- preload_app: bool = True
385
+ preload_app: bool = Field(True, alias="SOVEREIGN_PRELOAD")
365
386
  log_level: str = "warning"
366
387
  worker_class: str = "uvicorn.workers.UvicornWorker"
367
- worker_timeout: int = 30
388
+ worker_timeout: int = Field(30, alias="SOVEREIGN_WORKER_TIMEOUT")
368
389
  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
- }
390
+ graceful_timeout: Optional[int] = Field(None)
391
+ max_requests: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS")
392
+ max_requests_jitter: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS_JITTER")
393
+ model_config = SettingsConfigDict(
394
+ env_file=".env",
395
+ extra="ignore",
396
+ env_file_encoding="utf-8",
397
+ populate_by_name=True,
398
+ )
399
+
400
+ @model_validator(mode="after")
401
+ def validate_graceful_timeout(self) -> Self:
402
+ if self.graceful_timeout is None:
403
+ self.graceful_timeout = self.worker_timeout * 2
404
+ return self
385
405
 
386
406
  def as_gunicorn_conf(self) -> Dict[str, Any]:
387
407
  return {
@@ -405,54 +425,45 @@ class SovereignConfig(BaseSettings):
405
425
  sources: List[ConfiguredSource]
406
426
  templates: Dict[str, Dict[str, Union[str, Loadable]]]
407
427
  template_context: Dict[str, Any] = {}
408
- eds_priority_matrix: Dict[str, Dict[str, str]] = {}
428
+ eds_priority_matrix: Dict[str, Dict[str, int]] = {}
409
429
  modifiers: List[str] = []
410
430
  global_modifiers: List[str] = []
411
431
  regions: List[str] = []
412
432
  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
433
+ auth_enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
434
+ auth_passwords: str = Field("", alias="SOVEREIGN_AUTH_PASSWORDS")
435
+ encryption_key: str = Field("", alias="SOVEREIGN_ENCRYPTION_KEY")
436
+ environment: str = Field("local", alias="SOVEREIGN_ENVIRONMENT")
437
+ debug_enabled: bool = Field(False, alias="SOVEREIGN_DEBUG_ENABLED")
438
+ sentry_dsn: str = Field("", alias="SOVEREIGN_SENTRY_DSN")
439
+ node_match_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
440
+ node_matching: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING")
441
+ source_match_key: str = Field(
442
+ "service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY"
443
+ )
444
+ sources_refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
445
+ cache_strategy: str = Field("context", alias="SOVEREIGN_CACHE_STRATEGY")
446
+ refresh_context: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
447
+ context_refresh_rate: Optional[int] = Field(
448
+ None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE"
449
+ )
450
+ context_refresh_cron: Optional[str] = Field(
451
+ None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON"
452
+ )
453
+ dns_hard_fail: bool = Field(False, alias="SOVEREIGN_DNS_HARD_FAIL")
454
+ enable_application_logs: bool = Field(
455
+ True, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS"
456
+ )
457
+ enable_access_logs: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
458
+ log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
459
+ ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
432
460
  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
- }
461
+ model_config = SettingsConfigDict(
462
+ env_file=".env",
463
+ extra="ignore",
464
+ env_file_encoding="utf-8",
465
+ populate_by_name=True,
466
+ )
456
467
 
457
468
  @property
458
469
  def passwords(self) -> List[str]:
@@ -492,16 +503,15 @@ class TemplateSpecification(BaseModel):
492
503
 
493
504
 
494
505
  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
- }
506
+ enabled: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING_ENABLED")
507
+ source_key: str = Field("service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY")
508
+ node_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
509
+ model_config = SettingsConfigDict(
510
+ env_file=".env",
511
+ extra="ignore",
512
+ env_file_encoding="utf-8",
513
+ populate_by_name=True,
514
+ )
505
515
 
506
516
 
507
517
  @dataclass
@@ -511,9 +521,15 @@ class EncryptionConfig:
511
521
 
512
522
 
513
523
  class AuthConfiguration(BaseSettings):
514
- enabled: bool = False
515
- auth_passwords: SecretStr = SecretStr("")
516
- encryption_key: SecretStr = SecretStr("")
524
+ enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
525
+ auth_passwords: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_AUTH_PASSWORDS")
526
+ encryption_key: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_ENCRYPTION_KEY")
527
+ model_config = SettingsConfigDict(
528
+ env_file=".env",
529
+ extra="ignore",
530
+ env_file_encoding="utf-8",
531
+ populate_by_name=True,
532
+ )
517
533
 
518
534
  @staticmethod
519
535
  def _create_encryption_config(encryption_key_setting: str) -> EncryptionConfig:
@@ -534,37 +550,29 @@ class AuthConfiguration(BaseSettings):
534
550
  )
535
551
  return configs
536
552
 
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
553
 
545
554
  class ApplicationLogConfiguration(BaseSettings):
546
- enabled: bool = False
547
- log_fmt: Optional[str] = None
555
+ enabled: bool = Field(False, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS")
556
+ log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_APPLICATION_LOG_FORMAT")
548
557
  # 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
- }
558
+ model_config = SettingsConfigDict(
559
+ env_file=".env",
560
+ extra="ignore",
561
+ env_file_encoding="utf-8",
562
+ populate_by_name=True,
563
+ )
555
564
 
556
565
 
557
566
  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
- }
567
+ enabled: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
568
+ log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_LOG_FORMAT")
569
+ ignore_empty_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
570
+ model_config = SettingsConfigDict(
571
+ env_file=".env",
572
+ extra="ignore",
573
+ env_file_encoding="utf-8",
574
+ populate_by_name=True,
575
+ )
568
576
 
569
577
 
570
578
  class LoggingConfiguration(BaseSettings):
@@ -574,11 +582,19 @@ class LoggingConfiguration(BaseSettings):
574
582
 
575
583
  class ContextConfiguration(BaseSettings):
576
584
  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
585
+ refresh: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
586
+ refresh_rate: Optional[int] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE")
587
+ refresh_cron: Optional[str] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON")
588
+ refresh_num_retries: int = Field(3, alias="SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES")
589
+ refresh_retry_interval_secs: int = Field(
590
+ 10, alias="SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
591
+ )
592
+ model_config = SettingsConfigDict(
593
+ env_file=".env",
594
+ extra="ignore",
595
+ env_file_encoding="utf-8",
596
+ populate_by_name=True,
597
+ )
582
598
 
583
599
  @staticmethod
584
600
  def context_from_legacy(context: Dict[str, str]) -> Dict[str, Loadable]:
@@ -587,20 +603,16 @@ class ContextConfiguration(BaseSettings):
587
603
  ret[key] = Loadable.from_legacy_fmt(value)
588
604
  return ret
589
605
 
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):
606
+ @model_validator(mode="after")
607
+ def validate_single_use_refresh_method(self) -> Self:
608
+ if (self.refresh_rate is not None) and (self.refresh_cron is not None):
598
609
  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=}"
610
+ 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
611
  )
601
- return values
612
+ return self
602
613
 
603
- @root_validator
614
+ @model_validator(mode="before")
615
+ @classmethod
604
616
  def set_default_refresh_rate(cls, values: Dict[str, Any]) -> Dict[str, Any]:
605
617
  refresh_rate = values.get("refresh_rate")
606
618
  refresh_cron = values.get("refresh_cron")
@@ -609,7 +621,8 @@ class ContextConfiguration(BaseSettings):
609
621
  values["refresh_rate"] = 3600
610
622
  return values
611
623
 
612
- @validator("refresh_cron")
624
+ @field_validator("refresh_cron")
625
+ @classmethod
613
626
  def validate_refresh_cron(cls, v: Optional[str]) -> Optional[str]:
614
627
  if v is None:
615
628
  return v
@@ -617,36 +630,34 @@ class ContextConfiguration(BaseSettings):
617
630
  raise CroniterBadCronError(f"'{v}' is not a valid cron expression")
618
631
  return v
619
632
 
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
633
 
632
634
  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
- }
635
+ refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
636
+ cache_strategy: CacheStrategy = Field(
637
+ CacheStrategy.context, alias="SOVEREIGN_CACHE_STRATEGY"
638
+ )
639
+ model_config = SettingsConfigDict(
640
+ env_file=".env",
641
+ extra="ignore",
642
+ env_file_encoding="utf-8",
643
+ populate_by_name=True,
644
+ )
641
645
 
642
646
 
643
647
  class LegacyConfig(BaseSettings):
644
648
  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
649
+ eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
650
+ dns_hard_fail: Optional[bool] = Field(None, alias="SOVEREIGN_DNS_HARD_FAIL")
651
+ environment: Optional[str] = Field(None, alias="SOVEREIGN_ENVIRONMENT")
652
+ model_config = SettingsConfigDict(
653
+ env_file=".env",
654
+ extra="ignore",
655
+ env_file_encoding="utf-8",
656
+ populate_by_name=True,
657
+ )
648
658
 
649
- @validator("regions")
659
+ @field_validator("regions")
660
+ @classmethod
650
661
  def regions_is_set(cls, v: Optional[List[str]]) -> List[str]:
651
662
  if v is not None:
652
663
  warnings.warn(
@@ -659,7 +670,8 @@ class LegacyConfig(BaseSettings):
659
670
  else:
660
671
  return []
661
672
 
662
- @validator("eds_priority_matrix")
673
+ @field_validator("eds_priority_matrix")
674
+ @classmethod
663
675
  def eds_priority_matrix_is_set(
664
676
  cls, v: Optional[Dict[str, Dict[str, Any]]]
665
677
  ) -> Dict[str, Dict[str, Any]]:
@@ -674,7 +686,8 @@ class LegacyConfig(BaseSettings):
674
686
  else:
675
687
  return {}
676
688
 
677
- @validator("dns_hard_fail")
689
+ @field_validator("dns_hard_fail")
690
+ @classmethod
678
691
  def dns_hard_fail_is_set(cls, v: Optional[bool]) -> bool:
679
692
  if v is not None:
680
693
  warnings.warn(
@@ -688,7 +701,8 @@ class LegacyConfig(BaseSettings):
688
701
  else:
689
702
  return False
690
703
 
691
- @validator("environment")
704
+ @field_validator("environment")
705
+ @classmethod
692
706
  def environment_is_set(cls, v: Optional[str]) -> Optional[str]:
693
707
  if v is not None:
694
708
  warnings.warn(
@@ -701,12 +715,6 @@ class LegacyConfig(BaseSettings):
701
715
  else:
702
716
  return None
703
717
 
704
- class Config:
705
- fields = {
706
- "dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
707
- "environment": {"env": "SOVEREIGN_ENVIRONMENT"},
708
- }
709
-
710
718
 
711
719
  class SovereignConfigv2(BaseSettings):
712
720
  sources: List[ConfiguredSource]
@@ -719,16 +727,16 @@ class SovereignConfigv2(BaseSettings):
719
727
  authentication: AuthConfiguration = AuthConfiguration()
720
728
  logging: LoggingConfiguration = LoggingConfiguration()
721
729
  statsd: StatsdConfig = StatsdConfig()
722
- sentry_dsn: SecretStr = SecretStr("")
723
- debug: bool = False
730
+ sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
731
+ debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
724
732
  legacy_fields: LegacyConfig = LegacyConfig()
725
733
  discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
726
-
727
- class Config:
728
- fields = {
729
- "sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
730
- "debug": {"env": "SOVEREIGN_DEBUG"},
731
- }
734
+ model_config = SettingsConfigDict(
735
+ env_file=".env",
736
+ extra="ignore",
737
+ env_file_encoding="utf-8",
738
+ populate_by_name=True,
739
+ )
732
740
 
733
741
  @property
734
742
  def passwords(self) -> List[str]:
@@ -751,10 +759,10 @@ class SovereignConfigv2(BaseSettings):
751
759
  return self.__repr__()
752
760
 
753
761
  def __repr__(self) -> str:
754
- return f"SovereignConfigv2({self.dict()})"
762
+ return f"SovereignConfigv2({self.model_dump()})"
755
763
 
756
764
  def show(self) -> Dict[str, Any]:
757
- return self.dict()
765
+ return self.model_dump()
758
766
 
759
767
  @staticmethod
760
768
  def from_legacy_config(other: SovereignConfig) -> "SovereignConfigv2":
@@ -88,14 +88,24 @@
88
88
  1: 'resource'
89
89
  } %}
90
90
  {% for resource in res %}
91
+ {% if "sovereign_error" in resource %}
92
+ <span class="panel-icon">
93
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
94
+ </span>
95
+ <div class="notification is-danger">
96
+ {{ resource["sovereign_error"] }}
97
+ </div>
98
+ {{ resource["sovereign_error"] }}
99
+ {% else %}
91
100
  {% set name = resource.get('name') or resource['cluster_name'] %}
92
101
  <a class="panel-block has-text-weight-medium"
93
- href="/ui/resources/{{ resource_type }}/{{ name }}">
94
- <span class="panel-icon">
95
- <i class="fas fa-arrow-right" aria-hidden="true"></i>
96
- </span>
97
- {{ name }}
98
- </a>
102
+ href="/ui/resources/{{ resource_type }}/{{ name }}">
103
+ <span class="panel-icon">
104
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
105
+ </span>
106
+ {{ name }}
107
+ </a>
108
+ {% endif %}
99
109
  {% endfor %}
100
110
  <div class="panel-block">
101
111
  <p class="content is-small">
@@ -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"
@@ -103,8 +103,8 @@ async def resources(
103
103
  resource_type=xds_type,
104
104
  skip_auth=True,
105
105
  )
106
- except KeyError:
107
- ret["resources"] = []
106
+ except KeyError as e:
107
+ ret["resources"] = [{"sovereign_error": str(e)}]
108
108
  else:
109
109
  ret["resources"] += response.deserialize_resources()
110
110
  return html_templates.TemplateResponse(
File without changes
File without changes