sovereign 1.0.0a4__py3-none-any.whl → 1.0.0b148__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 +2 -2
- sovereign/app.py +3 -6
- sovereign/cache/__init__.py +12 -85
- sovereign/cache/backends/__init__.py +1 -1
- sovereign/cache/backends/s3.py +6 -24
- sovereign/cache/filesystem.py +5 -6
- sovereign/cache/types.py +0 -2
- sovereign/configuration.py +8 -42
- sovereign/context.py +9 -8
- sovereign/dynamic_config/__init__.py +4 -3
- sovereign/dynamic_config/deser.py +1 -1
- sovereign/dynamic_config/loaders.py +3 -3
- sovereign/error_info.py +3 -2
- sovereign/events.py +3 -3
- sovereign/logging/access_logger.py +1 -1
- sovereign/logging/application_logger.py +1 -1
- sovereign/logging/bootstrapper.py +1 -1
- sovereign/modifiers/lib.py +1 -1
- sovereign/rendering.py +90 -22
- sovereign/response_class.py +2 -2
- sovereign/server.py +26 -45
- sovereign/sources/__init__.py +3 -0
- sovereign/sources/file.py +21 -0
- sovereign/sources/inline.py +39 -0
- sovereign/sources/lib.py +41 -0
- sovereign/sources/poller.py +537 -0
- sovereign/statistics.py +1 -2
- sovereign/testing/loaders.py +0 -1
- sovereign/tracing.py +5 -6
- sovereign/types.py +10 -15
- sovereign/utils/auth.py +2 -3
- sovereign/utils/crypto/suites/disabled_cipher.py +2 -2
- sovereign/utils/dictupdate.py +1 -1
- sovereign/utils/eds.py +1 -3
- sovereign/utils/entry_point_loader.py +2 -2
- sovereign/utils/mock.py +3 -4
- sovereign/utils/resources.py +1 -1
- sovereign/utils/templates.py +2 -4
- sovereign/utils/timer.py +3 -5
- sovereign/utils/weighted_clusters.py +1 -2
- sovereign/views/__init__.py +3 -6
- sovereign/views/api.py +7 -28
- sovereign/views/crypto.py +1 -1
- sovereign/views/discovery.py +5 -20
- sovereign/views/healthchecks.py +27 -45
- sovereign/views/interface.py +10 -70
- sovereign/worker.py +31 -20
- {sovereign-1.0.0a4.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +3 -4
- sovereign-1.0.0b148.dist-info/RECORD +77 -0
- {sovereign-1.0.0a4.dist-info → sovereign-1.0.0b148.dist-info}/entry_points.txt +0 -8
- sovereign/rendering_common.py +0 -91
- sovereign/v2/__init__.py +0 -0
- sovereign/v2/data/data_store.py +0 -621
- sovereign/v2/data/render_discovery_response.py +0 -24
- sovereign/v2/data/repositories.py +0 -90
- sovereign/v2/data/utils.py +0 -33
- sovereign/v2/data/worker_queue.py +0 -273
- sovereign/v2/jobs/refresh_context.py +0 -117
- sovereign/v2/jobs/render_discovery_job.py +0 -145
- sovereign/v2/logging.py +0 -81
- sovereign/v2/types.py +0 -41
- sovereign/v2/web.py +0 -101
- sovereign/v2/worker.py +0 -199
- sovereign-1.0.0a4.dist-info/RECORD +0 -85
- {sovereign-1.0.0a4.dist-info → sovereign-1.0.0b148.dist-info}/WHEEL +0 -0
sovereign/rendering.py
CHANGED
|
@@ -7,34 +7,48 @@ Functions used to render and return discovery responses to Envoy proxies.
|
|
|
7
7
|
The templates are configurable. `todo See ref:Configuration#Templates`
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import importlib
|
|
11
|
-
import os
|
|
12
10
|
import traceback
|
|
11
|
+
import importlib
|
|
13
12
|
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
-
from multiprocessing import
|
|
15
|
-
|
|
16
|
-
# noinspection PyProtectedMember
|
|
13
|
+
from multiprocessing import Process, Pipe, cpu_count
|
|
17
14
|
from multiprocessing.connection import Connection
|
|
18
15
|
from typing import Any
|
|
19
16
|
|
|
17
|
+
import yaml
|
|
20
18
|
import pydantic
|
|
19
|
+
from starlette.exceptions import HTTPException
|
|
20
|
+
from yaml.parser import ParserError, ScannerError # type: ignore
|
|
21
21
|
|
|
22
|
-
from sovereign import application_logger as log
|
|
23
|
-
from sovereign import cache, stats
|
|
22
|
+
from sovereign import logs, cache, stats, application_logger as log
|
|
24
23
|
from sovereign.cache.types import Entry
|
|
25
24
|
from sovereign.configuration import config
|
|
26
|
-
from sovereign.rendering_common import (
|
|
27
|
-
add_type_urls,
|
|
28
|
-
deserialize_config,
|
|
29
|
-
filter_resources,
|
|
30
|
-
)
|
|
31
25
|
from sovereign.types import DiscoveryRequest, ProcessedTemplate
|
|
32
|
-
|
|
26
|
+
|
|
33
27
|
|
|
34
28
|
writer = cache.CacheWriter()
|
|
35
29
|
# limit render jobs to number of cores
|
|
36
30
|
POOL = ThreadPoolExecutor(max_workers=cpu_count())
|
|
37
31
|
|
|
32
|
+
type_urls = {
|
|
33
|
+
"v2": {
|
|
34
|
+
"listeners": "type.googleapis.com/envoy.api.v2.Listener",
|
|
35
|
+
"clusters": "type.googleapis.com/envoy.api.v2.Cluster",
|
|
36
|
+
"endpoints": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
|
|
37
|
+
"secrets": "type.googleapis.com/envoy.api.v2.auth.Secret",
|
|
38
|
+
"routes": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
|
|
39
|
+
"scoped-routes": "type.googleapis.com/envoy.api.v2.ScopedRouteConfiguration",
|
|
40
|
+
},
|
|
41
|
+
"v3": {
|
|
42
|
+
"listeners": "type.googleapis.com/envoy.config.listener.v3.Listener",
|
|
43
|
+
"clusters": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
|
|
44
|
+
"endpoints": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
|
|
45
|
+
"secrets": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
|
|
46
|
+
"routes": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
|
|
47
|
+
"scoped-routes": "type.googleapis.com/envoy.config.route.v3.ScopedRouteConfiguration",
|
|
48
|
+
"runtime": "type.googleapis.com/envoy.service.runtime.v3.Runtime",
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
|
|
39
53
|
class RenderJob(pydantic.BaseModel):
|
|
40
54
|
id: str
|
|
@@ -66,7 +80,6 @@ class RenderJob(pydantic.BaseModel):
|
|
|
66
80
|
logger(message)
|
|
67
81
|
|
|
68
82
|
|
|
69
|
-
# noinspection DuplicatedCode
|
|
70
83
|
def generate(job: RenderJob, tx: Connection) -> None:
|
|
71
84
|
request = job.request
|
|
72
85
|
tags = [f"type:{request.resource_type}"]
|
|
@@ -76,7 +89,6 @@ def generate(job: RenderJob, tx: Connection) -> None:
|
|
|
76
89
|
discovery_request=request,
|
|
77
90
|
host_header=request.desired_controlplane,
|
|
78
91
|
resource_names=request.resources,
|
|
79
|
-
utils=templates,
|
|
80
92
|
**job.context,
|
|
81
93
|
)
|
|
82
94
|
if not request.template.is_python_source:
|
|
@@ -86,13 +98,7 @@ def generate(job: RenderJob, tx: Connection) -> None:
|
|
|
86
98
|
resources = filter_resources(content["resources"], request.resources)
|
|
87
99
|
add_type_urls(request.api_version, request.resource_type, resources)
|
|
88
100
|
response = ProcessedTemplate(resources=resources)
|
|
89
|
-
tx.send(
|
|
90
|
-
(
|
|
91
|
-
"info",
|
|
92
|
-
f"Completed rendering of {request}: client_id={job.id} version={response.version_info} "
|
|
93
|
-
f"resources={len(response.resources)} pid={os.getpid()}",
|
|
94
|
-
)
|
|
95
|
-
)
|
|
101
|
+
tx.send(("info", f"Completed rendering of {request} for {job.id}"))
|
|
96
102
|
cached, cache_result = writer.set(
|
|
97
103
|
job.id,
|
|
98
104
|
Entry(
|
|
@@ -122,3 +128,65 @@ def generate(job: RenderJob, tx: Connection) -> None:
|
|
|
122
128
|
finally:
|
|
123
129
|
stats.increment("template.render", tags=tags)
|
|
124
130
|
tx.close()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def deserialize_config(content: str) -> dict[str, Any]:
|
|
134
|
+
try:
|
|
135
|
+
envoy_configuration = yaml.safe_load(content)
|
|
136
|
+
except (ParserError, ScannerError) as e:
|
|
137
|
+
logs.access_logger.queue_log_fields(
|
|
138
|
+
error=repr(e),
|
|
139
|
+
YAML_CONTEXT=e.context,
|
|
140
|
+
YAML_CONTEXT_MARK=e.context_mark,
|
|
141
|
+
YAML_NOTE=e.note,
|
|
142
|
+
YAML_PROBLEM=e.problem,
|
|
143
|
+
YAML_PROBLEM_MARK=e.problem_mark,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if config.sentry_dsn:
|
|
147
|
+
mod = importlib.import_module("sentry_sdk")
|
|
148
|
+
mod.capture_exception(e)
|
|
149
|
+
|
|
150
|
+
raise HTTPException(
|
|
151
|
+
status_code=500,
|
|
152
|
+
detail=(
|
|
153
|
+
"Failed to load configuration, there may be "
|
|
154
|
+
"a syntax error in the configured templates. "
|
|
155
|
+
"Please check Sentry if you have configured Sentry DSN"
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
if not isinstance(envoy_configuration, dict):
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
f"Deserialized configuration is of unexpected format: {envoy_configuration}"
|
|
161
|
+
)
|
|
162
|
+
return envoy_configuration
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def filter_resources(
|
|
166
|
+
generated: list[dict[str, Any]], requested: list[str]
|
|
167
|
+
) -> list[dict[str, Any]]:
|
|
168
|
+
"""
|
|
169
|
+
If Envoy specifically requested a resource, this removes everything
|
|
170
|
+
that does not match the name of the resource.
|
|
171
|
+
If Envoy did not specifically request anything, every resource is retained.
|
|
172
|
+
"""
|
|
173
|
+
if len(requested) == 0:
|
|
174
|
+
return generated
|
|
175
|
+
return [resource for resource in generated if resource_name(resource) in requested]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def resource_name(resource: dict[str, Any]) -> str:
|
|
179
|
+
name = resource.get("name") or resource.get("cluster_name")
|
|
180
|
+
if isinstance(name, str):
|
|
181
|
+
return name
|
|
182
|
+
raise KeyError(
|
|
183
|
+
f"Failed to determine the name or cluster_name of the following resource: {resource}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def add_type_urls(api_version, resource_type, resources):
|
|
188
|
+
type_url = type_urls.get(api_version, {}).get(resource_type)
|
|
189
|
+
if type_url is not None:
|
|
190
|
+
for resource in resources:
|
|
191
|
+
if not resource.get("@type"):
|
|
192
|
+
resource["@type"] = type_url
|
sovereign/response_class.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from importlib.util import find_spec
|
|
2
1
|
from typing import Type
|
|
3
|
-
|
|
2
|
+
from importlib.util import find_spec
|
|
4
3
|
from fastapi.responses import JSONResponse
|
|
5
4
|
|
|
5
|
+
|
|
6
6
|
json_response_class: Type[JSONResponse] = JSONResponse
|
|
7
7
|
if find_spec("orjson"):
|
|
8
8
|
from fastapi.responses import ORJSONResponse
|
sovereign/server.py
CHANGED
|
@@ -1,65 +1,46 @@
|
|
|
1
|
-
import configparser
|
|
2
|
-
import tempfile
|
|
3
1
|
import warnings
|
|
2
|
+
import tempfile
|
|
3
|
+
import configparser
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
import uvicorn
|
|
7
7
|
|
|
8
8
|
from sovereign import application_logger as log
|
|
9
|
-
from sovereign.configuration import SovereignAsgiConfig, SupervisordConfig
|
|
10
|
-
from sovereign.v2.worker import Worker
|
|
9
|
+
from sovereign.configuration import SovereignAsgiConfig, SupervisordConfig
|
|
11
10
|
|
|
12
|
-
# noinspection PyArgumentList
|
|
13
11
|
asgi_config = SovereignAsgiConfig()
|
|
14
|
-
# noinspection PyArgumentList
|
|
15
12
|
supervisord_config = SupervisordConfig()
|
|
16
13
|
|
|
17
14
|
|
|
18
|
-
def web(
|
|
15
|
+
def web() -> None:
|
|
19
16
|
from sovereign.app import app
|
|
20
17
|
|
|
21
18
|
log.debug("Starting web server")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
else:
|
|
34
|
-
uvicorn.run(
|
|
35
|
-
app,
|
|
36
|
-
fd=0,
|
|
37
|
-
log_level=asgi_config.log_level,
|
|
38
|
-
access_log=False,
|
|
39
|
-
timeout_keep_alive=asgi_config.keepalive,
|
|
40
|
-
host=asgi_config.host,
|
|
41
|
-
port=asgi_config.port,
|
|
42
|
-
workers=1, # per managed supervisor proc
|
|
43
|
-
)
|
|
19
|
+
uvicorn.run(
|
|
20
|
+
app,
|
|
21
|
+
fd=0,
|
|
22
|
+
log_level=asgi_config.log_level,
|
|
23
|
+
access_log=False,
|
|
24
|
+
timeout_keep_alive=asgi_config.keepalive,
|
|
25
|
+
host=asgi_config.host,
|
|
26
|
+
port=asgi_config.port,
|
|
27
|
+
workers=1, # per managed supervisor proc
|
|
28
|
+
)
|
|
44
29
|
|
|
45
30
|
|
|
46
31
|
def worker():
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
host="127.0.0.1",
|
|
60
|
-
port=9080,
|
|
61
|
-
workers=1, # per managed supervisor proc
|
|
62
|
-
)
|
|
32
|
+
from sovereign.worker import worker as worker_app
|
|
33
|
+
|
|
34
|
+
log.debug("Starting worker")
|
|
35
|
+
uvicorn.run(
|
|
36
|
+
worker_app,
|
|
37
|
+
log_level=asgi_config.log_level,
|
|
38
|
+
access_log=False,
|
|
39
|
+
timeout_keep_alive=asgi_config.keepalive,
|
|
40
|
+
host="127.0.0.1",
|
|
41
|
+
port=9080,
|
|
42
|
+
workers=1, # per managed supervisor proc
|
|
43
|
+
)
|
|
63
44
|
|
|
64
45
|
|
|
65
46
|
def write_supervisor_conf() -> Path:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
from sovereign.sources.lib import Source
|
|
3
|
+
from sovereign.dynamic_config import Loadable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class File(Source):
|
|
7
|
+
def __init__(self, config: Dict[str, Any], scope: str = "default"):
|
|
8
|
+
super(File, self).__init__(config, scope)
|
|
9
|
+
try:
|
|
10
|
+
self.path = Loadable.from_legacy_fmt(config["path"])
|
|
11
|
+
except KeyError:
|
|
12
|
+
try:
|
|
13
|
+
self.path = Loadable(**config["spec"])
|
|
14
|
+
except KeyError:
|
|
15
|
+
raise KeyError('File source needs to specify "spec" within config')
|
|
16
|
+
|
|
17
|
+
def get(self) -> Any:
|
|
18
|
+
"""
|
|
19
|
+
Uses the file config loader to load the given path
|
|
20
|
+
"""
|
|
21
|
+
return self.path.load()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inline Source
|
|
3
|
+
-------------
|
|
4
|
+
|
|
5
|
+
Example configuration (YAML):
|
|
6
|
+
|
|
7
|
+
.. code-block:: yaml
|
|
8
|
+
|
|
9
|
+
sources:
|
|
10
|
+
- type: inline
|
|
11
|
+
config:
|
|
12
|
+
instances:
|
|
13
|
+
- instance_id: my_service
|
|
14
|
+
service_clusters:
|
|
15
|
+
- P2
|
|
16
|
+
parameters:
|
|
17
|
+
upstream_address:
|
|
18
|
+
- address: service.domain.com
|
|
19
|
+
region: us-east-1
|
|
20
|
+
plan_id: 7d57270a-0348-58d3-829d-447a98fe98d5
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Dict, Any, List
|
|
24
|
+
from sovereign.sources.lib import Source
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Inline(Source):
|
|
28
|
+
def __init__(
|
|
29
|
+
self, config: Dict[str, List[Dict[str, Any]]], scope: str = "default"
|
|
30
|
+
) -> None:
|
|
31
|
+
super(Inline, self).__init__(config, scope)
|
|
32
|
+
try:
|
|
33
|
+
self.instances = config["instances"]
|
|
34
|
+
except KeyError:
|
|
35
|
+
raise KeyError('Inline source config must contain "instances"')
|
|
36
|
+
|
|
37
|
+
def get(self) -> List[Dict[str, Any]]:
|
|
38
|
+
"""Returns the data passed via configuration"""
|
|
39
|
+
return self.instances
|
sovereign/sources/lib.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Source abstract class
|
|
3
|
+
---------------------
|
|
4
|
+
This class can be subclassed, installed as an entry point, and then
|
|
5
|
+
used via configuration.
|
|
6
|
+
|
|
7
|
+
`todo entry point install guide`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import abc
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Source(abc.ABC):
|
|
15
|
+
def __init__(self, config: Dict[str, Any], scope: str) -> None:
|
|
16
|
+
"""
|
|
17
|
+
:param config: arbitrary configuration which can be used by the subclass
|
|
18
|
+
"""
|
|
19
|
+
self.config = config
|
|
20
|
+
self.scope = scope
|
|
21
|
+
|
|
22
|
+
def setup(self) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Optional method which is invoked prior to the Source running self.get()
|
|
25
|
+
"""
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def get(self) -> List[Dict[str, Any]]:
|
|
30
|
+
"""
|
|
31
|
+
Required method to retrieve data from an arbitrary source
|
|
32
|
+
"""
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SourceImplementation(Source):
|
|
37
|
+
def __call__(self, config: Dict[str, Any], scope: str) -> "SourceImplementation":
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def get(self) -> List[Dict[str, Any]]:
|
|
41
|
+
return []
|