sovereign 0.22.0__tar.gz → 0.24.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 (57) hide show
  1. {sovereign-0.22.0 → sovereign-0.24.0}/PKG-INFO +1 -1
  2. {sovereign-0.22.0 → sovereign-0.24.0}/pyproject.toml +1 -1
  3. sovereign-0.24.0/src/sovereign/__init__.py +105 -0
  4. sovereign-0.24.0/src/sovereign/app.py +126 -0
  5. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/config_loader.py +16 -5
  6. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/configuration.py +2 -0
  7. sovereign-0.24.0/src/sovereign/context.py +179 -0
  8. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/discovery.py +10 -10
  9. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/middlewares.py +9 -10
  10. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/modifiers/test.py +2 -2
  11. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/schemas.py +55 -4
  12. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/server.py +2 -2
  13. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/auth.py +9 -9
  14. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/eds.py +4 -4
  15. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/templates.py +3 -3
  16. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/admin.py +8 -8
  17. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/crypto.py +4 -5
  18. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/discovery.py +20 -19
  19. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/healthchecks.py +1 -3
  20. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/interface.py +8 -10
  21. sovereign-0.22.0/src/sovereign/__init__.py +0 -19
  22. sovereign-0.22.0/src/sovereign/app.py +0 -117
  23. sovereign-0.22.0/src/sovereign/context.py +0 -125
  24. {sovereign-0.22.0 → sovereign-0.24.0}/LICENSE.txt +0 -0
  25. {sovereign-0.22.0 → sovereign-0.24.0}/README.md +0 -0
  26. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/constants.py +0 -0
  27. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/error_info.py +0 -0
  28. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/access_logger.py +0 -0
  29. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/application_logger.py +0 -0
  30. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/base_logger.py +0 -0
  31. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/bootstrapper.py +0 -0
  32. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/types.py +0 -0
  33. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/modifiers/__init__.py +0 -0
  34. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/modifiers/lib.py +0 -0
  35. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/response_class.py +0 -0
  36. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/__init__.py +0 -0
  37. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/file.py +0 -0
  38. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/inline.py +0 -0
  39. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/lib.py +0 -0
  40. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/poller.py +0 -0
  41. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/static/sass/style.scss +0 -0
  42. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/static/style.css +0 -0
  43. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/statistics.py +0 -0
  44. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/base.html +0 -0
  45. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/err.html +0 -0
  46. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/resources.html +0 -0
  47. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/ul_filter.html +0 -0
  48. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/__init__.py +0 -0
  49. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/crypto.py +0 -0
  50. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/dictupdate.py +0 -0
  51. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/entry_point_loader.py +0 -0
  52. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/mock.py +0 -0
  53. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/resources.py +0 -0
  54. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/timer.py +0 -0
  55. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/version_info.py +0 -0
  56. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/weighted_clusters.py +0 -0
  57. {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 0.22.0
3
+ Version: 0.24.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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sovereign"
3
- version = "0.22.0"
3
+ version = "0.24.0"
4
4
  description = "Envoy Proxy control-plane written in Python"
5
5
  license = "Apache-2.0"
6
6
  packages = [
@@ -0,0 +1,105 @@
1
+ import os
2
+ from contextvars import ContextVar
3
+ from typing import Type, Any, Mapping
4
+ from importlib.metadata import version
5
+
6
+ from fastapi.responses import JSONResponse
7
+ from starlette.templating import Jinja2Templates
8
+ from pydantic.error_wrappers import ValidationError
9
+
10
+ from sovereign.schemas import (
11
+ SovereignAsgiConfig,
12
+ SovereignConfig,
13
+ SovereignConfigv2,
14
+ )
15
+ from sovereign import config_loader
16
+ from sovereign.logging.bootstrapper import LoggerBootstrapper
17
+ from sovereign.statistics import configure_statsd
18
+ from sovereign.utils.dictupdate import merge # type: ignore
19
+ from sovereign.sources import SourcePoller
20
+ from sovereign.context import TemplateContext
21
+ from sovereign.utils.crypto import CipherContainer, create_cipher_suite
22
+ from sovereign.utils.resources import get_package_file
23
+
24
+
25
+ json_response_class: Type[JSONResponse] = JSONResponse
26
+ try:
27
+ import orjson
28
+ from fastapi.responses import ORJSONResponse
29
+
30
+ json_response_class = ORJSONResponse
31
+ except ImportError:
32
+ try:
33
+ import ujson
34
+ from fastapi.responses import UJSONResponse
35
+
36
+ json_response_class = UJSONResponse
37
+ except ImportError:
38
+ pass
39
+
40
+
41
+ def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
42
+ ret: Mapping[Any, Any] = dict()
43
+ for p in path.split(","):
44
+ spec = config_loader.Loadable.from_legacy_fmt(p)
45
+ ret = merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
46
+ return ret
47
+
48
+
49
+ _request_id_ctx_var: ContextVar[str] = ContextVar("request_id", default="")
50
+
51
+
52
+ def get_request_id() -> str:
53
+ return _request_id_ctx_var.get()
54
+
55
+
56
+ DIST_NAME = "sovereign"
57
+
58
+ __version__ = version(DIST_NAME)
59
+ config_path = os.getenv("SOVEREIGN_CONFIG", "file:///etc/sovereign.yaml")
60
+
61
+ html_templates = Jinja2Templates(get_package_file(DIST_NAME, "templates")) # type: ignore[arg-type]
62
+
63
+ try:
64
+ config = SovereignConfigv2(**parse_raw_configuration(config_path))
65
+ except ValidationError:
66
+ old_config = SovereignConfig(**parse_raw_configuration(config_path))
67
+ config = SovereignConfigv2.from_legacy_config(old_config)
68
+ asgi_config = SovereignAsgiConfig()
69
+ XDS_TEMPLATES = config.xds_templates()
70
+
71
+ logs = LoggerBootstrapper(config)
72
+ stats = configure_statsd(config=config.statsd)
73
+ poller = SourcePoller(
74
+ sources=config.sources,
75
+ matching_enabled=config.matching.enabled,
76
+ node_match_key=config.matching.node_key,
77
+ source_match_key=config.matching.source_key,
78
+ source_refresh_rate=config.source_config.refresh_rate,
79
+ logger=logs.application_logger.logger,
80
+ stats=stats,
81
+ )
82
+
83
+ fernet_keys = config.authentication.encryption_key
84
+ encryption_keys = fernet_keys.get_secret_value().encode().split()
85
+ cipher_suite = CipherContainer(
86
+ [
87
+ create_cipher_suite(key=key, logger=logs.application_logger.logger)
88
+ for key in encryption_keys
89
+ ]
90
+ )
91
+
92
+ template_context = TemplateContext(
93
+ refresh_rate=config.template_context.refresh_rate,
94
+ refresh_cron=config.template_context.refresh_cron,
95
+ refresh_num_retries=config.template_context.refresh_num_retries,
96
+ refresh_retry_interval_secs=config.template_context.refresh_retry_interval_secs,
97
+ configured_context=config.template_context.context,
98
+ poller=poller,
99
+ encryption_suite=cipher_suite,
100
+ disabled_suite=create_cipher_suite(b"", logs.application_logger.logger),
101
+ logger=logs.application_logger.logger,
102
+ stats=stats,
103
+ )
104
+ poller.lazy_load_modifiers(config.modifiers)
105
+ poller.lazy_load_global_modifiers(config.global_modifiers)
@@ -0,0 +1,126 @@
1
+ import asyncio
2
+ import traceback
3
+ import uvicorn
4
+ from collections import namedtuple
5
+ from fastapi import FastAPI, Request
6
+ from fastapi.responses import RedirectResponse, FileResponse, Response, JSONResponse
7
+ from sovereign import (
8
+ __version__,
9
+ config,
10
+ asgi_config,
11
+ json_response_class,
12
+ poller,
13
+ template_context,
14
+ logs,
15
+ )
16
+ from sovereign.error_info import ErrorInfo
17
+ from sovereign.views import crypto, discovery, healthchecks, admin, interface
18
+ from sovereign.middlewares import (
19
+ RequestContextLogMiddleware,
20
+ LoggingMiddleware,
21
+ )
22
+ from sovereign.utils.resources import get_package_file
23
+
24
+ Router = namedtuple("Router", "module tags prefix")
25
+
26
+ DEBUG = config.debug
27
+ SENTRY_DSN = config.sentry_dsn.get_secret_value()
28
+
29
+ try:
30
+ import sentry_sdk
31
+ from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
32
+
33
+ SENTRY_INSTALLED = True
34
+ except ImportError: # pragma: no cover
35
+ SENTRY_INSTALLED = False
36
+
37
+
38
+ def generic_error_response(e: Exception) -> JSONResponse:
39
+ """
40
+ Responds with a JSON object containing basic context
41
+ about the exception passed in to this function.
42
+
43
+ If the server is in debug mode, it will include a traceback in the response.
44
+
45
+ The traceback is **always** emitted in logs.
46
+ """
47
+ tb = [line for line in traceback.format_exc().split("\n")]
48
+ info = ErrorInfo.from_exception(e)
49
+ logs.access_logger.queue_log_fields(
50
+ ERROR=info.error,
51
+ ERROR_DETAIL=info.detail,
52
+ TRACEBACK=tb,
53
+ )
54
+ # Don't expose tracebacks in responses, but add it to the logs
55
+ if DEBUG:
56
+ info.traceback = tb
57
+ return json_response_class(
58
+ content=info.response, status_code=getattr(e, "status_code", 500)
59
+ )
60
+
61
+
62
+ def init_app() -> FastAPI:
63
+ application = FastAPI(
64
+ title="Sovereign",
65
+ version=__version__,
66
+ debug=DEBUG,
67
+ default_response_class=json_response_class,
68
+ )
69
+
70
+ routers = (
71
+ Router(discovery.router, ["Configuration Discovery"], ""),
72
+ Router(crypto.router, ["Cryptographic Utilities"], "/crypto"),
73
+ Router(admin.router, ["Debugging Endpoints"], "/admin"),
74
+ Router(interface.router, ["User Interface"], "/ui"),
75
+ Router(healthchecks.router, ["Healthchecks"], ""),
76
+ )
77
+ for router in routers:
78
+ application.include_router(
79
+ router.module, tags=router.tags, prefix=router.prefix
80
+ )
81
+
82
+ application.add_middleware(RequestContextLogMiddleware)
83
+ application.add_middleware(LoggingMiddleware)
84
+
85
+ if SENTRY_INSTALLED and SENTRY_DSN:
86
+ sentry_sdk.init(SENTRY_DSN)
87
+ application.add_middleware(SentryAsgiMiddleware)
88
+ logs.application_logger.logger.info("Sentry middleware enabled")
89
+
90
+ @application.exception_handler(500)
91
+ async def exception_handler(_: Request, exc: Exception) -> JSONResponse:
92
+ """
93
+ We cannot incur the execution of this function from unit tests
94
+ because the starlette test client simply returns exceptions and does
95
+ not run them through the exception handler.
96
+ Ergo, this is a facade function for `generic_error_response`
97
+ """
98
+ return generic_error_response(exc) # pragma: no cover
99
+
100
+ @application.get("/")
101
+ async def redirect_to_docs() -> Response:
102
+ return RedirectResponse("/ui")
103
+
104
+ @application.get("/static/{filename}", summary="Return a static asset")
105
+ async def static(filename: str) -> Response:
106
+ return FileResponse(get_package_file("sovereign", f"static/{filename}")) # type: ignore[arg-type]
107
+
108
+ @application.on_event("startup")
109
+ async def keep_sources_uptodate() -> None:
110
+ asyncio.create_task(poller.poll_forever())
111
+
112
+ @application.on_event("startup")
113
+ async def refresh_template_context() -> None:
114
+ asyncio.create_task(template_context.start_refresh_context())
115
+
116
+ return application
117
+
118
+
119
+ app = init_app()
120
+ logs.application_logger.logger.info(
121
+ f"Sovereign started and listening on {asgi_config.host}:{asgi_config.port}"
122
+ )
123
+
124
+
125
+ if __name__ == "__main__": # pragma: no cover
126
+ uvicorn.run(app, host="0.0.0.0", port=8000, access_log=False)
@@ -7,7 +7,6 @@ import yaml
7
7
  import jinja2
8
8
  import requests
9
9
  import importlib
10
- from importlib.util import find_spec
11
10
  from importlib.machinery import SourceFileLoader
12
11
  from pathlib import Path
13
12
  from pydantic import BaseModel
@@ -57,13 +56,20 @@ serializers: Dict[Serialization, Callable[[Any], Any]] = {
57
56
  Serialization.raw: passthrough,
58
57
  }
59
58
 
60
- if find_spec("ujson"):
61
- ujson = importlib.import_module("ujson")
59
+ try:
60
+ import ujson
61
+
62
62
  serializers[Serialization.ujson] = ujson.loads
63
63
  jinja_env.policies["json.dumps_function"] = ujson.dumps
64
+ except ImportError:
65
+ # This lambda will raise an exception when the serializer is used; otherwise we should not crash
66
+ serializers[Serialization.ujson] = lambda *a, **kw: raise_(
67
+ ImportError("ujson must be installed to use in config_loaders")
68
+ )
69
+
70
+ try:
71
+ import orjson
64
72
 
65
- if find_spec("orjson"):
66
- orjson = importlib.import_module("orjson")
67
73
  serializers[Serialization.orjson] = orjson.loads
68
74
 
69
75
  # orjson.dumps returns bytes, so we have to wrap & decode it
@@ -81,6 +87,11 @@ if find_spec("orjson"):
81
87
 
82
88
  jinja_env.policies["json.dumps_function"] = orjson_dumps
83
89
  jinja_env.policies["json.dumps_kwargs"] = {"option": orjson.OPT_SORT_KEYS}
90
+ except ImportError:
91
+ # This lambda will raise an exception when the serializer is used; otherwise we should not crash
92
+ serializers[Serialization.orjson] = lambda *a, **kw: raise_(
93
+ ImportError("orjson must be installed to use in config_loaders")
94
+ )
84
95
 
85
96
  try:
86
97
  import boto3
@@ -61,6 +61,8 @@ POLLER = SourcePoller(
61
61
  TEMPLATE_CONTEXT = TemplateContext(
62
62
  refresh_rate=CONFIG.template_context.refresh_rate,
63
63
  refresh_cron=CONFIG.template_context.refresh_cron,
64
+ refresh_num_retries=CONFIG.template_context.refresh_num_retries,
65
+ refresh_retry_interval_secs=CONFIG.template_context.refresh_retry_interval_secs,
64
66
  configured_context=CONFIG.template_context.context,
65
67
  poller=POLLER,
66
68
  encryption_suite=CIPHER_SUITE,
@@ -0,0 +1,179 @@
1
+ import asyncio
2
+ import traceback
3
+ from copy import deepcopy
4
+ from typing import (
5
+ Any,
6
+ Awaitable,
7
+ Dict,
8
+ Generator,
9
+ Iterable,
10
+ NamedTuple,
11
+ NoReturn,
12
+ Optional,
13
+ )
14
+
15
+ from fastapi import HTTPException
16
+ from structlog.stdlib import BoundLogger
17
+
18
+ from sovereign.config_loader import Loadable
19
+ from sovereign.schemas import DiscoveryRequest, XdsTemplate
20
+ from sovereign.sources import SourcePoller
21
+ from sovereign.utils.crypto import CipherContainer, CipherSuite
22
+ from sovereign.utils.timer import poll_forever, poll_forever_cron
23
+
24
+
25
+ class LoadContextResponse(NamedTuple):
26
+ context_name: str
27
+ context: Dict[str, Any]
28
+
29
+
30
+ class TemplateContext:
31
+ def __init__(
32
+ self,
33
+ refresh_rate: Optional[int],
34
+ refresh_cron: Optional[str],
35
+ refresh_num_retries: int,
36
+ refresh_retry_interval_secs: int,
37
+ configured_context: Dict[str, Loadable],
38
+ poller: SourcePoller,
39
+ encryption_suite: Optional[CipherContainer],
40
+ disabled_suite: CipherSuite,
41
+ logger: BoundLogger,
42
+ stats: Any,
43
+ ) -> None:
44
+ self.poller = poller
45
+ self.refresh_rate = refresh_rate
46
+ self.refresh_cron = refresh_cron
47
+ self.refresh_num_retries = refresh_num_retries
48
+ self.refresh_retry_interval_secs = refresh_retry_interval_secs
49
+ self.configured_context = configured_context
50
+ self.crypto = encryption_suite
51
+ self.disabled_suite = disabled_suite
52
+ self.logger = logger
53
+ self.stats = stats
54
+ # initial load
55
+ self.context = asyncio.run(self.load_context_variables())
56
+
57
+ async def start_refresh_context(self) -> NoReturn:
58
+ if self.refresh_cron is not None:
59
+ await poll_forever_cron(self.refresh_cron, self.refresh_context)
60
+ elif self.refresh_rate is not None:
61
+ await poll_forever(self.refresh_rate, self.refresh_context)
62
+
63
+ raise RuntimeError("Failed to start refresh_context, this should never happen")
64
+
65
+ async def refresh_context(self) -> None:
66
+ self.context = await self.load_context_variables()
67
+
68
+ async def _load_context(
69
+ self,
70
+ context_name: str,
71
+ context_config: Loadable | str,
72
+ refresh_num_retries: int,
73
+ refresh_retry_interval_secs: int,
74
+ ) -> LoadContextResponse:
75
+ retries_left = refresh_num_retries
76
+ context_response = {}
77
+
78
+ while True:
79
+ try:
80
+ if isinstance(context_config, Loadable):
81
+ context_response = context_config.load()
82
+ elif isinstance(context_config, str):
83
+ context_response = Loadable.from_legacy_fmt(context_config).load()
84
+ self.stats.increment(
85
+ "context.refresh.success",
86
+ tags=[f"context:{context_name}"],
87
+ )
88
+ return LoadContextResponse(context_name, context_response)
89
+ # pylint: disable=broad-except
90
+ except Exception as e:
91
+ retries_left -= 1
92
+ if retries_left < 0:
93
+ tb = [line for line in traceback.format_exc().split("\n")]
94
+ self.logger.error(str(e), traceback=tb)
95
+ self.stats.increment(
96
+ "context.refresh.error",
97
+ tags=[f"context:{context_name}"],
98
+ )
99
+ return LoadContextResponse(context_name, context_response)
100
+ else:
101
+ await asyncio.sleep(refresh_retry_interval_secs)
102
+
103
+ async def load_context_variables(self) -> Dict[str, Any]:
104
+ context_response: Dict[str, Any] = dict()
105
+
106
+ context_coroutines: list[Awaitable[LoadContextResponse]] = []
107
+ for context_name, context_config in self.configured_context.items():
108
+ context_coroutines.append(
109
+ self._load_context(
110
+ context_name,
111
+ context_config,
112
+ self.refresh_num_retries,
113
+ self.refresh_retry_interval_secs,
114
+ )
115
+ )
116
+
117
+ context_results: list[LoadContextResponse] = await asyncio.gather(
118
+ *context_coroutines
119
+ )
120
+ for context_result in context_results:
121
+ context_response[context_result.context_name] = context_result.context
122
+
123
+ if "crypto" not in context_response and self.crypto:
124
+ context_response["crypto"] = self.crypto
125
+ return context_response
126
+
127
+ def build_new_context_from_instances(self, node_value: str) -> Dict[str, Any]:
128
+ matches = self.poller.match_node(node_value=node_value)
129
+ ret = dict()
130
+ for key, value in self.context.items():
131
+ try:
132
+ ret[key] = deepcopy(value)
133
+ except TypeError:
134
+ ret[key] = value
135
+
136
+ to_add = dict()
137
+ for scope, instances in matches.scopes.items():
138
+ if scope in ("default", None):
139
+ to_add["instances"] = instances
140
+ else:
141
+ to_add[scope] = instances
142
+ if to_add == {}:
143
+ raise HTTPException(
144
+ detail=(
145
+ "This node does not match any instances! ",
146
+ "If node matching is enabled, check that the node "
147
+ "match key aligns with the source match key. "
148
+ "If you don't know what any of this is, disable "
149
+ "node matching via the config",
150
+ ),
151
+ status_code=400,
152
+ )
153
+ ret.update(to_add)
154
+ return ret
155
+
156
+ def get_context(
157
+ self, request: DiscoveryRequest, template: XdsTemplate
158
+ ) -> Dict[str, Any]:
159
+ ret = self.build_new_context_from_instances(
160
+ node_value=self.poller.extract_node_key(request.node),
161
+ )
162
+ if request.hide_private_keys:
163
+ ret["crypto"] = self.disabled_suite
164
+ if not template.is_python_source:
165
+ keys_to_remove = self.unused_variables(list(ret), template.jinja_variables)
166
+ for key in keys_to_remove:
167
+ ret.pop(key, None)
168
+ return ret
169
+
170
+ @staticmethod
171
+ def unused_variables(
172
+ keys: Iterable[str], variables: Iterable[str]
173
+ ) -> Generator[str, None, None]:
174
+ for key in keys:
175
+ if key not in variables:
176
+ yield key
177
+
178
+ def get(self, *args: Any, **kwargs: Any) -> Any:
179
+ return self.context.get(*args, **kwargs)
@@ -20,9 +20,9 @@ try:
20
20
  except ImportError:
21
21
  SENTRY_INSTALLED = False
22
22
 
23
+ from sovereign import XDS_TEMPLATES, config, logs, template_context
23
24
  from sovereign.utils.version_info import compute_hash
24
- from sovereign.schemas import XdsTemplate, DiscoveryRequest
25
- from sovereign.configuration import XDS_TEMPLATES, CONFIG, LOGS, TEMPLATE_CONTEXT
25
+ from sovereign.schemas import XdsTemplate, DiscoveryRequest, ProcessedTemplate
26
26
 
27
27
 
28
28
  try:
@@ -33,7 +33,7 @@ except KeyError:
33
33
  "https://vsyrakis.bitbucket.io/sovereign/docs/html/guides/tutorial.html#create-templates "
34
34
  )
35
35
 
36
- cache_strategy = CONFIG.source_config.cache_strategy
36
+ cache_strategy = config.source_config.cache_strategy
37
37
 
38
38
  # Create an enum that bases all the available discovery types off what has been configured
39
39
  discovery_types = (_type for _type in sorted(XDS_TEMPLATES["__any__"].keys()))
@@ -65,7 +65,7 @@ def select_template(
65
65
  )
66
66
 
67
67
 
68
- def response(request: DiscoveryRequest, xds_type: str) -> Dict[str, Any]:
68
+ def response(request: DiscoveryRequest, xds_type: str) -> ProcessedTemplate:
69
69
  """
70
70
  A Discovery **Request** typically looks something like:
71
71
 
@@ -103,7 +103,7 @@ def response(request: DiscoveryRequest, xds_type: str) -> Dict[str, Any]:
103
103
  discovery_request=request,
104
104
  host_header=request.desired_controlplane,
105
105
  resource_names=request.resources,
106
- **TEMPLATE_CONTEXT.get_context(request.node),
106
+ **template_context.get_context(request, template),
107
107
  )
108
108
  content = template(**context)
109
109
 
@@ -117,20 +117,20 @@ def response(request: DiscoveryRequest, xds_type: str) -> Dict[str, Any]:
117
117
 
118
118
  # Early return if the template is identical
119
119
  config_version = compute_hash(content)
120
- if config_version == request.version_info and not CONFIG.discovery_cache.enabled:
121
- return dict(version_info=config_version, resources=[])
120
+ if config_version == request.version_info and not config.discovery_cache.enabled:
121
+ return ProcessedTemplate(version_info=config_version, resources=[])
122
122
 
123
123
  if not isinstance(content, dict):
124
124
  raise RuntimeError(f"Attempting to filter unstructured data: {content}")
125
125
  resources = filter_resources(content["resources"], request.resources)
126
- return dict(resources=resources, version_info=config_version)
126
+ return ProcessedTemplate(resources=resources, version_info=config_version)
127
127
 
128
128
 
129
129
  def deserialize_config(content: str) -> Dict[str, Any]:
130
130
  try:
131
131
  envoy_configuration = yaml.safe_load(content)
132
132
  except (ParserError, ScannerError) as e:
133
- LOGS.access_logger.queue_log_fields(
133
+ logs.access_logger.queue_log_fields(
134
134
  error=repr(e),
135
135
  YAML_CONTEXT=e.context,
136
136
  YAML_CONTEXT_MARK=e.context_mark,
@@ -139,7 +139,7 @@ def deserialize_config(content: str) -> Dict[str, Any]:
139
139
  YAML_PROBLEM_MARK=e.problem_mark,
140
140
  )
141
141
 
142
- if SENTRY_INSTALLED and CONFIG.sentry_dsn:
142
+ if SENTRY_INSTALLED and config.sentry_dsn:
143
143
  sentry_sdk.capture_exception(e)
144
144
 
145
145
  raise HTTPException(
@@ -4,8 +4,7 @@ from uuid import uuid4
4
4
  from fastapi.requests import Request
5
5
  from fastapi.responses import Response
6
6
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
7
- from sovereign import get_request_id, _request_id_ctx_var
8
- from sovereign.configuration import CONFIG, LOGS, STATS
7
+ from sovereign import config, logs, get_request_id, _request_id_ctx_var, stats
9
8
 
10
9
 
11
10
  class RequestContextLogMiddleware(BaseHTTPMiddleware):
@@ -19,7 +18,7 @@ class RequestContextLogMiddleware(BaseHTTPMiddleware):
19
18
  finally:
20
19
  req_id = get_request_id()
21
20
  response.headers["X-Request-ID"] = req_id
22
- LOGS.access_logger.queue_log_fields(REQUEST_ID=req_id)
21
+ logs.access_logger.queue_log_fields(REQUEST_ID=req_id)
23
22
  _request_id_ctx_var.reset(token)
24
23
  return response
25
24
 
@@ -37,9 +36,9 @@ class LoggingMiddleware(BaseHTTPMiddleware):
37
36
  source_port = addr.port
38
37
  if xff := request.headers.get("X-Forwarded-For"):
39
38
  source_ip = xff.split(",")[0] # leftmost address
40
- LOGS.access_logger.clear_log_fields()
41
- LOGS.access_logger.queue_log_fields(
42
- ENVIRONMENT=CONFIG.legacy_fields.environment,
39
+ logs.access_logger.clear_log_fields()
40
+ logs.access_logger.queue_log_fields(
41
+ ENVIRONMENT=config.legacy_fields.environment,
43
42
  HOST=request.headers.get("host", "-"),
44
43
  METHOD=request.method,
45
44
  PATH=request.url.path,
@@ -54,7 +53,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
54
53
  response = await call_next(request)
55
54
  finally:
56
55
  duration = time.time() - start_time
57
- LOGS.access_logger.queue_log_fields(
56
+ logs.access_logger.queue_log_fields(
58
57
  BYTES_TX=response.headers.get("content-length", "-"),
59
58
  STATUS_CODE=response.status_code,
60
59
  DURATION=duration,
@@ -71,7 +70,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
71
70
  for k, v in request_info.items()
72
71
  if v is not None
73
72
  ]
74
- STATS.increment("discovery.rq_total", tags=tags)
75
- STATS.timing("discovery.rq_ms", value=duration * 1000, tags=tags)
76
- LOGS.access_logger.logger.info("request")
73
+ stats.increment("discovery.rq_total", tags=tags)
74
+ stats.timing("discovery.rq_ms", value=duration * 1000, tags=tags)
75
+ logs.access_logger.logger.info("request")
77
76
  return response
@@ -1,6 +1,6 @@
1
+ from sovereign import template_context
1
2
  from sovereign.modifiers.lib import Modifier
2
3
  from sovereign.utils import eds, templates
3
- from sovereign.configuration import TEMPLATE_CONTEXT
4
4
 
5
5
 
6
6
  class Test(Modifier):
@@ -8,7 +8,7 @@ class Test(Modifier):
8
8
  return True
9
9
 
10
10
  def apply(self) -> None:
11
- assert TEMPLATE_CONTEXT
11
+ assert template_context
12
12
  assert eds
13
13
  assert templates
14
14
  self.instance["modifier_test_executed"] = True