sovereign 0.29.3__py3-none-any.whl → 0.30.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sovereign might be problematic. Click here for more details.
- sovereign/__init__.py +34 -40
- sovereign/app.py +11 -10
- sovereign/context.py +37 -24
- sovereign/discovery.py +8 -9
- sovereign/dynamic_config/deser.py +1 -2
- sovereign/dynamic_config/loaders.py +1 -2
- sovereign/logging/access_logger.py +14 -1
- sovereign/logging/application_logger.py +0 -1
- sovereign/logging/base_logger.py +3 -27
- sovereign/logging/bootstrapper.py +1 -1
- sovereign/middlewares.py +3 -2
- sovereign/modifiers/lib.py +1 -0
- sovereign/schemas.py +86 -37
- sovereign/sources/inline.py +1 -0
- sovereign/sources/lib.py +1 -0
- sovereign/sources/poller.py +8 -8
- sovereign/static/panel.js +56 -0
- sovereign/static/search_filter.js +20 -0
- sovereign/templates/resources.html +39 -36
- sovereign/tracing.py +52 -52
- sovereign/utils/crypto/suites/base_cipher.py +5 -10
- sovereign/utils/dictupdate.py +2 -1
- sovereign/views/admin.py +1 -0
- sovereign/views/crypto.py +2 -1
- sovereign/views/healthchecks.py +2 -1
- sovereign/views/interface.py +22 -6
- {sovereign-0.29.3.dist-info → sovereign-0.30.0.dist-info}/METADATA +46 -47
- {sovereign-0.29.3.dist-info → sovereign-0.30.0.dist-info}/RECORD +31 -31
- {sovereign-0.29.3.dist-info → sovereign-0.30.0.dist-info}/WHEEL +1 -1
- {sovereign-0.29.3.dist-info → sovereign-0.30.0.dist-info}/entry_points.txt +2 -0
- sovereign/configuration.py +0 -66
- sovereign/templates/ul_filter.html +0 -22
- {sovereign-0.29.3.dist-info → sovereign-0.30.0.dist-info}/LICENSE.txt +0 -0
sovereign/__init__.py
CHANGED
|
@@ -1,46 +1,26 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
from contextvars import ContextVar
|
|
3
4
|
from importlib.metadata import version
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Optional
|
|
5
6
|
|
|
6
|
-
from fastapi.responses import JSONResponse
|
|
7
7
|
from pydantic import ValidationError
|
|
8
8
|
from starlette.templating import Jinja2Templates
|
|
9
9
|
|
|
10
|
-
from sovereign import dynamic_config
|
|
11
10
|
from sovereign.context import TemplateContext
|
|
12
11
|
from sovereign.logging.bootstrapper import LoggerBootstrapper
|
|
13
|
-
from sovereign.schemas import
|
|
12
|
+
from sovereign.schemas import (
|
|
13
|
+
SovereignAsgiConfig,
|
|
14
|
+
SovereignConfig,
|
|
15
|
+
SovereignConfigv2,
|
|
16
|
+
migrate_configs,
|
|
17
|
+
parse_raw_configuration,
|
|
18
|
+
)
|
|
14
19
|
from sovereign.sources import SourcePoller
|
|
15
20
|
from sovereign.statistics import configure_statsd
|
|
16
21
|
from sovereign.utils.crypto.crypto import CipherContainer
|
|
17
|
-
from sovereign.utils.dictupdate import merge # type: ignore
|
|
18
22
|
from sovereign.utils.resources import get_package_file
|
|
19
23
|
|
|
20
|
-
json_response_class: Type[JSONResponse] = JSONResponse
|
|
21
|
-
try:
|
|
22
|
-
import orjson
|
|
23
|
-
from fastapi.responses import ORJSONResponse
|
|
24
|
-
|
|
25
|
-
json_response_class = ORJSONResponse
|
|
26
|
-
except ImportError:
|
|
27
|
-
try:
|
|
28
|
-
import ujson
|
|
29
|
-
from fastapi.responses import UJSONResponse
|
|
30
|
-
|
|
31
|
-
json_response_class = UJSONResponse
|
|
32
|
-
except ImportError:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
|
|
37
|
-
ret: Mapping[Any, Any] = dict()
|
|
38
|
-
for p in path.split(","):
|
|
39
|
-
spec = dynamic_config.Loadable.from_legacy_fmt(p)
|
|
40
|
-
ret = merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
|
|
41
|
-
return ret
|
|
42
|
-
|
|
43
|
-
|
|
44
24
|
_request_id_ctx_var: ContextVar[str] = ContextVar("request_id", default="")
|
|
45
25
|
|
|
46
26
|
|
|
@@ -57,6 +37,9 @@ html_templates = Jinja2Templates(
|
|
|
57
37
|
directory=str(get_package_file(DIST_NAME, "templates"))
|
|
58
38
|
)
|
|
59
39
|
|
|
40
|
+
if sys.argv[0].endswith("sovereign"):
|
|
41
|
+
migrate_configs()
|
|
42
|
+
|
|
60
43
|
try:
|
|
61
44
|
config = SovereignConfigv2(**parse_raw_configuration(config_path))
|
|
62
45
|
except ValidationError:
|
|
@@ -69,15 +52,25 @@ logs = LoggerBootstrapper(config)
|
|
|
69
52
|
application_logger = logs.application_logger.logger
|
|
70
53
|
|
|
71
54
|
stats = configure_statsd(config=config.statsd)
|
|
72
|
-
poller =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
55
|
+
poller = None
|
|
56
|
+
if config.sources is not None:
|
|
57
|
+
if config.matching is not None:
|
|
58
|
+
matching_enabled = config.matching.enabled
|
|
59
|
+
node_key: Optional[str] = config.matching.node_key
|
|
60
|
+
source_key: Optional[str] = config.matching.source_key
|
|
61
|
+
else:
|
|
62
|
+
matching_enabled = False
|
|
63
|
+
node_key = None
|
|
64
|
+
source_key = None
|
|
65
|
+
poller = SourcePoller(
|
|
66
|
+
sources=config.sources,
|
|
67
|
+
matching_enabled=matching_enabled,
|
|
68
|
+
node_match_key=node_key,
|
|
69
|
+
source_match_key=source_key,
|
|
70
|
+
source_refresh_rate=config.source_config.refresh_rate,
|
|
71
|
+
logger=application_logger,
|
|
72
|
+
stats=stats,
|
|
73
|
+
)
|
|
81
74
|
|
|
82
75
|
encryption_configs = config.authentication.encryption_configs
|
|
83
76
|
server_cipher_container = CipherContainer.from_encryption_configs(
|
|
@@ -95,5 +88,6 @@ template_context = TemplateContext(
|
|
|
95
88
|
logger=application_logger,
|
|
96
89
|
stats=stats,
|
|
97
90
|
)
|
|
98
|
-
poller
|
|
99
|
-
poller.
|
|
91
|
+
if poller is not None:
|
|
92
|
+
poller.lazy_load_modifiers(config.modifiers)
|
|
93
|
+
poller.lazy_load_global_modifiers(config.global_modifiers)
|
sovereign/app.py
CHANGED
|
@@ -4,24 +4,22 @@ from collections import namedtuple
|
|
|
4
4
|
|
|
5
5
|
import uvicorn
|
|
6
6
|
from fastapi import FastAPI, Request
|
|
7
|
-
from fastapi.responses import
|
|
7
|
+
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response
|
|
8
|
+
from starlette_context.middleware import RawContextMiddleware
|
|
8
9
|
|
|
9
10
|
from sovereign import (
|
|
10
11
|
__version__,
|
|
11
|
-
config,
|
|
12
12
|
asgi_config,
|
|
13
|
-
|
|
13
|
+
config,
|
|
14
|
+
logs,
|
|
14
15
|
poller,
|
|
15
16
|
template_context,
|
|
16
|
-
logs,
|
|
17
17
|
)
|
|
18
|
+
from sovereign.response_class import json_response_class
|
|
18
19
|
from sovereign.error_info import ErrorInfo
|
|
20
|
+
from sovereign.middlewares import LoggingMiddleware, RequestContextLogMiddleware
|
|
19
21
|
from sovereign.utils.resources import get_package_file
|
|
20
|
-
from sovereign.views import crypto, discovery, healthchecks,
|
|
21
|
-
from sovereign.middlewares import (
|
|
22
|
-
RequestContextLogMiddleware,
|
|
23
|
-
LoggingMiddleware,
|
|
24
|
-
)
|
|
22
|
+
from sovereign.views import admin, crypto, discovery, healthchecks, interface
|
|
25
23
|
|
|
26
24
|
Router = namedtuple("Router", "module tags prefix")
|
|
27
25
|
|
|
@@ -89,6 +87,8 @@ def init_app() -> FastAPI:
|
|
|
89
87
|
application.add_middleware(SentryAsgiMiddleware)
|
|
90
88
|
logs.application_logger.logger.info("Sentry middleware enabled")
|
|
91
89
|
|
|
90
|
+
application.add_middleware(RawContextMiddleware)
|
|
91
|
+
|
|
92
92
|
@application.exception_handler(500)
|
|
93
93
|
async def exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
94
94
|
"""
|
|
@@ -109,7 +109,8 @@ def init_app() -> FastAPI:
|
|
|
109
109
|
|
|
110
110
|
@application.on_event("startup")
|
|
111
111
|
async def keep_sources_uptodate() -> None:
|
|
112
|
-
|
|
112
|
+
if poller is not None:
|
|
113
|
+
asyncio.create_task(poller.poll_forever())
|
|
113
114
|
|
|
114
115
|
@application.on_event("startup")
|
|
115
116
|
async def refresh_template_context() -> None:
|
sovereign/context.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import warnings
|
|
2
3
|
import traceback
|
|
3
4
|
from copy import deepcopy
|
|
4
5
|
from typing import (
|
|
@@ -37,7 +38,7 @@ class TemplateContext:
|
|
|
37
38
|
refresh_num_retries: int,
|
|
38
39
|
refresh_retry_interval_secs: int,
|
|
39
40
|
configured_context: Dict[str, Loadable],
|
|
40
|
-
poller: SourcePoller,
|
|
41
|
+
poller: Optional[SourcePoller],
|
|
41
42
|
encryption_suite: Optional[CipherContainer],
|
|
42
43
|
logger: BoundLogger,
|
|
43
44
|
stats: Any,
|
|
@@ -121,41 +122,45 @@ class TemplateContext:
|
|
|
121
122
|
if "crypto" not in self.context and self.crypto:
|
|
122
123
|
self.context["crypto"] = self.crypto
|
|
123
124
|
|
|
124
|
-
def build_new_context_from_instances(
|
|
125
|
-
|
|
125
|
+
def build_new_context_from_instances(
|
|
126
|
+
self, node_value: Optional[str]
|
|
127
|
+
) -> Dict[str, Any]:
|
|
128
|
+
to_add = dict()
|
|
129
|
+
if self.poller is not None:
|
|
130
|
+
matches = self.poller.match_node(node_value=node_value)
|
|
131
|
+
for scope, instances in matches.scopes.items():
|
|
132
|
+
if scope in ("default", None):
|
|
133
|
+
to_add["instances"] = instances
|
|
134
|
+
else:
|
|
135
|
+
to_add[scope] = instances
|
|
136
|
+
if to_add == {}:
|
|
137
|
+
raise HTTPException(
|
|
138
|
+
detail=(
|
|
139
|
+
"This node does not match any instances! ",
|
|
140
|
+
"If node matching is enabled, check that the node "
|
|
141
|
+
"match key aligns with the source match key. "
|
|
142
|
+
"If you don't know what any of this is, disable "
|
|
143
|
+
"node matching via the config",
|
|
144
|
+
),
|
|
145
|
+
status_code=400,
|
|
146
|
+
)
|
|
126
147
|
ret = dict()
|
|
127
148
|
for key, value in self.context.items():
|
|
128
149
|
try:
|
|
129
150
|
ret[key] = deepcopy(value)
|
|
130
151
|
except TypeError:
|
|
131
152
|
ret[key] = value
|
|
132
|
-
|
|
133
|
-
to_add = dict()
|
|
134
|
-
for scope, instances in matches.scopes.items():
|
|
135
|
-
if scope in ("default", None):
|
|
136
|
-
to_add["instances"] = instances
|
|
137
|
-
else:
|
|
138
|
-
to_add[scope] = instances
|
|
139
|
-
if to_add == {}:
|
|
140
|
-
raise HTTPException(
|
|
141
|
-
detail=(
|
|
142
|
-
"This node does not match any instances! ",
|
|
143
|
-
"If node matching is enabled, check that the node "
|
|
144
|
-
"match key aligns with the source match key. "
|
|
145
|
-
"If you don't know what any of this is, disable "
|
|
146
|
-
"node matching via the config",
|
|
147
|
-
),
|
|
148
|
-
status_code=400,
|
|
149
|
-
)
|
|
150
153
|
ret.update(to_add)
|
|
151
154
|
return ret
|
|
152
155
|
|
|
153
156
|
def get_context(
|
|
154
157
|
self, request: DiscoveryRequest, template: XdsTemplate
|
|
155
158
|
) -> Dict[str, Any]:
|
|
156
|
-
ret =
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
ret = {}
|
|
160
|
+
node_value = None
|
|
161
|
+
if self.poller is not None and self.poller.node_match_key is not None:
|
|
162
|
+
node_value = self.poller.extract_node_key(request.node)
|
|
163
|
+
ret = self.build_new_context_from_instances(node_value=node_value)
|
|
159
164
|
ret["__hide_from_ui"] = lambda v: v
|
|
160
165
|
if request.hide_private_keys:
|
|
161
166
|
ret["__hide_from_ui"] = lambda _: "(value hidden)"
|
|
@@ -178,4 +183,12 @@ class TemplateContext:
|
|
|
178
183
|
yield key
|
|
179
184
|
|
|
180
185
|
def get(self, *args: Any, **kwargs: Any) -> Any:
|
|
186
|
+
warnings.warn(
|
|
187
|
+
(
|
|
188
|
+
"Accessing values from template_context directly is deprecated and "
|
|
189
|
+
"will be removed in a future release."
|
|
190
|
+
),
|
|
191
|
+
DeprecationWarning,
|
|
192
|
+
stacklevel=2,
|
|
193
|
+
)
|
|
181
194
|
return self.context.get(*args, **kwargs)
|
sovereign/discovery.py
CHANGED
|
@@ -6,12 +6,14 @@ Functions used to render and return discovery responses to Envoy proxies.
|
|
|
6
6
|
|
|
7
7
|
The templates are configurable. `todo See ref:Configuration#Templates`
|
|
8
8
|
"""
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
9
11
|
from enum import Enum
|
|
10
|
-
from typing import
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
11
13
|
|
|
12
14
|
import yaml
|
|
13
|
-
from yaml.parser import ParserError, ScannerError # type: ignore
|
|
14
15
|
from starlette.exceptions import HTTPException
|
|
16
|
+
from yaml.parser import ParserError, ScannerError # type: ignore
|
|
15
17
|
|
|
16
18
|
try:
|
|
17
19
|
import sentry_sdk
|
|
@@ -20,21 +22,18 @@ try:
|
|
|
20
22
|
except ImportError:
|
|
21
23
|
SENTRY_INSTALLED = False
|
|
22
24
|
|
|
23
|
-
from sovereign import XDS_TEMPLATES, config,
|
|
25
|
+
from sovereign import XDS_TEMPLATES, config, template_context, logs
|
|
26
|
+
from sovereign.schemas import DiscoveryRequest, ProcessedTemplate, XdsTemplate
|
|
24
27
|
from sovereign.utils.version_info import compute_hash
|
|
25
|
-
from sovereign.schemas import XdsTemplate, DiscoveryRequest, ProcessedTemplate
|
|
26
|
-
|
|
27
28
|
|
|
28
29
|
try:
|
|
29
30
|
default_templates = XDS_TEMPLATES["default"]
|
|
30
31
|
except KeyError:
|
|
31
|
-
|
|
32
|
+
warnings.warn(
|
|
32
33
|
"Your configuration should contain default templates. For more details, see "
|
|
33
|
-
"https://
|
|
34
|
+
"https://developer.atlassian.com/platform/sovereign/tutorial/templates/#versioning-templates"
|
|
34
35
|
)
|
|
35
36
|
|
|
36
|
-
cache_strategy = config.source_config.cache_strategy
|
|
37
|
-
|
|
38
37
|
# Create an enum that bases all the available discovery types off what has been configured
|
|
39
38
|
discovery_types = (_type for _type in sorted(XDS_TEMPLATES["__any__"].keys()))
|
|
40
39
|
discovery_types_base: Dict[str, str] = {t: t for t in discovery_types}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
1
2
|
from functools import cached_property
|
|
2
3
|
from typing import Any, Dict
|
|
3
4
|
|
|
4
5
|
import structlog
|
|
6
|
+
from starlette_context import context
|
|
5
7
|
from structlog.stdlib import BoundLogger
|
|
6
8
|
|
|
7
9
|
from sovereign.logging.base_logger import BaseLogger
|
|
@@ -20,7 +22,7 @@ class AccessLogger(BaseLogger):
|
|
|
20
22
|
wrapper_class=structlog.BoundLogger,
|
|
21
23
|
processors=[
|
|
22
24
|
self.is_enabled_processor,
|
|
23
|
-
self.
|
|
25
|
+
self.merge_starlette_contextvars,
|
|
24
26
|
self.format_access_log_fields,
|
|
25
27
|
],
|
|
26
28
|
type=LoggingType.ACCESS,
|
|
@@ -70,3 +72,14 @@ class AccessLogger(BaseLogger):
|
|
|
70
72
|
continue
|
|
71
73
|
formatted_dict[k] = value
|
|
72
74
|
return formatted_dict
|
|
75
|
+
|
|
76
|
+
def merge_starlette_contextvars(
|
|
77
|
+
self, _, __, event_dict: EventDict
|
|
78
|
+
) -> ProcessedMessage:
|
|
79
|
+
merged_context = deepcopy(event_dict)
|
|
80
|
+
for k, v in context.data.items():
|
|
81
|
+
merged_context[k] = v
|
|
82
|
+
return merged_context
|
|
83
|
+
|
|
84
|
+
def queue_log_fields(self, **kwargs: Any) -> None:
|
|
85
|
+
context.update(kwargs)
|
sovereign/logging/base_logger.py
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import threading
|
|
3
2
|
from abc import ABC, abstractmethod
|
|
4
3
|
from functools import cached_property
|
|
5
|
-
from typing import
|
|
4
|
+
from typing import Dict, Optional
|
|
6
5
|
|
|
7
6
|
from structlog.exceptions import DropEvent
|
|
8
7
|
from structlog.stdlib import BoundLogger
|
|
9
8
|
|
|
10
9
|
from sovereign.logging.types import EventDict, ProcessedMessage
|
|
11
10
|
|
|
12
|
-
LOG_QUEUE = threading.local()
|
|
13
|
-
|
|
14
11
|
|
|
15
12
|
class BaseLogger(ABC):
|
|
16
13
|
_user_log_fmt: Optional[str]
|
|
17
14
|
|
|
18
15
|
@property
|
|
19
16
|
@abstractmethod
|
|
20
|
-
def is_enabled(self) -> bool:
|
|
21
|
-
...
|
|
17
|
+
def is_enabled(self) -> bool: ...
|
|
22
18
|
|
|
23
19
|
@property
|
|
24
20
|
@abstractmethod
|
|
25
|
-
def _default_log_fmt(self) -> Dict[str, str]:
|
|
26
|
-
...
|
|
21
|
+
def _default_log_fmt(self) -> Dict[str, str]: ...
|
|
27
22
|
|
|
28
23
|
def is_enabled_processor(
|
|
29
24
|
self, logger: BoundLogger, method_name: str, event_dict: EventDict
|
|
@@ -44,22 +39,3 @@ class BaseLogger(ABC):
|
|
|
44
39
|
format["event"] = "{event}"
|
|
45
40
|
return format
|
|
46
41
|
return self._default_log_fmt
|
|
47
|
-
|
|
48
|
-
def merge_in_threadlocal(
|
|
49
|
-
self, logger: Any, method_name: str, event_dict: EventDict
|
|
50
|
-
) -> ProcessedMessage:
|
|
51
|
-
self._ensure_threadlocal()
|
|
52
|
-
fields: Dict[str, Any] = LOG_QUEUE.fields.copy()
|
|
53
|
-
fields.update(event_dict)
|
|
54
|
-
return fields
|
|
55
|
-
|
|
56
|
-
def clear_log_fields(self) -> None:
|
|
57
|
-
LOG_QUEUE.fields = dict()
|
|
58
|
-
|
|
59
|
-
def _ensure_threadlocal(self) -> None:
|
|
60
|
-
if not hasattr(LOG_QUEUE, "fields"):
|
|
61
|
-
LOG_QUEUE.fields = dict()
|
|
62
|
-
|
|
63
|
-
def queue_log_fields(self, **kwargs: Any) -> None:
|
|
64
|
-
self._ensure_threadlocal()
|
|
65
|
-
LOG_QUEUE.fields.update(kwargs)
|
|
@@ -29,6 +29,6 @@ class LoggerBootstrapper:
|
|
|
29
29
|
def debug_logs_processor(
|
|
30
30
|
self, logger: BoundLogger, method_name: str, event_dict: EventDict
|
|
31
31
|
) -> ProcessedMessage:
|
|
32
|
-
if method_name == "debug" and self.show_debug
|
|
32
|
+
if method_name == "debug" and self.show_debug is False:
|
|
33
33
|
raise DropEvent
|
|
34
34
|
return event_dict
|
sovereign/middlewares.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
3
|
from uuid import uuid4
|
|
4
|
+
|
|
4
5
|
from fastapi.requests import Request
|
|
5
6
|
from fastapi.responses import Response
|
|
6
7
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
from sovereign import _request_id_ctx_var, config, get_request_id, logs, stats
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class RequestContextLogMiddleware(BaseHTTPMiddleware):
|
|
@@ -36,7 +38,6 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|
|
36
38
|
source_port = addr.port
|
|
37
39
|
if xff := request.headers.get("X-Forwarded-For"):
|
|
38
40
|
source_ip = xff.split(",")[0] # leftmost address
|
|
39
|
-
logs.access_logger.clear_log_fields()
|
|
40
41
|
logs.access_logger.queue_log_fields(
|
|
41
42
|
ENVIRONMENT=config.legacy_fields.environment,
|
|
42
43
|
HOST=request.headers.get("host", "-"),
|