sovereign 0.25.4__tar.gz → 0.27.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.25.4 → sovereign-0.27.0}/PKG-INFO +4 -2
- {sovereign-0.25.4 → sovereign-0.27.0}/pyproject.toml +14 -3
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/__init__.py +1 -1
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/app.py +4 -2
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/config_loader.py +45 -46
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/configuration.py +1 -1
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/context.py +32 -26
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/schemas.py +187 -190
- sovereign-0.27.0/src/sovereign/testing/loaders.py +9 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/crypto.py +2 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/admin.py +2 -2
- {sovereign-0.25.4 → sovereign-0.27.0}/LICENSE.txt +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/README.md +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/constants.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/discovery.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/error_info.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/access_logger.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/application_logger.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/base_logger.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/bootstrapper.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/types.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/middlewares.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/modifiers/__init__.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/modifiers/lib.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/response_class.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/server.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/__init__.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/file.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/inline.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/lib.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/poller.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/static/sass/style.scss +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/static/style.css +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/statistics.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/base.html +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/err.html +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/resources.html +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/ul_filter.html +0 -0
- /sovereign-0.25.4/src/sovereign/modifiers/test.py → /sovereign-0.27.0/src/sovereign/testing/modifiers.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/__init__.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/auth.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/__init__.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/dictupdate.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/eds.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/entry_point_loader.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/mock.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/resources.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/templates.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/timer.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/version_info.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/weighted_clusters.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/__init__.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/crypto.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/discovery.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/healthchecks.py +0 -0
- {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sovereign
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.0
|
|
4
4
|
Summary: Envoy Proxy control-plane written in Python
|
|
5
5
|
Home-page: https://pypi.org/project/sovereign/
|
|
6
6
|
License: Apache-2.0
|
|
@@ -44,13 +44,15 @@ Requires-Dist: glom (>=23.3.0,<24.0.0)
|
|
|
44
44
|
Requires-Dist: gunicorn (>=22.0.0,<23.0.0)
|
|
45
45
|
Requires-Dist: httptools (>=0.6.0,<0.7.0) ; extra == "httptools"
|
|
46
46
|
Requires-Dist: orjson (>=3.9.15,<4.0.0) ; extra == "orjson"
|
|
47
|
+
Requires-Dist: pydantic (>=2.7.2,<3.0.0)
|
|
48
|
+
Requires-Dist: pydantic-settings (>=2.3.1,<3.0.0)
|
|
47
49
|
Requires-Dist: redis (<=5.0.0)
|
|
48
50
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
49
51
|
Requires-Dist: sentry-sdk (>=1.23.1,<2.0.0) ; extra == "sentry"
|
|
50
52
|
Requires-Dist: structlog (>=23.1.0,<24.0.0)
|
|
51
53
|
Requires-Dist: ujson (>=5.8.0,<6.0.0) ; extra == "ujson"
|
|
52
54
|
Requires-Dist: uvicorn (>=0.23.2,<0.24.0)
|
|
53
|
-
Requires-Dist: uvloop (>=0.
|
|
55
|
+
Requires-Dist: uvloop (>=0.19.0,<0.20.0)
|
|
54
56
|
Project-URL: Documentation, https://vsyrakis.bitbucket.io/sovereign/docs/
|
|
55
57
|
Project-URL: Repository, https://bitbucket.org/atlassian/sovereign/src/master/
|
|
56
58
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "sovereign"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.27.0"
|
|
4
4
|
description = "Envoy Proxy control-plane written in Python"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
packages = [
|
|
@@ -37,7 +37,10 @@ sovereign = 'sovereign.server:main'
|
|
|
37
37
|
"inline" = "sovereign.sources.inline:Inline"
|
|
38
38
|
|
|
39
39
|
[tool.poetry.plugins."sovereign.modifiers"]
|
|
40
|
-
"sovereign_3rd_party_test" = "sovereign.modifiers
|
|
40
|
+
"sovereign_3rd_party_test" = "sovereign.testing.modifiers:Test"
|
|
41
|
+
|
|
42
|
+
[tool.poetry.plugins."sovereign.loaders"]
|
|
43
|
+
"example" = "sovereign.testing.loaders:Multiply"
|
|
41
44
|
|
|
42
45
|
[tool.poetry.dependencies]
|
|
43
46
|
python = "^3.11"
|
|
@@ -52,7 +55,7 @@ cachelib = "^0.10.2"
|
|
|
52
55
|
glom = "^23.3.0"
|
|
53
56
|
cryptography = "^42.0.5"
|
|
54
57
|
fastapi = "^0.110.0"
|
|
55
|
-
uvloop = "^0.
|
|
58
|
+
uvloop = "^0.19.0"
|
|
56
59
|
sentry-sdk = "^1.23.1"
|
|
57
60
|
boto3 = {version = "^1.28.62", optional = true}
|
|
58
61
|
datadog = {version = "^0.47.0", optional = true}
|
|
@@ -63,6 +66,8 @@ cashews = {extras = ["redis"], version = "^6.3.0", optional = true}
|
|
|
63
66
|
redis = {version = "<= 5.0.0", optional = true}
|
|
64
67
|
httptools = {version = "^0.6.0", optional = true}
|
|
65
68
|
cachetools = "^5.3.2"
|
|
69
|
+
pydantic = "^2.7.2"
|
|
70
|
+
pydantic-settings = "^2.3.1"
|
|
66
71
|
|
|
67
72
|
[tool.poetry.extras]
|
|
68
73
|
sentry = ["sentry-sdk"]
|
|
@@ -111,6 +116,12 @@ lint = { cmd = "pylint src/sovereign", help = "Run linter checks" }
|
|
|
111
116
|
[tool.black]
|
|
112
117
|
target-version = ['py311']
|
|
113
118
|
|
|
119
|
+
[tool.mypy]
|
|
120
|
+
plugins = [
|
|
121
|
+
"pydantic.mypy"
|
|
122
|
+
]
|
|
123
|
+
ignore_missing_imports = true
|
|
124
|
+
|
|
114
125
|
[tool.coverage.run]
|
|
115
126
|
omit = ["test/*"]
|
|
116
127
|
|
|
@@ -4,7 +4,7 @@ from importlib.metadata import version
|
|
|
4
4
|
from typing import Any, Mapping, Type
|
|
5
5
|
|
|
6
6
|
from fastapi.responses import JSONResponse
|
|
7
|
-
from pydantic
|
|
7
|
+
from pydantic import ValidationError
|
|
8
8
|
from starlette.templating import Jinja2Templates
|
|
9
9
|
|
|
10
10
|
from sovereign import config_loader
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import traceback
|
|
3
|
-
import uvicorn
|
|
4
3
|
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
5
6
|
from fastapi import FastAPI, Request
|
|
6
7
|
from fastapi.responses import RedirectResponse, FileResponse, Response, JSONResponse
|
|
8
|
+
|
|
7
9
|
from sovereign import (
|
|
8
10
|
__version__,
|
|
9
11
|
config,
|
|
@@ -14,12 +16,12 @@ from sovereign import (
|
|
|
14
16
|
logs,
|
|
15
17
|
)
|
|
16
18
|
from sovereign.error_info import ErrorInfo
|
|
19
|
+
from sovereign.utils.resources import get_package_file
|
|
17
20
|
from sovereign.views import crypto, discovery, healthchecks, admin, interface
|
|
18
21
|
from sovereign.middlewares import (
|
|
19
22
|
RequestContextLogMiddleware,
|
|
20
23
|
LoggingMiddleware,
|
|
21
24
|
)
|
|
22
|
-
from sovereign.utils.resources import get_package_file
|
|
23
25
|
|
|
24
26
|
Router = namedtuple("Router", "module tags prefix")
|
|
25
27
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Any, Dict, Callable, Union
|
|
4
|
+
from typing import Any, Dict, Callable, Union, Protocol
|
|
5
5
|
from types import ModuleType
|
|
6
6
|
import yaml
|
|
7
7
|
import jinja2
|
|
@@ -14,6 +14,11 @@ from sovereign.utils.resources import get_package_file_bytes
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Serialization(Enum):
|
|
17
|
+
"""
|
|
18
|
+
Types of deserialization available in Sovereign
|
|
19
|
+
for loading configuration field values.
|
|
20
|
+
"""
|
|
21
|
+
|
|
17
22
|
yaml = "yaml"
|
|
18
23
|
json = "json"
|
|
19
24
|
orjson = "orjson"
|
|
@@ -22,18 +27,7 @@ class Serialization(Enum):
|
|
|
22
27
|
jinja2 = "jinja2"
|
|
23
28
|
string = "string"
|
|
24
29
|
raw = "raw"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class Protocol(Enum):
|
|
28
|
-
file = "file"
|
|
29
|
-
http = "http"
|
|
30
|
-
https = "https"
|
|
31
|
-
pkgdata = "pkgdata"
|
|
32
|
-
env = "env"
|
|
33
|
-
module = "module"
|
|
34
|
-
s3 = "s3"
|
|
35
|
-
python = "python"
|
|
36
|
-
inline = "inline"
|
|
30
|
+
skip = "skip"
|
|
37
31
|
|
|
38
32
|
|
|
39
33
|
jinja_env = jinja2.Environment(autoescape=True)
|
|
@@ -56,6 +50,7 @@ serializers: Dict[Serialization, Callable[[Any], Any]] = {
|
|
|
56
50
|
Serialization.raw: passthrough,
|
|
57
51
|
}
|
|
58
52
|
|
|
53
|
+
|
|
59
54
|
try:
|
|
60
55
|
import ujson
|
|
61
56
|
|
|
@@ -99,8 +94,13 @@ except ImportError:
|
|
|
99
94
|
boto3 = None
|
|
100
95
|
|
|
101
96
|
|
|
97
|
+
class CustomLoader(Protocol):
|
|
98
|
+
def load(self, path: str, ser: Serialization) -> Any:
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
|
|
102
102
|
class Loadable(BaseModel):
|
|
103
|
-
protocol:
|
|
103
|
+
protocol: str = "http"
|
|
104
104
|
serialization: Serialization = Serialization.yaml
|
|
105
105
|
path: str
|
|
106
106
|
|
|
@@ -113,26 +113,25 @@ class Loadable(BaseModel):
|
|
|
113
113
|
raise
|
|
114
114
|
|
|
115
115
|
@staticmethod
|
|
116
|
-
def from_legacy_fmt(
|
|
117
|
-
if "://" not in
|
|
116
|
+
def from_legacy_fmt(fmt_string: str) -> "Loadable":
|
|
117
|
+
if "://" not in fmt_string:
|
|
118
118
|
return Loadable(
|
|
119
|
-
protocol=
|
|
119
|
+
protocol="inline", serialization=Serialization.string, path=fmt_string
|
|
120
120
|
)
|
|
121
121
|
try:
|
|
122
|
-
scheme, path =
|
|
122
|
+
scheme, path = fmt_string.split("://")
|
|
123
123
|
except ValueError:
|
|
124
|
-
raise ValueError(
|
|
124
|
+
raise ValueError(fmt_string)
|
|
125
125
|
try:
|
|
126
|
-
|
|
126
|
+
proto, ser = scheme.split("+")
|
|
127
127
|
except ValueError:
|
|
128
|
-
|
|
128
|
+
proto, ser = scheme, "yaml"
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if proto in (Protocol.python, Protocol.module):
|
|
130
|
+
serialization: Serialization = Serialization(ser)
|
|
131
|
+
if proto in ("python", "module"):
|
|
133
132
|
serialization = Serialization.raw
|
|
134
|
-
if proto in (
|
|
135
|
-
path = "://".join([proto
|
|
133
|
+
if proto in ("http", "https"):
|
|
134
|
+
path = "://".join([proto, path])
|
|
136
135
|
|
|
137
136
|
return Loadable(
|
|
138
137
|
protocol=proto,
|
|
@@ -145,32 +144,32 @@ def raise_(e: Exception) -> Exception:
|
|
|
145
144
|
raise e
|
|
146
145
|
|
|
147
146
|
|
|
148
|
-
def load_file(path: str,
|
|
147
|
+
def load_file(path: str, ser: Serialization) -> Any:
|
|
149
148
|
with open(path) as f:
|
|
150
149
|
contents = f.read()
|
|
151
150
|
try:
|
|
152
|
-
return serializers[
|
|
151
|
+
return serializers[ser](contents)
|
|
153
152
|
except FileNotFoundError:
|
|
154
153
|
raise FileNotFoundError(f"Unable to load {path}")
|
|
155
154
|
|
|
156
155
|
|
|
157
|
-
def load_package_data(path: str,
|
|
156
|
+
def load_package_data(path: str, ser: Serialization) -> Any:
|
|
158
157
|
pkg, pkg_file = path.split(":")
|
|
159
158
|
data = get_package_file_bytes(pkg, pkg_file)
|
|
160
|
-
return serializers[
|
|
159
|
+
return serializers[ser](data)
|
|
161
160
|
|
|
162
161
|
|
|
163
|
-
def load_http(path: str,
|
|
162
|
+
def load_http(path: str, ser: Serialization) -> Any:
|
|
164
163
|
response = requests.get(path)
|
|
165
164
|
response.raise_for_status()
|
|
166
165
|
data = response.text
|
|
167
|
-
return serializers[
|
|
166
|
+
return serializers[ser](data)
|
|
168
167
|
|
|
169
168
|
|
|
170
|
-
def load_env(variable: str,
|
|
169
|
+
def load_env(variable: str, ser: Serialization = Serialization.raw) -> Any:
|
|
171
170
|
data = os.getenv(variable)
|
|
172
171
|
try:
|
|
173
|
-
return serializers[
|
|
172
|
+
return serializers[ser](data)
|
|
174
173
|
except AttributeError as e:
|
|
175
174
|
raise AttributeError(
|
|
176
175
|
f"Unable to read environment variable {variable}: {repr(e)}"
|
|
@@ -188,7 +187,7 @@ def load_module(name: str, _: Serialization = Serialization.raw) -> Any:
|
|
|
188
187
|
return imported
|
|
189
188
|
|
|
190
189
|
|
|
191
|
-
def load_s3(path: str,
|
|
190
|
+
def load_s3(path: str, ser: Serialization = Serialization.raw) -> Any:
|
|
192
191
|
if isinstance(boto3, type(None)):
|
|
193
192
|
raise ImportError(
|
|
194
193
|
"boto3 must be installed to load S3 paths. Use ``pip install sovereign[boto]``"
|
|
@@ -197,7 +196,7 @@ def load_s3(path: str, loader: Serialization = Serialization.raw) -> Any:
|
|
|
197
196
|
s3 = boto3.client("s3")
|
|
198
197
|
response = s3.get_object(Bucket=bucket, Key=key)
|
|
199
198
|
data = "".join([chunk.decode() for chunk in response["Body"]])
|
|
200
|
-
return serializers[
|
|
199
|
+
return serializers[ser](data)
|
|
201
200
|
|
|
202
201
|
|
|
203
202
|
def load_python(path: str, _: Serialization = Serialization.raw) -> ModuleType:
|
|
@@ -210,14 +209,14 @@ def load_inline(path: str, _: Serialization = Serialization.raw) -> Any:
|
|
|
210
209
|
return str(path)
|
|
211
210
|
|
|
212
211
|
|
|
213
|
-
loaders: Dict[
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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,
|
|
223
222
|
}
|
|
@@ -13,6 +13,8 @@ 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
|
|
16
18
|
from structlog.stdlib import BoundLogger
|
|
17
19
|
|
|
18
20
|
from sovereign.config_loader import Loadable
|
|
@@ -53,6 +55,14 @@ class TemplateContext:
|
|
|
53
55
|
self.stats = stats
|
|
54
56
|
# initial load
|
|
55
57
|
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
|
|
56
66
|
asyncio.run(self.load_context_variables())
|
|
57
67
|
|
|
58
68
|
async def start_refresh_context(self) -> NoReturn:
|
|
@@ -60,7 +70,6 @@ class TemplateContext:
|
|
|
60
70
|
await poll_forever_cron(self.refresh_cron, self.refresh_context)
|
|
61
71
|
elif self.refresh_rate is not None:
|
|
62
72
|
await poll_forever(self.refresh_rate, self.refresh_context)
|
|
63
|
-
|
|
64
73
|
raise RuntimeError("Failed to start refresh_context, this should never happen")
|
|
65
74
|
|
|
66
75
|
async def refresh_context(self) -> None:
|
|
@@ -68,25 +77,25 @@ class TemplateContext:
|
|
|
68
77
|
|
|
69
78
|
async def _load_context(
|
|
70
79
|
self,
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
name: str,
|
|
81
|
+
loader: Loadable | str,
|
|
73
82
|
refresh_num_retries: int,
|
|
74
83
|
refresh_retry_interval_secs: int,
|
|
75
84
|
) -> LoadContextResponse:
|
|
76
85
|
retries_left = refresh_num_retries
|
|
77
|
-
|
|
86
|
+
response = {}
|
|
78
87
|
|
|
79
88
|
while True:
|
|
80
89
|
try:
|
|
81
|
-
if isinstance(
|
|
82
|
-
|
|
83
|
-
elif isinstance(
|
|
84
|
-
|
|
90
|
+
if isinstance(loader, Loadable):
|
|
91
|
+
response = loader.load()
|
|
92
|
+
elif isinstance(loader, str):
|
|
93
|
+
response = Loadable.from_legacy_fmt(loader).load()
|
|
85
94
|
self.stats.increment(
|
|
86
95
|
"context.refresh.success",
|
|
87
|
-
tags=[f"context:{
|
|
96
|
+
tags=[f"context:{name}"],
|
|
88
97
|
)
|
|
89
|
-
return LoadContextResponse(
|
|
98
|
+
return LoadContextResponse(name, response)
|
|
90
99
|
# pylint: disable=broad-except
|
|
91
100
|
except Exception as e:
|
|
92
101
|
retries_left -= 1
|
|
@@ -95,34 +104,29 @@ class TemplateContext:
|
|
|
95
104
|
self.logger.error(str(e), traceback=tb)
|
|
96
105
|
self.stats.increment(
|
|
97
106
|
"context.refresh.error",
|
|
98
|
-
tags=[f"context:{
|
|
107
|
+
tags=[f"context:{name}"],
|
|
99
108
|
)
|
|
100
|
-
return LoadContextResponse(
|
|
109
|
+
return LoadContextResponse(name, response, False)
|
|
101
110
|
else:
|
|
102
111
|
await asyncio.sleep(refresh_retry_interval_secs)
|
|
103
112
|
|
|
104
113
|
async def load_context_variables(self) -> None:
|
|
105
|
-
|
|
106
|
-
for
|
|
107
|
-
|
|
114
|
+
coroutines: list[Awaitable[LoadContextResponse]] = []
|
|
115
|
+
for name, conf in self.configured_context.items():
|
|
116
|
+
coroutines.append(
|
|
108
117
|
self._load_context(
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
name,
|
|
119
|
+
conf,
|
|
111
120
|
self.refresh_num_retries,
|
|
112
121
|
self.refresh_retry_interval_secs,
|
|
113
122
|
)
|
|
114
123
|
)
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
*context_coroutines
|
|
118
|
-
)
|
|
125
|
+
results: list[LoadContextResponse] = await asyncio.gather(*coroutines)
|
|
119
126
|
|
|
120
|
-
for
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
or context_result.context_name not in self.context
|
|
124
|
-
):
|
|
125
|
-
self.context[context_result.context_name] = context_result.context
|
|
127
|
+
for res in results:
|
|
128
|
+
if res.success or res.context_name not in self.context:
|
|
129
|
+
self.context[res.context_name] = res.context
|
|
126
130
|
|
|
127
131
|
if "crypto" not in self.context and self.crypto:
|
|
128
132
|
self.context["crypto"] = self.crypto
|
|
@@ -162,7 +166,9 @@ class TemplateContext:
|
|
|
162
166
|
ret = self.build_new_context_from_instances(
|
|
163
167
|
node_value=self.poller.extract_node_key(request.node),
|
|
164
168
|
)
|
|
169
|
+
ret["__hide_from_ui"] = lambda v: v
|
|
165
170
|
if request.hide_private_keys:
|
|
171
|
+
ret["__hide_from_ui"] = lambda _: "(value hidden)"
|
|
166
172
|
ret["crypto"] = CipherContainer.from_encryption_configs(
|
|
167
173
|
encryption_configs=[EncryptionConfig("", EncryptionType.DISABLED)],
|
|
168
174
|
logger=self.logger,
|
|
@@ -5,21 +5,22 @@ from dataclasses import dataclass
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from os import getenv
|
|
7
7
|
from types import ModuleType
|
|
8
|
-
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
8
|
+
from typing import Any, Dict, List, Optional, Self, Tuple, Type, Union
|
|
9
9
|
|
|
10
10
|
from croniter import CroniterBadCronError, croniter
|
|
11
11
|
from fastapi.responses import JSONResponse
|
|
12
12
|
from jinja2 import Template, meta
|
|
13
13
|
from pydantic import (
|
|
14
14
|
BaseModel,
|
|
15
|
-
|
|
15
|
+
ConfigDict,
|
|
16
16
|
Field,
|
|
17
17
|
SecretStr,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
model_validator,
|
|
19
|
+
field_validator,
|
|
20
20
|
)
|
|
21
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
21
22
|
|
|
22
|
-
from sovereign.config_loader import Loadable,
|
|
23
|
+
from sovereign.config_loader import Loadable, Serialization, jinja_env
|
|
23
24
|
from sovereign.utils.crypto.suites import EncryptionType
|
|
24
25
|
from sovereign.utils.version_info import compute_hash
|
|
25
26
|
|
|
@@ -65,11 +66,13 @@ class StatsdConfig(BaseModel):
|
|
|
65
66
|
enabled: bool = False
|
|
66
67
|
use_ms: bool = True
|
|
67
68
|
|
|
68
|
-
@
|
|
69
|
+
@field_validator("host", mode="before")
|
|
70
|
+
@classmethod
|
|
69
71
|
def load_host(cls, v: str) -> Any:
|
|
70
72
|
return Loadable.from_legacy_fmt(v).load()
|
|
71
73
|
|
|
72
|
-
@
|
|
74
|
+
@field_validator("port", mode="before")
|
|
75
|
+
@classmethod
|
|
73
76
|
def load_port(cls, v: Union[int, str]) -> Any:
|
|
74
77
|
if isinstance(v, int):
|
|
75
78
|
return v
|
|
@@ -78,7 +81,8 @@ class StatsdConfig(BaseModel):
|
|
|
78
81
|
else:
|
|
79
82
|
raise ValueError(f"Received an invalid port: {v}")
|
|
80
83
|
|
|
81
|
-
@
|
|
84
|
+
@field_validator("tags", mode="before")
|
|
85
|
+
@classmethod
|
|
82
86
|
def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
|
|
83
87
|
ret = dict()
|
|
84
88
|
for key, value in v.items():
|
|
@@ -108,14 +112,14 @@ class DiscoveryCacheConfig(BaseModel):
|
|
|
108
112
|
socket_keepalive: bool = True # Try to keep connections to redis around.
|
|
109
113
|
ttl: int = 60
|
|
110
114
|
|
|
111
|
-
@
|
|
112
|
-
def set_default_protocol(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return values
|
|
115
|
+
@model_validator(mode="after")
|
|
116
|
+
def set_default_protocol(self) -> Self:
|
|
117
|
+
if self.secure:
|
|
118
|
+
self.protocol = "rediss://"
|
|
119
|
+
return self
|
|
117
120
|
|
|
118
|
-
@
|
|
121
|
+
@model_validator(mode="before")
|
|
122
|
+
@classmethod
|
|
119
123
|
def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
120
124
|
if host := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_HOST"):
|
|
121
125
|
values["host"] = host
|
|
@@ -132,7 +136,7 @@ class XdsTemplate:
|
|
|
132
136
|
self.loadable: Loadable = Loadable.from_legacy_fmt(path)
|
|
133
137
|
elif isinstance(path, Loadable):
|
|
134
138
|
self.loadable = path
|
|
135
|
-
self.is_python_source = self.loadable.protocol ==
|
|
139
|
+
self.is_python_source = self.loadable.protocol == "python"
|
|
136
140
|
self.source = self.load_source()
|
|
137
141
|
template_ast = jinja_env.parse(self.source)
|
|
138
142
|
self.jinja_variables = meta.find_undeclared_variables(template_ast)
|
|
@@ -177,7 +181,7 @@ class XdsTemplate:
|
|
|
177
181
|
# we can simply read and return the source of it.
|
|
178
182
|
old_protocol = self.loadable.protocol
|
|
179
183
|
old_serialization = self.loadable.serialization
|
|
180
|
-
self.loadable.protocol =
|
|
184
|
+
self.loadable.protocol = "inline"
|
|
181
185
|
self.loadable.serialization = Serialization("string")
|
|
182
186
|
ret = self.loadable.load()
|
|
183
187
|
self.loadable.protocol = old_protocol
|
|
@@ -221,9 +225,9 @@ class ProcessedTemplate:
|
|
|
221
225
|
|
|
222
226
|
|
|
223
227
|
class Locality(BaseModel):
|
|
224
|
-
region: str = Field(None)
|
|
225
|
-
zone: str = Field(None)
|
|
226
|
-
sub_zone: str = Field(None)
|
|
228
|
+
region: Optional[str] = Field(None)
|
|
229
|
+
zone: Optional[str] = Field(None)
|
|
230
|
+
sub_zone: Optional[str] = Field(None)
|
|
227
231
|
|
|
228
232
|
|
|
229
233
|
class SemanticVersion(BaseModel):
|
|
@@ -255,8 +259,8 @@ class Node(BaseModel):
|
|
|
255
259
|
description="The ``--service-cluster`` configured by the Envoy client",
|
|
256
260
|
)
|
|
257
261
|
metadata: Dict[str, Any] = Field(default_factory=dict, title="Key:value metadata")
|
|
258
|
-
locality: Locality = Field(Locality(), title="Locality")
|
|
259
|
-
build_version: str = Field(
|
|
262
|
+
locality: Locality = Field(Locality(), title="Locality")
|
|
263
|
+
build_version: Optional[str] = Field(
|
|
260
264
|
None, # Optional in the v3 Envoy API
|
|
261
265
|
title="Envoy build/release version string",
|
|
262
266
|
description="Used to identify what version of Envoy the "
|
|
@@ -269,7 +273,7 @@ class Node(BaseModel):
|
|
|
269
273
|
client_features: List[str] = []
|
|
270
274
|
|
|
271
275
|
@property
|
|
272
|
-
def common(self) -> Tuple[str, str, str, BuildVersion, Locality]:
|
|
276
|
+
def common(self) -> Tuple[str, Optional[str], str, BuildVersion, Locality]:
|
|
273
277
|
"""
|
|
274
278
|
Returns fields that are the same in adjacent proxies
|
|
275
279
|
ie. proxies that are part of the same logical group
|
|
@@ -292,7 +296,7 @@ class Resources(List[str]):
|
|
|
292
296
|
def __contains__(self, item: object) -> bool:
|
|
293
297
|
if len(self) == 0:
|
|
294
298
|
return True
|
|
295
|
-
return
|
|
299
|
+
return super().__contains__(item)
|
|
296
300
|
|
|
297
301
|
|
|
298
302
|
class Status(BaseModel):
|
|
@@ -306,19 +310,20 @@ class DiscoveryRequest(BaseModel):
|
|
|
306
310
|
version_info: str = Field(
|
|
307
311
|
"0", title="The version of the envoy clients current configuration"
|
|
308
312
|
)
|
|
309
|
-
resource_names: Resources = Field(
|
|
313
|
+
resource_names: list[str] | Resources = Field(
|
|
310
314
|
Resources(), title="List of requested resource names"
|
|
311
315
|
)
|
|
312
316
|
hide_private_keys: bool = False
|
|
313
317
|
type_url: Optional[str] = Field(
|
|
314
318
|
None, title="The corresponding type_url for the requested resource"
|
|
315
319
|
)
|
|
316
|
-
desired_controlplane: str = Field(
|
|
320
|
+
desired_controlplane: Optional[str] = Field(
|
|
317
321
|
None, title="The host header provided in the Discovery Request"
|
|
318
322
|
)
|
|
319
|
-
error_detail: Status = Field(
|
|
323
|
+
error_detail: Optional[Status] = Field(
|
|
320
324
|
None, title="Error details from the previous xDS request"
|
|
321
325
|
)
|
|
326
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
322
327
|
|
|
323
328
|
@property
|
|
324
329
|
def envoy_version(self) -> str:
|
|
@@ -328,6 +333,8 @@ class DiscoveryRequest(BaseModel):
|
|
|
328
333
|
except AssertionError:
|
|
329
334
|
try:
|
|
330
335
|
build_version = self.node.build_version
|
|
336
|
+
if build_version is None:
|
|
337
|
+
return "default"
|
|
331
338
|
_, version, *_ = build_version.split("/")
|
|
332
339
|
except (AttributeError, ValueError):
|
|
333
340
|
# TODO: log/metric this?
|
|
@@ -355,33 +362,35 @@ class DiscoveryResponse(BaseModel):
|
|
|
355
362
|
|
|
356
363
|
|
|
357
364
|
class SovereignAsgiConfig(BaseSettings):
|
|
358
|
-
host: str = "0.0.0.0"
|
|
359
|
-
port: int = 8080
|
|
360
|
-
keepalive: int = 5
|
|
361
|
-
workers: int =
|
|
362
|
-
|
|
365
|
+
host: str = Field("0.0.0.0", alias="SOVEREIGN_HOST")
|
|
366
|
+
port: int = Field(8080, alias="SOVEREIGN_PORT")
|
|
367
|
+
keepalive: int = Field(5, alias="SOVEREIGN_KEEPALIVE")
|
|
368
|
+
workers: int = Field(
|
|
369
|
+
default_factory=lambda: multiprocessing.cpu_count() * 2 + 1,
|
|
370
|
+
alias="SOVEREIGN_WORKERS",
|
|
371
|
+
)
|
|
372
|
+
threads: int = Field(1, alias="SOVEREIGN_THREADS")
|
|
363
373
|
reuse_port: bool = True
|
|
364
|
-
preload_app: bool = True
|
|
374
|
+
preload_app: bool = Field(True, alias="SOVEREIGN_PRELOAD")
|
|
365
375
|
log_level: str = "warning"
|
|
366
376
|
worker_class: str = "uvicorn.workers.UvicornWorker"
|
|
367
|
-
worker_timeout: int = 30
|
|
377
|
+
worker_timeout: int = Field(30, alias="SOVEREIGN_WORKER_TIMEOUT")
|
|
368
378
|
worker_tmp_dir: str = "/dev/shm"
|
|
369
|
-
graceful_timeout: int =
|
|
370
|
-
max_requests: int = 0
|
|
371
|
-
max_requests_jitter: int = 0
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
379
|
+
graceful_timeout: Optional[int] = Field(None)
|
|
380
|
+
max_requests: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS")
|
|
381
|
+
max_requests_jitter: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS_JITTER")
|
|
382
|
+
model_config = SettingsConfigDict(
|
|
383
|
+
env_file=".env",
|
|
384
|
+
extra="ignore",
|
|
385
|
+
env_file_encoding="utf-8",
|
|
386
|
+
populate_by_name=True,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
@model_validator(mode="after")
|
|
390
|
+
def validate_graceful_timeout(self) -> Self:
|
|
391
|
+
if self.graceful_timeout is None:
|
|
392
|
+
self.graceful_timeout = self.worker_timeout * 2
|
|
393
|
+
return self
|
|
385
394
|
|
|
386
395
|
def as_gunicorn_conf(self) -> Dict[str, Any]:
|
|
387
396
|
return {
|
|
@@ -405,54 +414,45 @@ class SovereignConfig(BaseSettings):
|
|
|
405
414
|
sources: List[ConfiguredSource]
|
|
406
415
|
templates: Dict[str, Dict[str, Union[str, Loadable]]]
|
|
407
416
|
template_context: Dict[str, Any] = {}
|
|
408
|
-
eds_priority_matrix: Dict[str, Dict[str,
|
|
417
|
+
eds_priority_matrix: Dict[str, Dict[str, int]] = {}
|
|
409
418
|
modifiers: List[str] = []
|
|
410
419
|
global_modifiers: List[str] = []
|
|
411
420
|
regions: List[str] = []
|
|
412
421
|
statsd: StatsdConfig = StatsdConfig()
|
|
413
|
-
auth_enabled: bool = False
|
|
414
|
-
auth_passwords: str = ""
|
|
415
|
-
encryption_key: str = ""
|
|
416
|
-
environment: str = "local"
|
|
417
|
-
debug_enabled: bool = False
|
|
418
|
-
sentry_dsn: str = ""
|
|
419
|
-
node_match_key: str = "cluster"
|
|
420
|
-
node_matching: bool = True
|
|
421
|
-
source_match_key: str =
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
422
|
+
auth_enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
|
|
423
|
+
auth_passwords: str = Field("", alias="SOVEREIGN_AUTH_PASSWORDS")
|
|
424
|
+
encryption_key: str = Field("", alias="SOVEREIGN_ENCRYPTION_KEY")
|
|
425
|
+
environment: str = Field("local", alias="SOVEREIGN_ENVIRONMENT")
|
|
426
|
+
debug_enabled: bool = Field(False, alias="SOVEREIGN_DEBUG_ENABLED")
|
|
427
|
+
sentry_dsn: str = Field("", alias="SOVEREIGN_SENTRY_DSN")
|
|
428
|
+
node_match_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
|
|
429
|
+
node_matching: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING")
|
|
430
|
+
source_match_key: str = Field(
|
|
431
|
+
"service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY"
|
|
432
|
+
)
|
|
433
|
+
sources_refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
|
|
434
|
+
cache_strategy: str = Field("context", alias="SOVEREIGN_CACHE_STRATEGY")
|
|
435
|
+
refresh_context: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
|
|
436
|
+
context_refresh_rate: Optional[int] = Field(
|
|
437
|
+
None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE"
|
|
438
|
+
)
|
|
439
|
+
context_refresh_cron: Optional[str] = Field(
|
|
440
|
+
None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON"
|
|
441
|
+
)
|
|
442
|
+
dns_hard_fail: bool = Field(False, alias="SOVEREIGN_DNS_HARD_FAIL")
|
|
443
|
+
enable_application_logs: bool = Field(
|
|
444
|
+
True, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS"
|
|
445
|
+
)
|
|
446
|
+
enable_access_logs: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
|
|
447
|
+
log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
|
|
448
|
+
ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
|
|
432
449
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"environment": {"env": "SOVEREIGN_ENVIRONMENT"},
|
|
440
|
-
"debug_enabled": {"env": "SOVEREIGN_DEBUG_ENABLED"},
|
|
441
|
-
"sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
|
|
442
|
-
"node_match_key": {"env": "SOVEREIGN_NODE_MATCH_KEY"},
|
|
443
|
-
"node_matching": {"env": "SOVEREIGN_NODE_MATCHING"},
|
|
444
|
-
"source_match_key": {"env": "SOVEREIGN_SOURCE_MATCH_KEY"},
|
|
445
|
-
"sources_refresh_rate": {"env": "SOVEREIGN_SOURCES_REFRESH_RATE"},
|
|
446
|
-
"cache_strategy": {"env": "SOVEREIGN_CACHE_STRATEGY"},
|
|
447
|
-
"refresh_context": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
|
|
448
|
-
"context_refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
|
|
449
|
-
"context_refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
|
|
450
|
-
"dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
|
|
451
|
-
"enable_application_logs": {"env": "SOVEREIGN_ENABLE_APPLICATION_LOGS"},
|
|
452
|
-
"enable_access_logs": {"env": "SOVEREIGN_ENABLE_ACCESS_LOGS"},
|
|
453
|
-
"log_fmt": {"env": "SOVEREIGN_LOG_FORMAT"},
|
|
454
|
-
"ignore_empty_fields": {"env": "SOVEREIGN_LOG_IGNORE_EMPTY"},
|
|
455
|
-
}
|
|
450
|
+
model_config = SettingsConfigDict(
|
|
451
|
+
env_file=".env",
|
|
452
|
+
extra="ignore",
|
|
453
|
+
env_file_encoding="utf-8",
|
|
454
|
+
populate_by_name=True,
|
|
455
|
+
)
|
|
456
456
|
|
|
457
457
|
@property
|
|
458
458
|
def passwords(self) -> List[str]:
|
|
@@ -492,16 +492,15 @@ class TemplateSpecification(BaseModel):
|
|
|
492
492
|
|
|
493
493
|
|
|
494
494
|
class NodeMatching(BaseSettings):
|
|
495
|
-
enabled: bool = True
|
|
496
|
-
source_key: str = "service_clusters"
|
|
497
|
-
node_key: str = "cluster"
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
495
|
+
enabled: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING_ENABLED")
|
|
496
|
+
source_key: str = Field("service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY")
|
|
497
|
+
node_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
|
|
498
|
+
model_config = SettingsConfigDict(
|
|
499
|
+
env_file=".env",
|
|
500
|
+
extra="ignore",
|
|
501
|
+
env_file_encoding="utf-8",
|
|
502
|
+
populate_by_name=True,
|
|
503
|
+
)
|
|
505
504
|
|
|
506
505
|
|
|
507
506
|
@dataclass
|
|
@@ -511,9 +510,15 @@ class EncryptionConfig:
|
|
|
511
510
|
|
|
512
511
|
|
|
513
512
|
class AuthConfiguration(BaseSettings):
|
|
514
|
-
enabled: bool = False
|
|
515
|
-
auth_passwords: SecretStr = SecretStr("")
|
|
516
|
-
encryption_key: SecretStr = SecretStr("")
|
|
513
|
+
enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
|
|
514
|
+
auth_passwords: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_AUTH_PASSWORDS")
|
|
515
|
+
encryption_key: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_ENCRYPTION_KEY")
|
|
516
|
+
model_config = SettingsConfigDict(
|
|
517
|
+
env_file=".env",
|
|
518
|
+
extra="ignore",
|
|
519
|
+
env_file_encoding="utf-8",
|
|
520
|
+
populate_by_name=True,
|
|
521
|
+
)
|
|
517
522
|
|
|
518
523
|
@staticmethod
|
|
519
524
|
def _create_encryption_config(encryption_key_setting: str) -> EncryptionConfig:
|
|
@@ -534,37 +539,29 @@ class AuthConfiguration(BaseSettings):
|
|
|
534
539
|
)
|
|
535
540
|
return configs
|
|
536
541
|
|
|
537
|
-
class Config:
|
|
538
|
-
fields = {
|
|
539
|
-
"enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
|
|
540
|
-
"auth_passwords": {"env": "SOVEREIGN_AUTH_PASSWORDS"},
|
|
541
|
-
"encryption_key": {"env": "SOVEREIGN_ENCRYPTION_KEY"},
|
|
542
|
-
}
|
|
543
|
-
|
|
544
542
|
|
|
545
543
|
class ApplicationLogConfiguration(BaseSettings):
|
|
546
|
-
enabled: bool = False
|
|
547
|
-
log_fmt: Optional[str] = None
|
|
544
|
+
enabled: bool = Field(False, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS")
|
|
545
|
+
log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_APPLICATION_LOG_FORMAT")
|
|
548
546
|
# currently only support /dev/stdout as JSON
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
547
|
+
model_config = SettingsConfigDict(
|
|
548
|
+
env_file=".env",
|
|
549
|
+
extra="ignore",
|
|
550
|
+
env_file_encoding="utf-8",
|
|
551
|
+
populate_by_name=True,
|
|
552
|
+
)
|
|
555
553
|
|
|
556
554
|
|
|
557
555
|
class AccessLogConfiguration(BaseSettings):
|
|
558
|
-
enabled: bool = True
|
|
559
|
-
log_fmt: Optional[str] = None
|
|
560
|
-
ignore_empty_fields: bool = False
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
556
|
+
enabled: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
|
|
557
|
+
log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_LOG_FORMAT")
|
|
558
|
+
ignore_empty_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
|
|
559
|
+
model_config = SettingsConfigDict(
|
|
560
|
+
env_file=".env",
|
|
561
|
+
extra="ignore",
|
|
562
|
+
env_file_encoding="utf-8",
|
|
563
|
+
populate_by_name=True,
|
|
564
|
+
)
|
|
568
565
|
|
|
569
566
|
|
|
570
567
|
class LoggingConfiguration(BaseSettings):
|
|
@@ -574,11 +571,19 @@ class LoggingConfiguration(BaseSettings):
|
|
|
574
571
|
|
|
575
572
|
class ContextConfiguration(BaseSettings):
|
|
576
573
|
context: Dict[str, Loadable] = {}
|
|
577
|
-
refresh: bool = False
|
|
578
|
-
refresh_rate: Optional[int] = None
|
|
579
|
-
refresh_cron: Optional[str] = None
|
|
580
|
-
refresh_num_retries: int = 3
|
|
581
|
-
refresh_retry_interval_secs: int =
|
|
574
|
+
refresh: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
|
|
575
|
+
refresh_rate: Optional[int] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE")
|
|
576
|
+
refresh_cron: Optional[str] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON")
|
|
577
|
+
refresh_num_retries: int = Field(3, alias="SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES")
|
|
578
|
+
refresh_retry_interval_secs: int = Field(
|
|
579
|
+
10, alias="SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
|
|
580
|
+
)
|
|
581
|
+
model_config = SettingsConfigDict(
|
|
582
|
+
env_file=".env",
|
|
583
|
+
extra="ignore",
|
|
584
|
+
env_file_encoding="utf-8",
|
|
585
|
+
populate_by_name=True,
|
|
586
|
+
)
|
|
582
587
|
|
|
583
588
|
@staticmethod
|
|
584
589
|
def context_from_legacy(context: Dict[str, str]) -> Dict[str, Loadable]:
|
|
@@ -587,20 +592,16 @@ class ContextConfiguration(BaseSettings):
|
|
|
587
592
|
ret[key] = Loadable.from_legacy_fmt(value)
|
|
588
593
|
return ret
|
|
589
594
|
|
|
590
|
-
@
|
|
591
|
-
def validate_single_use_refresh_method(
|
|
592
|
-
|
|
593
|
-
) -> Dict[str, Any]:
|
|
594
|
-
refresh_rate = values.get("refresh_rate")
|
|
595
|
-
refresh_cron = values.get("refresh_cron")
|
|
596
|
-
|
|
597
|
-
if (refresh_rate is not None) and (refresh_cron is not None):
|
|
595
|
+
@model_validator(mode="after")
|
|
596
|
+
def validate_single_use_refresh_method(self) -> Self:
|
|
597
|
+
if (self.refresh_rate is not None) and (self.refresh_cron is not None):
|
|
598
598
|
raise RuntimeError(
|
|
599
|
-
f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {refresh_rate=} and {refresh_cron=}"
|
|
599
|
+
f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {self.refresh_rate=} and {self.refresh_cron=}"
|
|
600
600
|
)
|
|
601
|
-
return
|
|
601
|
+
return self
|
|
602
602
|
|
|
603
|
-
@
|
|
603
|
+
@model_validator(mode="before")
|
|
604
|
+
@classmethod
|
|
604
605
|
def set_default_refresh_rate(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
605
606
|
refresh_rate = values.get("refresh_rate")
|
|
606
607
|
refresh_cron = values.get("refresh_cron")
|
|
@@ -609,7 +610,8 @@ class ContextConfiguration(BaseSettings):
|
|
|
609
610
|
values["refresh_rate"] = 3600
|
|
610
611
|
return values
|
|
611
612
|
|
|
612
|
-
@
|
|
613
|
+
@field_validator("refresh_cron")
|
|
614
|
+
@classmethod
|
|
613
615
|
def validate_refresh_cron(cls, v: Optional[str]) -> Optional[str]:
|
|
614
616
|
if v is None:
|
|
615
617
|
return v
|
|
@@ -617,36 +619,34 @@ class ContextConfiguration(BaseSettings):
|
|
|
617
619
|
raise CroniterBadCronError(f"'{v}' is not a valid cron expression")
|
|
618
620
|
return v
|
|
619
621
|
|
|
620
|
-
class Config:
|
|
621
|
-
fields = {
|
|
622
|
-
"refresh": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
|
|
623
|
-
"refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
|
|
624
|
-
"refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
|
|
625
|
-
"refresh_num_retries": {"env": "SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES"},
|
|
626
|
-
"refresh_retry_interval_secs": {
|
|
627
|
-
"env": "SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
|
|
628
|
-
},
|
|
629
|
-
}
|
|
630
|
-
|
|
631
622
|
|
|
632
623
|
class SourcesConfiguration(BaseSettings):
|
|
633
|
-
refresh_rate: int = 30
|
|
634
|
-
cache_strategy: CacheStrategy =
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
624
|
+
refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
|
|
625
|
+
cache_strategy: CacheStrategy = Field(
|
|
626
|
+
CacheStrategy.context, alias="SOVEREIGN_CACHE_STRATEGY"
|
|
627
|
+
)
|
|
628
|
+
model_config = SettingsConfigDict(
|
|
629
|
+
env_file=".env",
|
|
630
|
+
extra="ignore",
|
|
631
|
+
env_file_encoding="utf-8",
|
|
632
|
+
populate_by_name=True,
|
|
633
|
+
)
|
|
641
634
|
|
|
642
635
|
|
|
643
636
|
class LegacyConfig(BaseSettings):
|
|
644
637
|
regions: Optional[List[str]] = None
|
|
645
|
-
eds_priority_matrix: Optional[Dict[str, Dict[str,
|
|
646
|
-
dns_hard_fail: Optional[bool] = None
|
|
647
|
-
environment: Optional[str] = None
|
|
638
|
+
eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
|
|
639
|
+
dns_hard_fail: Optional[bool] = Field(None, alias="SOVEREIGN_DNS_HARD_FAIL")
|
|
640
|
+
environment: Optional[str] = Field(None, alias="SOVEREIGN_ENVIRONMENT")
|
|
641
|
+
model_config = SettingsConfigDict(
|
|
642
|
+
env_file=".env",
|
|
643
|
+
extra="ignore",
|
|
644
|
+
env_file_encoding="utf-8",
|
|
645
|
+
populate_by_name=True,
|
|
646
|
+
)
|
|
648
647
|
|
|
649
|
-
@
|
|
648
|
+
@field_validator("regions")
|
|
649
|
+
@classmethod
|
|
650
650
|
def regions_is_set(cls, v: Optional[List[str]]) -> List[str]:
|
|
651
651
|
if v is not None:
|
|
652
652
|
warnings.warn(
|
|
@@ -659,7 +659,8 @@ class LegacyConfig(BaseSettings):
|
|
|
659
659
|
else:
|
|
660
660
|
return []
|
|
661
661
|
|
|
662
|
-
@
|
|
662
|
+
@field_validator("eds_priority_matrix")
|
|
663
|
+
@classmethod
|
|
663
664
|
def eds_priority_matrix_is_set(
|
|
664
665
|
cls, v: Optional[Dict[str, Dict[str, Any]]]
|
|
665
666
|
) -> Dict[str, Dict[str, Any]]:
|
|
@@ -674,7 +675,8 @@ class LegacyConfig(BaseSettings):
|
|
|
674
675
|
else:
|
|
675
676
|
return {}
|
|
676
677
|
|
|
677
|
-
@
|
|
678
|
+
@field_validator("dns_hard_fail")
|
|
679
|
+
@classmethod
|
|
678
680
|
def dns_hard_fail_is_set(cls, v: Optional[bool]) -> bool:
|
|
679
681
|
if v is not None:
|
|
680
682
|
warnings.warn(
|
|
@@ -688,7 +690,8 @@ class LegacyConfig(BaseSettings):
|
|
|
688
690
|
else:
|
|
689
691
|
return False
|
|
690
692
|
|
|
691
|
-
@
|
|
693
|
+
@field_validator("environment")
|
|
694
|
+
@classmethod
|
|
692
695
|
def environment_is_set(cls, v: Optional[str]) -> Optional[str]:
|
|
693
696
|
if v is not None:
|
|
694
697
|
warnings.warn(
|
|
@@ -701,12 +704,6 @@ class LegacyConfig(BaseSettings):
|
|
|
701
704
|
else:
|
|
702
705
|
return None
|
|
703
706
|
|
|
704
|
-
class Config:
|
|
705
|
-
fields = {
|
|
706
|
-
"dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
|
|
707
|
-
"environment": {"env": "SOVEREIGN_ENVIRONMENT"},
|
|
708
|
-
}
|
|
709
|
-
|
|
710
707
|
|
|
711
708
|
class SovereignConfigv2(BaseSettings):
|
|
712
709
|
sources: List[ConfiguredSource]
|
|
@@ -719,16 +716,16 @@ class SovereignConfigv2(BaseSettings):
|
|
|
719
716
|
authentication: AuthConfiguration = AuthConfiguration()
|
|
720
717
|
logging: LoggingConfiguration = LoggingConfiguration()
|
|
721
718
|
statsd: StatsdConfig = StatsdConfig()
|
|
722
|
-
sentry_dsn: SecretStr = SecretStr("")
|
|
723
|
-
debug: bool = False
|
|
719
|
+
sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
|
|
720
|
+
debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
|
|
724
721
|
legacy_fields: LegacyConfig = LegacyConfig()
|
|
725
722
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
723
|
+
model_config = SettingsConfigDict(
|
|
724
|
+
env_file=".env",
|
|
725
|
+
extra="ignore",
|
|
726
|
+
env_file_encoding="utf-8",
|
|
727
|
+
populate_by_name=True,
|
|
728
|
+
)
|
|
732
729
|
|
|
733
730
|
@property
|
|
734
731
|
def passwords(self) -> List[str]:
|
|
@@ -751,10 +748,10 @@ class SovereignConfigv2(BaseSettings):
|
|
|
751
748
|
return self.__repr__()
|
|
752
749
|
|
|
753
750
|
def __repr__(self) -> str:
|
|
754
|
-
return f"SovereignConfigv2({self.
|
|
751
|
+
return f"SovereignConfigv2({self.model_dump()})"
|
|
755
752
|
|
|
756
753
|
def show(self) -> Dict[str, Any]:
|
|
757
|
-
return self.
|
|
754
|
+
return self.model_dump()
|
|
758
755
|
|
|
759
756
|
@staticmethod
|
|
760
757
|
def from_legacy_config(other: SovereignConfig) -> "SovereignConfigv2":
|
|
@@ -16,7 +16,7 @@ router = APIRouter()
|
|
|
16
16
|
@router.get("/xds_dump", summary="Displays all xDS resources as JSON")
|
|
17
17
|
async def display_config(
|
|
18
18
|
xds_type: str = Query(
|
|
19
|
-
..., title="xDS type", description="The type of request",
|
|
19
|
+
..., title="xDS type", description="The type of request", examples=["clusters"]
|
|
20
20
|
),
|
|
21
21
|
service_cluster: str = Query(
|
|
22
22
|
"*", title="The clients service cluster to emulate in this XDS request"
|
|
@@ -48,7 +48,7 @@ async def display_config(
|
|
|
48
48
|
)
|
|
49
49
|
async def debug_template(
|
|
50
50
|
xds_type: str = Query(
|
|
51
|
-
..., title="xDS type", description="The type of request",
|
|
51
|
+
..., title="xDS type", description="The type of request", examples=["clusters"]
|
|
52
52
|
),
|
|
53
53
|
service_cluster: str = Query(
|
|
54
54
|
"*", title="The clients service cluster to emulate in this XDS request"
|
|
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
|
|
File without changes
|
|
File without changes
|