sovereign 0.27.0__tar.gz → 0.29.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.27.0 → sovereign-0.29.0}/PKG-INFO +3 -2
- {sovereign-0.27.0 → sovereign-0.29.0}/pyproject.toml +22 -2
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/__init__.py +2 -2
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/configuration.py +2 -2
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/context.py +1 -11
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/discovery.py +2 -0
- sovereign-0.29.0/src/sovereign/dynamic_config/__init__.py +88 -0
- sovereign-0.29.0/src/sovereign/dynamic_config/deser.py +79 -0
- sovereign-0.29.0/src/sovereign/dynamic_config/loaders.py +121 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/schemas.py +64 -9
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/sources/file.py +1 -1
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/templates/resources.html +16 -6
- sovereign-0.29.0/src/sovereign/testing/loaders.py +8 -0
- sovereign-0.29.0/src/sovereign/tracing.py +85 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/views/discovery.py +18 -15
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/views/interface.py +2 -2
- sovereign-0.27.0/src/sovereign/config_loader.py +0 -222
- sovereign-0.27.0/src/sovereign/testing/loaders.py +0 -9
- {sovereign-0.27.0 → sovereign-0.29.0}/LICENSE.txt +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/README.md +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/app.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/constants.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/error_info.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/logging/access_logger.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/logging/application_logger.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/logging/base_logger.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/logging/bootstrapper.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/logging/types.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/middlewares.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/modifiers/__init__.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/modifiers/lib.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/response_class.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/server.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/sources/__init__.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/sources/inline.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/sources/lib.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/sources/poller.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/static/sass/style.scss +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/static/style.css +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/statistics.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/templates/base.html +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/templates/err.html +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/templates/ul_filter.html +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/testing/modifiers.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/__init__.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/auth.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/crypto/__init__.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/crypto/crypto.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/dictupdate.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/eds.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/entry_point_loader.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/mock.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/resources.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/templates.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/timer.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/version_info.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/utils/weighted_clusters.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/views/__init__.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/views/admin.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.0}/src/sovereign/views/crypto.py +0 -0
- {sovereign-0.27.0 → sovereign-0.29.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.
|
|
3
|
+
Version: 0.29.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
|
|
@@ -18,6 +18,7 @@ Classifier: Natural Language :: English
|
|
|
18
18
|
Classifier: Operating System :: POSIX :: Linux
|
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
23
|
Classifier: Programming Language :: Python :: 3.8
|
|
23
24
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -37,7 +38,7 @@ Requires-Dist: cachelib (>=0.10.2,<0.11.0)
|
|
|
37
38
|
Requires-Dist: cachetools (>=5.3.2,<6.0.0)
|
|
38
39
|
Requires-Dist: cashews[redis] (>=6.3.0,<7.0.0) ; extra == "caching"
|
|
39
40
|
Requires-Dist: croniter (>=1.4.1,<2.0.0)
|
|
40
|
-
Requires-Dist: cryptography (>=
|
|
41
|
+
Requires-Dist: cryptography (>=43.0.1,<44.0.0)
|
|
41
42
|
Requires-Dist: datadog (>=0.47.0,<0.48.0) ; extra == "statsd"
|
|
42
43
|
Requires-Dist: fastapi (>=0.110.0,<0.111.0)
|
|
43
44
|
Requires-Dist: glom (>=23.3.0,<24.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "sovereign"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.29.0"
|
|
4
4
|
description = "Envoy Proxy control-plane written in Python"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
packages = [
|
|
@@ -41,6 +41,26 @@ sovereign = 'sovereign.server:main'
|
|
|
41
41
|
|
|
42
42
|
[tool.poetry.plugins."sovereign.loaders"]
|
|
43
43
|
"example" = "sovereign.testing.loaders:Multiply"
|
|
44
|
+
"file" = "sovereign.dynamic_config.loaders:File"
|
|
45
|
+
"pkgdata" = "sovereign.dynamic_config.loaders:PackageData"
|
|
46
|
+
"http" = "sovereign.dynamic_config.loaders:Web"
|
|
47
|
+
"https" = "sovereign.dynamic_config.loaders:Web"
|
|
48
|
+
"env" = "sovereign.dynamic_config.loaders:EnvironmentVariable"
|
|
49
|
+
"module" = "sovereign.dynamic_config.loaders:PythonModule"
|
|
50
|
+
"s3" = "sovereign.dynamic_config.loaders:S3Bucket"
|
|
51
|
+
"python" = "sovereign.dynamic_config.loaders:PythonInlineCode"
|
|
52
|
+
"inline" = "sovereign.dynamic_config.loaders:Inline"
|
|
53
|
+
|
|
54
|
+
[tool.poetry.plugins."sovereign.deserializers"]
|
|
55
|
+
"yaml" = "sovereign.dynamic_config.deser:YamlDeserializer"
|
|
56
|
+
"json" = "sovereign.dynamic_config.deser:JsonDeserializer"
|
|
57
|
+
"jinja" = "sovereign.dynamic_config.deser:JinjaDeserializer"
|
|
58
|
+
"jinja2" = "sovereign.dynamic_config.deser:JinjaDeserializer"
|
|
59
|
+
"string" = "sovereign.dynamic_config.deser:StringDeserializer"
|
|
60
|
+
"raw" = "sovereign.dynamic_config.deser:PassthroughDeserializer"
|
|
61
|
+
"ujson" = "sovereign.dynamic_config.deser:UjsonDeserializer"
|
|
62
|
+
"orjson" = "sovereign.dynamic_config.deser:OrjsonDeserializer"
|
|
63
|
+
|
|
44
64
|
|
|
45
65
|
[tool.poetry.dependencies]
|
|
46
66
|
python = "^3.11"
|
|
@@ -53,7 +73,7 @@ Jinja2 = "^3.1.2"
|
|
|
53
73
|
structlog = "^23.1.0"
|
|
54
74
|
cachelib = "^0.10.2"
|
|
55
75
|
glom = "^23.3.0"
|
|
56
|
-
cryptography = "^
|
|
76
|
+
cryptography = "^43.0.1"
|
|
57
77
|
fastapi = "^0.110.0"
|
|
58
78
|
uvloop = "^0.19.0"
|
|
59
79
|
sentry-sdk = "^1.23.1"
|
|
@@ -7,7 +7,7 @@ from fastapi.responses import JSONResponse
|
|
|
7
7
|
from pydantic import ValidationError
|
|
8
8
|
from starlette.templating import Jinja2Templates
|
|
9
9
|
|
|
10
|
-
from sovereign import
|
|
10
|
+
from sovereign import dynamic_config
|
|
11
11
|
from sovereign.context import TemplateContext
|
|
12
12
|
from sovereign.logging.bootstrapper import LoggerBootstrapper
|
|
13
13
|
from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
|
|
@@ -36,7 +36,7 @@ except ImportError:
|
|
|
36
36
|
def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
|
|
37
37
|
ret: Mapping[Any, Any] = dict()
|
|
38
38
|
for p in path.split(","):
|
|
39
|
-
spec =
|
|
39
|
+
spec = dynamic_config.Loadable.from_legacy_fmt(p)
|
|
40
40
|
ret = merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
|
|
41
41
|
return ret
|
|
42
42
|
|
|
@@ -3,7 +3,7 @@ from typing import Any, Mapping
|
|
|
3
3
|
|
|
4
4
|
from pydantic import ValidationError
|
|
5
5
|
|
|
6
|
-
from sovereign import
|
|
6
|
+
from sovereign import dynamic_config
|
|
7
7
|
from sovereign.context import TemplateContext
|
|
8
8
|
from sovereign.logging.bootstrapper import LoggerBootstrapper
|
|
9
9
|
from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
|
|
@@ -16,7 +16,7 @@ from sovereign.utils.dictupdate import merge # type: ignore
|
|
|
16
16
|
def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
|
|
17
17
|
ret: Mapping[Any, Any] = dict()
|
|
18
18
|
for p in path.split(","):
|
|
19
|
-
spec =
|
|
19
|
+
spec = dynamic_config.Loadable.from_legacy_fmt(p)
|
|
20
20
|
ret = merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
|
|
21
21
|
return ret
|
|
22
22
|
|
|
@@ -13,11 +13,9 @@ from typing import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
from fastapi import HTTPException
|
|
16
|
-
from sovereign import config_loader
|
|
17
|
-
from sovereign.utils.entry_point_loader import EntryPointLoader
|
|
18
16
|
from structlog.stdlib import BoundLogger
|
|
19
17
|
|
|
20
|
-
from sovereign.
|
|
18
|
+
from sovereign.dynamic_config import Loadable
|
|
21
19
|
from sovereign.schemas import DiscoveryRequest, EncryptionConfig, XdsTemplate
|
|
22
20
|
from sovereign.sources import SourcePoller
|
|
23
21
|
from sovereign.utils.crypto.crypto import CipherContainer
|
|
@@ -55,14 +53,6 @@ class TemplateContext:
|
|
|
55
53
|
self.stats = stats
|
|
56
54
|
# initial load
|
|
57
55
|
self.context: Dict[str, Any] = {}
|
|
58
|
-
entry_points = EntryPointLoader("loaders")
|
|
59
|
-
for entry_point in entry_points.groups["loaders"]:
|
|
60
|
-
custom_loader = entry_point.load()
|
|
61
|
-
try:
|
|
62
|
-
func = custom_loader.load
|
|
63
|
-
except AttributeError:
|
|
64
|
-
raise AttributeError("Custom loader does not implement .load()")
|
|
65
|
-
config_loader.loaders[entry_point.name] = func
|
|
66
56
|
asyncio.run(self.load_context_variables())
|
|
67
57
|
|
|
68
58
|
async def start_refresh_context(self) -> NoReturn:
|
|
@@ -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
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from sovereign.utils.entry_point_loader import EntryPointLoader
|
|
7
|
+
from sovereign.dynamic_config.loaders import CustomLoader
|
|
8
|
+
from sovereign.dynamic_config.deser import ConfigDeserializer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Loadable(BaseModel):
|
|
12
|
+
path: str
|
|
13
|
+
protocol: str
|
|
14
|
+
serialization: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
def load(self, default: Any = None) -> Any:
|
|
17
|
+
if self.protocol not in LOADERS:
|
|
18
|
+
raise KeyError(
|
|
19
|
+
f"Could not find CustomLoader {self.protocol}. Available: {LOADERS}"
|
|
20
|
+
)
|
|
21
|
+
loader_ = LOADERS[self.protocol]
|
|
22
|
+
|
|
23
|
+
ser = self.serialization
|
|
24
|
+
if ser is None:
|
|
25
|
+
ser = loader_.default_deser
|
|
26
|
+
if ser not in DESERIALIZERS:
|
|
27
|
+
raise KeyError(
|
|
28
|
+
f"Could not find Deserializer {ser}. Available: {DESERIALIZERS}"
|
|
29
|
+
)
|
|
30
|
+
deserializer = DESERIALIZERS[ser]
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
data = loader_.load(self.path)
|
|
34
|
+
return deserializer.deserialize(data)
|
|
35
|
+
except Exception as original_error:
|
|
36
|
+
if default is not None:
|
|
37
|
+
return default
|
|
38
|
+
raise Exception(
|
|
39
|
+
f"{self.protocol=}, {self.path=}, {self.serialization=}, {original_error=}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def from_legacy_fmt(fmt_string: str) -> "Loadable":
|
|
44
|
+
if "://" not in fmt_string:
|
|
45
|
+
return Loadable(protocol="inline", serialization="string", path=fmt_string)
|
|
46
|
+
try:
|
|
47
|
+
scheme, path = fmt_string.split("://")
|
|
48
|
+
except ValueError:
|
|
49
|
+
raise ValueError(fmt_string)
|
|
50
|
+
try:
|
|
51
|
+
proto, ser = scheme.split("+")
|
|
52
|
+
except ValueError:
|
|
53
|
+
proto, ser = scheme, "yaml"
|
|
54
|
+
|
|
55
|
+
if proto in ("python", "module"):
|
|
56
|
+
ser = "raw"
|
|
57
|
+
if proto in ("http", "https"):
|
|
58
|
+
path = "://".join([proto, path])
|
|
59
|
+
|
|
60
|
+
return Loadable(
|
|
61
|
+
protocol=proto,
|
|
62
|
+
serialization=ser,
|
|
63
|
+
path=path,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
LOADERS: Dict[str, CustomLoader] = {}
|
|
68
|
+
for entry_point in EntryPointLoader("loaders").groups["loaders"]:
|
|
69
|
+
custom_loader = entry_point.load()
|
|
70
|
+
func = custom_loader()
|
|
71
|
+
method = getattr(func, "load")
|
|
72
|
+
if not inspect.ismethod(method):
|
|
73
|
+
raise AttributeError(
|
|
74
|
+
f"CustomLoader {entry_point.name} does not implement .load()"
|
|
75
|
+
)
|
|
76
|
+
LOADERS[entry_point.name] = func
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
DESERIALIZERS: Dict[str, ConfigDeserializer] = {}
|
|
80
|
+
for entry_point in EntryPointLoader("deserializers").groups["deserializers"]:
|
|
81
|
+
deserializer = entry_point.load()
|
|
82
|
+
func = deserializer()
|
|
83
|
+
method = getattr(func, "deserialize")
|
|
84
|
+
if not inspect.ismethod(method):
|
|
85
|
+
raise AttributeError(
|
|
86
|
+
f"Deserializer {entry_point.name} does not implement .deserialize()"
|
|
87
|
+
)
|
|
88
|
+
DESERIALIZERS[entry_point.name] = func
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Protocol
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
import jinja2
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import ujson
|
|
9
|
+
|
|
10
|
+
UJSON_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
UJSON_AVAILABLE = False
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import orjson
|
|
16
|
+
|
|
17
|
+
ORJSON_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
ORJSON_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
jinja_env = jinja2.Environment(autoescape=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigDeserializer(Protocol):
|
|
25
|
+
"""
|
|
26
|
+
Deserializers can be added to sovereign by creating a subclass
|
|
27
|
+
and then specified in config:
|
|
28
|
+
|
|
29
|
+
template_context:
|
|
30
|
+
context:
|
|
31
|
+
...:
|
|
32
|
+
protocol: ...
|
|
33
|
+
serialization: <serializer name>
|
|
34
|
+
path: ...
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def deserialize(self, input: Any) -> Any:
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class YamlDeserializer(ConfigDeserializer):
|
|
42
|
+
def deserialize(self, input: Any) -> Any:
|
|
43
|
+
return yaml.safe_load(input)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class JsonDeserializer(ConfigDeserializer):
|
|
47
|
+
def deserialize(self, input: Any) -> Any:
|
|
48
|
+
return json.loads(input)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class JinjaDeserializer(ConfigDeserializer):
|
|
52
|
+
def deserialize(self, input: Any) -> Any:
|
|
53
|
+
return jinja_env.from_string(input)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StringDeserializer(ConfigDeserializer):
|
|
57
|
+
def deserialize(self, input: Any) -> Any:
|
|
58
|
+
return str(input)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PassthroughDeserializer(ConfigDeserializer):
|
|
62
|
+
def deserialize(self, input: Any) -> Any:
|
|
63
|
+
return input
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class UjsonDeserializer(ConfigDeserializer):
|
|
67
|
+
def deserialize(self, input: Any) -> Any:
|
|
68
|
+
if not UJSON_AVAILABLE:
|
|
69
|
+
raise ImportError("Configured a UJSON deserializer but it's not installed")
|
|
70
|
+
return ujson.loads(input)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class OrjsonDeserializer(ConfigDeserializer):
|
|
74
|
+
def deserialize(self, input: Any) -> Any:
|
|
75
|
+
if not ORJSON_AVAILABLE:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"Configured an ORJSON deserializer but it's not installed"
|
|
78
|
+
)
|
|
79
|
+
return orjson.loads(input)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import importlib
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from importlib.machinery import SourceFileLoader
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from sovereign.utils.resources import get_package_file_bytes
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import boto3
|
|
13
|
+
|
|
14
|
+
BOTO_IS_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
BOTO_IS_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CustomLoader(Protocol):
|
|
20
|
+
"""
|
|
21
|
+
Custom loaders can be added to sovereign by creating a subclass
|
|
22
|
+
and then in config:
|
|
23
|
+
|
|
24
|
+
template_context:
|
|
25
|
+
context:
|
|
26
|
+
...:
|
|
27
|
+
protocol: <loader name>
|
|
28
|
+
serialization: ...
|
|
29
|
+
path: <path argument>
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
default_deser: str = "yaml"
|
|
33
|
+
|
|
34
|
+
def load(self, path: str) -> Any:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class File(CustomLoader):
|
|
39
|
+
default_deser = "passthrough"
|
|
40
|
+
|
|
41
|
+
def load(self, path: str) -> Any:
|
|
42
|
+
with open(path) as f:
|
|
43
|
+
contents = f.read()
|
|
44
|
+
try:
|
|
45
|
+
return contents
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
raise FileNotFoundError(f"Unable to load {path}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PackageData(CustomLoader):
|
|
51
|
+
default_deser = "string"
|
|
52
|
+
|
|
53
|
+
def load(self, path: str) -> Any:
|
|
54
|
+
pkg, pkg_file = path.split(":")
|
|
55
|
+
data = get_package_file_bytes(pkg, pkg_file)
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Web(CustomLoader):
|
|
60
|
+
default_deser = "json"
|
|
61
|
+
|
|
62
|
+
def load(self, path: str) -> Any:
|
|
63
|
+
response = requests.get(path)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
data = response.text
|
|
66
|
+
return data
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EnvironmentVariable(CustomLoader):
|
|
70
|
+
default_deser = "raw"
|
|
71
|
+
|
|
72
|
+
def load(self, path: str) -> Any:
|
|
73
|
+
data = os.getenv(path)
|
|
74
|
+
if data is None:
|
|
75
|
+
raise AttributeError(f"Unable to read environment variable {path}")
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PythonModule(CustomLoader):
|
|
80
|
+
default_deser = "passthrough"
|
|
81
|
+
|
|
82
|
+
def load(self, path: str) -> Any:
|
|
83
|
+
if ":" in path:
|
|
84
|
+
mod, fn = path.rsplit(":", maxsplit=1)
|
|
85
|
+
else:
|
|
86
|
+
mod, fn = path, ""
|
|
87
|
+
imported = importlib.import_module(mod)
|
|
88
|
+
if fn != "":
|
|
89
|
+
return getattr(imported, fn)
|
|
90
|
+
return imported
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class S3Bucket(CustomLoader):
|
|
94
|
+
default_deser = "raw"
|
|
95
|
+
|
|
96
|
+
def load(self, path: str) -> Any:
|
|
97
|
+
if not BOTO_IS_AVAILABLE:
|
|
98
|
+
raise ImportError(
|
|
99
|
+
"boto3 must be installed to load S3 paths. Use ``pip install sovereign[boto]``"
|
|
100
|
+
)
|
|
101
|
+
bucket, key = path.split("/", maxsplit=1)
|
|
102
|
+
s3 = boto3.client("s3")
|
|
103
|
+
response = s3.get_object(Bucket=bucket, Key=key)
|
|
104
|
+
data = "".join([chunk.decode() for chunk in response["Body"]])
|
|
105
|
+
return data
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PythonInlineCode(CustomLoader):
|
|
109
|
+
default_deser = "passthrough"
|
|
110
|
+
|
|
111
|
+
def load(self, path: str) -> Any:
|
|
112
|
+
p = str(Path(path).absolute())
|
|
113
|
+
loader = SourceFileLoader(p, path=p)
|
|
114
|
+
return loader.load_module(p)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Inline(CustomLoader):
|
|
118
|
+
default_deser = "string"
|
|
119
|
+
|
|
120
|
+
def load(self, path: str) -> Any:
|
|
121
|
+
return path
|
|
@@ -20,7 +20,8 @@ from pydantic import (
|
|
|
20
20
|
)
|
|
21
21
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
22
22
|
|
|
23
|
-
from sovereign.
|
|
23
|
+
from sovereign.dynamic_config import Loadable
|
|
24
|
+
from sovereign.dynamic_config.deser import jinja_env
|
|
24
25
|
from sovereign.utils.crypto.suites import EncryptionType
|
|
25
26
|
from sovereign.utils.version_info import compute_hash
|
|
26
27
|
|
|
@@ -111,6 +112,7 @@ class DiscoveryCacheConfig(BaseModel):
|
|
|
111
112
|
suppress: bool = False # False = Don't suppress connection errors. True = suppress connection errors
|
|
112
113
|
socket_keepalive: bool = True # Try to keep connections to redis around.
|
|
113
114
|
ttl: int = 60
|
|
115
|
+
extra_keys: Dict[str, Any] = {}
|
|
114
116
|
|
|
115
117
|
@model_validator(mode="after")
|
|
116
118
|
def set_default_protocol(self) -> Self:
|
|
@@ -164,15 +166,15 @@ class XdsTemplate:
|
|
|
164
166
|
return self.code.render(*args, **kwargs)
|
|
165
167
|
|
|
166
168
|
def load_source(self) -> str:
|
|
167
|
-
|
|
169
|
+
old_serialization = self.loadable.serialization
|
|
170
|
+
if self.loadable.serialization in ("jinja", "jinja2"):
|
|
168
171
|
# The Jinja2 template serializer does not properly set a name
|
|
169
172
|
# for the loaded template.
|
|
170
173
|
# The repr for the template prints out as the memory address
|
|
171
174
|
# This makes it really hard to generate a consistent version_info string
|
|
172
175
|
# in rendered configuration.
|
|
173
176
|
# For this reason, we re-load the template as a string instead, and create a checksum.
|
|
174
|
-
|
|
175
|
-
self.loadable.serialization = Serialization("string")
|
|
177
|
+
self.loadable.serialization = "string"
|
|
176
178
|
ret = self.loadable.load()
|
|
177
179
|
self.loadable.serialization = old_serialization
|
|
178
180
|
return str(ret)
|
|
@@ -180,9 +182,8 @@ class XdsTemplate:
|
|
|
180
182
|
# If the template specified is a python source file,
|
|
181
183
|
# we can simply read and return the source of it.
|
|
182
184
|
old_protocol = self.loadable.protocol
|
|
183
|
-
old_serialization = self.loadable.serialization
|
|
184
185
|
self.loadable.protocol = "inline"
|
|
185
|
-
self.loadable.serialization =
|
|
186
|
+
self.loadable.serialization = "string"
|
|
186
187
|
ret = self.loadable.load()
|
|
187
188
|
self.loadable.protocol = old_protocol
|
|
188
189
|
self.loadable.serialization = old_serialization
|
|
@@ -294,7 +295,9 @@ class Resources(List[str]):
|
|
|
294
295
|
"""
|
|
295
296
|
|
|
296
297
|
def __contains__(self, item: object) -> bool:
|
|
297
|
-
if
|
|
298
|
+
if (
|
|
299
|
+
len(self) == 0
|
|
300
|
+
): # TODO: refactor to remove overriding __contains__; its being used in multiple places
|
|
298
301
|
return True
|
|
299
302
|
return super().__contains__(item)
|
|
300
303
|
|
|
@@ -305,13 +308,17 @@ class Status(BaseModel):
|
|
|
305
308
|
details: List[Any]
|
|
306
309
|
|
|
307
310
|
|
|
311
|
+
def resources_factory() -> Resources:
|
|
312
|
+
return Resources()
|
|
313
|
+
|
|
314
|
+
|
|
308
315
|
class DiscoveryRequest(BaseModel):
|
|
309
316
|
node: Node = Field(..., title="Node information about the envoy proxy")
|
|
310
317
|
version_info: str = Field(
|
|
311
318
|
"0", title="The version of the envoy clients current configuration"
|
|
312
319
|
)
|
|
313
|
-
resource_names:
|
|
314
|
-
|
|
320
|
+
resource_names: Resources = Field(
|
|
321
|
+
default_factory=resources_factory, title="List of requested resource names"
|
|
315
322
|
)
|
|
316
323
|
hide_private_keys: bool = False
|
|
317
324
|
type_url: Optional[str] = Field(
|
|
@@ -353,6 +360,11 @@ class DiscoveryRequest(BaseModel):
|
|
|
353
360
|
self.desired_controlplane,
|
|
354
361
|
)
|
|
355
362
|
|
|
363
|
+
@field_validator("resource_names", mode="before")
|
|
364
|
+
@classmethod
|
|
365
|
+
def validate_resources(cls, v: Union[Resources, List[str]]) -> Resources:
|
|
366
|
+
return Resources(v)
|
|
367
|
+
|
|
356
368
|
|
|
357
369
|
class DiscoveryResponse(BaseModel):
|
|
358
370
|
version_info: str = Field(
|
|
@@ -447,6 +459,7 @@ class SovereignConfig(BaseSettings):
|
|
|
447
459
|
log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
|
|
448
460
|
ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
|
|
449
461
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
462
|
+
tracing: Optional["TracingConfig"] = None
|
|
450
463
|
model_config = SettingsConfigDict(
|
|
451
464
|
env_file=".env",
|
|
452
465
|
extra="ignore",
|
|
@@ -633,6 +646,46 @@ class SourcesConfiguration(BaseSettings):
|
|
|
633
646
|
)
|
|
634
647
|
|
|
635
648
|
|
|
649
|
+
class TracingConfig(BaseSettings):
|
|
650
|
+
enabled: bool = Field(False)
|
|
651
|
+
collector: str = Field("notset")
|
|
652
|
+
endpoint: str = Field("/v2/api/spans")
|
|
653
|
+
trace_id_128bit: bool = Field(True)
|
|
654
|
+
tags: Dict[str, Union[Loadable, str]] = dict()
|
|
655
|
+
model_config = SettingsConfigDict(
|
|
656
|
+
env_file=".env",
|
|
657
|
+
extra="ignore",
|
|
658
|
+
env_file_encoding="utf-8",
|
|
659
|
+
populate_by_name=True,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
@field_validator("tags", mode="before")
|
|
663
|
+
@classmethod
|
|
664
|
+
def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
|
|
665
|
+
ret = dict()
|
|
666
|
+
for key, value in v.items():
|
|
667
|
+
if isinstance(value, dict):
|
|
668
|
+
ret[key] = Loadable(**value).load()
|
|
669
|
+
elif isinstance(value, str):
|
|
670
|
+
ret[key] = Loadable.from_legacy_fmt(value).load()
|
|
671
|
+
else:
|
|
672
|
+
raise ValueError(f"Received an invalid tag for tracing: {value}")
|
|
673
|
+
return ret
|
|
674
|
+
|
|
675
|
+
@model_validator(mode="before")
|
|
676
|
+
@classmethod
|
|
677
|
+
def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
678
|
+
if enabled := getenv("SOVEREIGN_TRACING_ENABLED"):
|
|
679
|
+
values["enabled"] = enabled
|
|
680
|
+
if collector := getenv("SOVEREIGN_TRACING_COLLECTOR"):
|
|
681
|
+
values["collector"] = collector
|
|
682
|
+
if endpoint := getenv("SOVEREIGN_TRACING_ENDPOINT"):
|
|
683
|
+
values["endpoint"] = endpoint
|
|
684
|
+
if trace_id_128bit := getenv("SOVEREIGN_TRACING_TRACE_ID_128BIT"):
|
|
685
|
+
values["trace_id_128bit"] = trace_id_128bit
|
|
686
|
+
return values
|
|
687
|
+
|
|
688
|
+
|
|
636
689
|
class LegacyConfig(BaseSettings):
|
|
637
690
|
regions: Optional[List[str]] = None
|
|
638
691
|
eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
|
|
@@ -720,6 +773,7 @@ class SovereignConfigv2(BaseSettings):
|
|
|
720
773
|
debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
|
|
721
774
|
legacy_fields: LegacyConfig = LegacyConfig()
|
|
722
775
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
776
|
+
tracing: TracingConfig = TracingConfig()
|
|
723
777
|
model_config = SettingsConfigDict(
|
|
724
778
|
env_file=".env",
|
|
725
779
|
extra="ignore",
|
|
@@ -810,6 +864,7 @@ class SovereignConfigv2(BaseSettings):
|
|
|
810
864
|
statsd=other.statsd,
|
|
811
865
|
sentry_dsn=SecretStr(other.sentry_dsn),
|
|
812
866
|
debug=other.debug_enabled,
|
|
867
|
+
tracing=other.tracing,
|
|
813
868
|
legacy_fields=LegacyConfig(
|
|
814
869
|
regions=other.regions,
|
|
815
870
|
eds_priority_matrix=other.eds_priority_matrix,
|
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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">
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import uuid
|
|
3
|
+
import requests
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from contextlib import nullcontext
|
|
6
|
+
|
|
7
|
+
from sovereign import config
|
|
8
|
+
|
|
9
|
+
_trace_id_ctx_var: ContextVar[str] = ContextVar("trace_id", default="")
|
|
10
|
+
_span_id_ctx_var: ContextVar[str] = ContextVar("span_id", default="")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_trace_id() -> str:
|
|
14
|
+
return _trace_id_ctx_var.get()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_span_id() -> str:
|
|
18
|
+
return _span_id_ctx_var.get()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_128bit():
|
|
22
|
+
return str(uuid.uuid4()).replace("-", "")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_64bit():
|
|
26
|
+
return generate_128bit()[:32]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def timestamp():
|
|
30
|
+
return str(time.time()).replace(".", "")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TRACING = config.tracing
|
|
34
|
+
TRACING_DISABLED = not TRACING.enabled
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Tracer:
|
|
38
|
+
def gen_id(self):
|
|
39
|
+
if TRACING.trace_id_128bit:
|
|
40
|
+
trace_id = generate_128bit()
|
|
41
|
+
else:
|
|
42
|
+
trace_id = generate_64bit()
|
|
43
|
+
_trace_id_ctx_var.set(trace_id)
|
|
44
|
+
return trace_id
|
|
45
|
+
|
|
46
|
+
def __init__(self, span_name):
|
|
47
|
+
if TRACING_DISABLED:
|
|
48
|
+
return
|
|
49
|
+
span_id = get_span_id()
|
|
50
|
+
self.parent_span_id = None
|
|
51
|
+
if span_id != "":
|
|
52
|
+
# We are already inside a trace context
|
|
53
|
+
self.parent_span_id = span_id
|
|
54
|
+
self.trace_id = get_trace_id()
|
|
55
|
+
self.span_id = self.gen_id()
|
|
56
|
+
self.span_name = span_name
|
|
57
|
+
|
|
58
|
+
def __enter__(self):
|
|
59
|
+
if TRACING_DISABLED:
|
|
60
|
+
return nullcontext()
|
|
61
|
+
self.trace = {
|
|
62
|
+
"traceId": self.trace_id,
|
|
63
|
+
"id": self.span_id,
|
|
64
|
+
"name": self.span_name,
|
|
65
|
+
"timestamp": time.time(),
|
|
66
|
+
"tags": TRACING.tags,
|
|
67
|
+
}
|
|
68
|
+
if self.parent_span_id:
|
|
69
|
+
self.trace["parent_span_id"] = self.parent_span_id
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
73
|
+
if TRACING_DISABLED:
|
|
74
|
+
return
|
|
75
|
+
self.trace["duration"] = time.time() - self.trace["timestamp"]
|
|
76
|
+
self.submit()
|
|
77
|
+
|
|
78
|
+
def submit(self):
|
|
79
|
+
print(f"{self.span_name}: {self.trace['duration']}")
|
|
80
|
+
try:
|
|
81
|
+
url = f"{TRACING.collector}{TRACING.endpoint}"
|
|
82
|
+
requests.post(url, json=self.trace)
|
|
83
|
+
# pylint: disable=broad-except
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Failed to submit trace: {self.trace}, Error:{e}")
|
|
@@ -117,21 +117,24 @@ async def perform_discovery(
|
|
|
117
117
|
authenticate(req)
|
|
118
118
|
if discovery_cache.enabled:
|
|
119
119
|
logs.access_logger.queue_log_fields(CACHE_XDS_HIT=False)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
120
|
+
metadata_keys = discovery_cache.extra_keys.get("metadata", [])
|
|
121
|
+
extra_metadata = [req.node.metadata.get(key, None) for key in metadata_keys]
|
|
122
|
+
hash_keys = [
|
|
123
|
+
api_version,
|
|
124
|
+
resource_type,
|
|
125
|
+
req.envoy_version,
|
|
126
|
+
req.resource_names,
|
|
127
|
+
req.desired_controlplane,
|
|
128
|
+
req.hide_private_keys,
|
|
129
|
+
req.type_url,
|
|
130
|
+
req.node.cluster,
|
|
131
|
+
req.node.locality,
|
|
132
|
+
req.node.metadata.get("auth", None),
|
|
133
|
+
req.node.metadata.get("num_cpus", None),
|
|
134
|
+
] + extra_metadata
|
|
135
|
+
|
|
136
|
+
cache_key = compute_hash(hash_keys)
|
|
137
|
+
|
|
135
138
|
if template := await cache.get(key=cache_key, default=None):
|
|
136
139
|
logs.access_logger.queue_log_fields(CACHE_XDS_HIT=True)
|
|
137
140
|
return template # type: ignore[no-any-return]
|
|
@@ -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(
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
from enum import Enum
|
|
4
|
-
from typing import Any, Dict, Callable, Union, Protocol
|
|
5
|
-
from types import ModuleType
|
|
6
|
-
import yaml
|
|
7
|
-
import jinja2
|
|
8
|
-
import requests
|
|
9
|
-
import importlib
|
|
10
|
-
from importlib.machinery import SourceFileLoader
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from pydantic import BaseModel
|
|
13
|
-
from sovereign.utils.resources import get_package_file_bytes
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Serialization(Enum):
|
|
17
|
-
"""
|
|
18
|
-
Types of deserialization available in Sovereign
|
|
19
|
-
for loading configuration field values.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
yaml = "yaml"
|
|
23
|
-
json = "json"
|
|
24
|
-
orjson = "orjson"
|
|
25
|
-
ujson = "ujson"
|
|
26
|
-
jinja = "jinja"
|
|
27
|
-
jinja2 = "jinja2"
|
|
28
|
-
string = "string"
|
|
29
|
-
raw = "raw"
|
|
30
|
-
skip = "skip"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
jinja_env = jinja2.Environment(autoescape=True)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def passthrough(item: Any) -> Any:
|
|
37
|
-
return item
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def string(item: Any) -> Any:
|
|
41
|
-
return str(item)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
serializers: Dict[Serialization, Callable[[Any], Any]] = {
|
|
45
|
-
Serialization.yaml: yaml.safe_load,
|
|
46
|
-
Serialization.json: json.loads,
|
|
47
|
-
Serialization.jinja: jinja_env.from_string,
|
|
48
|
-
Serialization.jinja2: jinja_env.from_string,
|
|
49
|
-
Serialization.string: string,
|
|
50
|
-
Serialization.raw: passthrough,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
import ujson
|
|
56
|
-
|
|
57
|
-
serializers[Serialization.ujson] = ujson.loads
|
|
58
|
-
jinja_env.policies["json.dumps_function"] = ujson.dumps
|
|
59
|
-
except ImportError:
|
|
60
|
-
# This lambda will raise an exception when the serializer is used; otherwise we should not crash
|
|
61
|
-
serializers[Serialization.ujson] = lambda *a, **kw: raise_(
|
|
62
|
-
ImportError("ujson must be installed to use in config_loaders")
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
import orjson
|
|
67
|
-
|
|
68
|
-
serializers[Serialization.orjson] = orjson.loads
|
|
69
|
-
|
|
70
|
-
# orjson.dumps returns bytes, so we have to wrap & decode it
|
|
71
|
-
def orjson_dumps(*args: Any, **kwargs: Any) -> Any:
|
|
72
|
-
try:
|
|
73
|
-
representation = orjson.dumps(*args, **kwargs)
|
|
74
|
-
except TypeError:
|
|
75
|
-
raise TypeError(f"Unable to dump objects using ORJSON: {args}, {kwargs}")
|
|
76
|
-
try:
|
|
77
|
-
return representation.decode()
|
|
78
|
-
except Exception as e:
|
|
79
|
-
raise e.__class__(
|
|
80
|
-
f"Unable to decode ORJSON: {representation!r}. Original exception: {e}"
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
jinja_env.policies["json.dumps_function"] = orjson_dumps
|
|
84
|
-
jinja_env.policies["json.dumps_kwargs"] = {"option": orjson.OPT_SORT_KEYS}
|
|
85
|
-
except ImportError:
|
|
86
|
-
# This lambda will raise an exception when the serializer is used; otherwise we should not crash
|
|
87
|
-
serializers[Serialization.orjson] = lambda *a, **kw: raise_(
|
|
88
|
-
ImportError("orjson must be installed to use in config_loaders")
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
import boto3
|
|
93
|
-
except ImportError:
|
|
94
|
-
boto3 = None
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class CustomLoader(Protocol):
|
|
98
|
-
def load(self, path: str, ser: Serialization) -> Any:
|
|
99
|
-
...
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class Loadable(BaseModel):
|
|
103
|
-
protocol: str = "http"
|
|
104
|
-
serialization: Serialization = Serialization.yaml
|
|
105
|
-
path: str
|
|
106
|
-
|
|
107
|
-
def load(self, default: Any = None) -> Any:
|
|
108
|
-
try:
|
|
109
|
-
return loaders[self.protocol](self.path, self.serialization)
|
|
110
|
-
except Exception:
|
|
111
|
-
if default is not None:
|
|
112
|
-
return default
|
|
113
|
-
raise
|
|
114
|
-
|
|
115
|
-
@staticmethod
|
|
116
|
-
def from_legacy_fmt(fmt_string: str) -> "Loadable":
|
|
117
|
-
if "://" not in fmt_string:
|
|
118
|
-
return Loadable(
|
|
119
|
-
protocol="inline", serialization=Serialization.string, path=fmt_string
|
|
120
|
-
)
|
|
121
|
-
try:
|
|
122
|
-
scheme, path = fmt_string.split("://")
|
|
123
|
-
except ValueError:
|
|
124
|
-
raise ValueError(fmt_string)
|
|
125
|
-
try:
|
|
126
|
-
proto, ser = scheme.split("+")
|
|
127
|
-
except ValueError:
|
|
128
|
-
proto, ser = scheme, "yaml"
|
|
129
|
-
|
|
130
|
-
serialization: Serialization = Serialization(ser)
|
|
131
|
-
if proto in ("python", "module"):
|
|
132
|
-
serialization = Serialization.raw
|
|
133
|
-
if proto in ("http", "https"):
|
|
134
|
-
path = "://".join([proto, path])
|
|
135
|
-
|
|
136
|
-
return Loadable(
|
|
137
|
-
protocol=proto,
|
|
138
|
-
serialization=serialization,
|
|
139
|
-
path=path,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def raise_(e: Exception) -> Exception:
|
|
144
|
-
raise e
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def load_file(path: str, ser: Serialization) -> Any:
|
|
148
|
-
with open(path) as f:
|
|
149
|
-
contents = f.read()
|
|
150
|
-
try:
|
|
151
|
-
return serializers[ser](contents)
|
|
152
|
-
except FileNotFoundError:
|
|
153
|
-
raise FileNotFoundError(f"Unable to load {path}")
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def load_package_data(path: str, ser: Serialization) -> Any:
|
|
157
|
-
pkg, pkg_file = path.split(":")
|
|
158
|
-
data = get_package_file_bytes(pkg, pkg_file)
|
|
159
|
-
return serializers[ser](data)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def load_http(path: str, ser: Serialization) -> Any:
|
|
163
|
-
response = requests.get(path)
|
|
164
|
-
response.raise_for_status()
|
|
165
|
-
data = response.text
|
|
166
|
-
return serializers[ser](data)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def load_env(variable: str, ser: Serialization = Serialization.raw) -> Any:
|
|
170
|
-
data = os.getenv(variable)
|
|
171
|
-
try:
|
|
172
|
-
return serializers[ser](data)
|
|
173
|
-
except AttributeError as e:
|
|
174
|
-
raise AttributeError(
|
|
175
|
-
f"Unable to read environment variable {variable}: {repr(e)}"
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def load_module(name: str, _: Serialization = Serialization.raw) -> Any:
|
|
180
|
-
if ":" in name:
|
|
181
|
-
mod, fn = name.rsplit(":", maxsplit=1)
|
|
182
|
-
else:
|
|
183
|
-
mod, fn = name, ""
|
|
184
|
-
imported = importlib.import_module(mod)
|
|
185
|
-
if fn != "":
|
|
186
|
-
return getattr(imported, fn)
|
|
187
|
-
return imported
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def load_s3(path: str, ser: Serialization = Serialization.raw) -> Any:
|
|
191
|
-
if isinstance(boto3, type(None)):
|
|
192
|
-
raise ImportError(
|
|
193
|
-
"boto3 must be installed to load S3 paths. Use ``pip install sovereign[boto]``"
|
|
194
|
-
)
|
|
195
|
-
bucket, key = path.split("/", maxsplit=1)
|
|
196
|
-
s3 = boto3.client("s3")
|
|
197
|
-
response = s3.get_object(Bucket=bucket, Key=key)
|
|
198
|
-
data = "".join([chunk.decode() for chunk in response["Body"]])
|
|
199
|
-
return serializers[ser](data)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def load_python(path: str, _: Serialization = Serialization.raw) -> ModuleType:
|
|
203
|
-
p = str(Path(path).absolute())
|
|
204
|
-
loader = SourceFileLoader(p, path=p)
|
|
205
|
-
return loader.load_module(p)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def load_inline(path: str, _: Serialization = Serialization.raw) -> Any:
|
|
209
|
-
return str(path)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
loaders: Dict[str, Callable[[str, Serialization], Union[str, Any]]] = {
|
|
213
|
-
"file": load_file,
|
|
214
|
-
"pkgdata": load_package_data,
|
|
215
|
-
"http": load_http,
|
|
216
|
-
"https": load_http,
|
|
217
|
-
"env": load_env,
|
|
218
|
-
"module": load_module,
|
|
219
|
-
"s3": load_s3,
|
|
220
|
-
"python": load_python,
|
|
221
|
-
"inline": load_inline,
|
|
222
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|