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.
- {sovereign-0.22.0 → sovereign-0.24.0}/PKG-INFO +1 -1
- {sovereign-0.22.0 → sovereign-0.24.0}/pyproject.toml +1 -1
- sovereign-0.24.0/src/sovereign/__init__.py +105 -0
- sovereign-0.24.0/src/sovereign/app.py +126 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/config_loader.py +16 -5
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/configuration.py +2 -0
- sovereign-0.24.0/src/sovereign/context.py +179 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/discovery.py +10 -10
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/middlewares.py +9 -10
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/modifiers/test.py +2 -2
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/schemas.py +55 -4
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/server.py +2 -2
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/auth.py +9 -9
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/eds.py +4 -4
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/templates.py +3 -3
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/admin.py +8 -8
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/crypto.py +4 -5
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/discovery.py +20 -19
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/healthchecks.py +1 -3
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/interface.py +8 -10
- sovereign-0.22.0/src/sovereign/__init__.py +0 -19
- sovereign-0.22.0/src/sovereign/app.py +0 -117
- sovereign-0.22.0/src/sovereign/context.py +0 -125
- {sovereign-0.22.0 → sovereign-0.24.0}/LICENSE.txt +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/README.md +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/constants.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/error_info.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/access_logger.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/application_logger.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/base_logger.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/bootstrapper.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/logging/types.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/modifiers/__init__.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/modifiers/lib.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/response_class.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/__init__.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/file.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/inline.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/lib.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/sources/poller.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/static/sass/style.scss +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/static/style.css +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/statistics.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/base.html +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/err.html +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/resources.html +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/templates/ul_filter.html +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/__init__.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/crypto.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/dictupdate.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/entry_point_loader.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/mock.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/resources.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/timer.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/version_info.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/utils/weighted_clusters.py +0 -0
- {sovereign-0.22.0 → sovereign-0.24.0}/src/sovereign/views/__init__.py +0 -0
|
@@ -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
|
-
|
|
61
|
-
|
|
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 =
|
|
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) ->
|
|
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
|
-
**
|
|
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
|
|
121
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
11
|
+
assert template_context
|
|
12
12
|
assert eds
|
|
13
13
|
assert templates
|
|
14
14
|
self.instance["modifier_test_executed"] = True
|