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.

Files changed (65) hide show
  1. sovereign/__init__.py +2 -2
  2. sovereign/app.py +3 -6
  3. sovereign/cache/__init__.py +12 -85
  4. sovereign/cache/backends/__init__.py +1 -1
  5. sovereign/cache/backends/s3.py +6 -24
  6. sovereign/cache/filesystem.py +5 -6
  7. sovereign/cache/types.py +0 -2
  8. sovereign/configuration.py +8 -42
  9. sovereign/context.py +9 -8
  10. sovereign/dynamic_config/__init__.py +4 -3
  11. sovereign/dynamic_config/deser.py +1 -1
  12. sovereign/dynamic_config/loaders.py +3 -3
  13. sovereign/error_info.py +3 -2
  14. sovereign/events.py +3 -3
  15. sovereign/logging/access_logger.py +1 -1
  16. sovereign/logging/application_logger.py +1 -1
  17. sovereign/logging/bootstrapper.py +1 -1
  18. sovereign/modifiers/lib.py +1 -1
  19. sovereign/rendering.py +90 -22
  20. sovereign/response_class.py +2 -2
  21. sovereign/server.py +26 -45
  22. sovereign/sources/__init__.py +3 -0
  23. sovereign/sources/file.py +21 -0
  24. sovereign/sources/inline.py +39 -0
  25. sovereign/sources/lib.py +41 -0
  26. sovereign/sources/poller.py +537 -0
  27. sovereign/statistics.py +1 -2
  28. sovereign/testing/loaders.py +0 -1
  29. sovereign/tracing.py +5 -6
  30. sovereign/types.py +10 -15
  31. sovereign/utils/auth.py +2 -3
  32. sovereign/utils/crypto/suites/disabled_cipher.py +2 -2
  33. sovereign/utils/dictupdate.py +1 -1
  34. sovereign/utils/eds.py +1 -3
  35. sovereign/utils/entry_point_loader.py +2 -2
  36. sovereign/utils/mock.py +3 -4
  37. sovereign/utils/resources.py +1 -1
  38. sovereign/utils/templates.py +2 -4
  39. sovereign/utils/timer.py +3 -5
  40. sovereign/utils/weighted_clusters.py +1 -2
  41. sovereign/views/__init__.py +3 -6
  42. sovereign/views/api.py +7 -28
  43. sovereign/views/crypto.py +1 -1
  44. sovereign/views/discovery.py +5 -20
  45. sovereign/views/healthchecks.py +27 -45
  46. sovereign/views/interface.py +10 -70
  47. sovereign/worker.py +31 -20
  48. {sovereign-1.0.0a4.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +3 -4
  49. sovereign-1.0.0b148.dist-info/RECORD +77 -0
  50. {sovereign-1.0.0a4.dist-info → sovereign-1.0.0b148.dist-info}/entry_points.txt +0 -8
  51. sovereign/rendering_common.py +0 -91
  52. sovereign/v2/__init__.py +0 -0
  53. sovereign/v2/data/data_store.py +0 -621
  54. sovereign/v2/data/render_discovery_response.py +0 -24
  55. sovereign/v2/data/repositories.py +0 -90
  56. sovereign/v2/data/utils.py +0 -33
  57. sovereign/v2/data/worker_queue.py +0 -273
  58. sovereign/v2/jobs/refresh_context.py +0 -117
  59. sovereign/v2/jobs/render_discovery_job.py +0 -145
  60. sovereign/v2/logging.py +0 -81
  61. sovereign/v2/types.py +0 -41
  62. sovereign/v2/web.py +0 -101
  63. sovereign/v2/worker.py +0 -199
  64. sovereign-1.0.0a4.dist-info/RECORD +0 -85
  65. {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 Pipe, Process, cpu_count
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
- from sovereign.utils import templates
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
@@ -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, config
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(supervisor_enabled=True) -> None:
15
+ def web() -> None:
19
16
  from sovereign.app import app
20
17
 
21
18
  log.debug("Starting web server")
22
-
23
- if not supervisor_enabled:
24
- uvicorn.run(
25
- app,
26
- log_level=asgi_config.log_level,
27
- access_log=False,
28
- timeout_keep_alive=asgi_config.keepalive,
29
- host=asgi_config.host,
30
- port=asgi_config.port,
31
- workers=1, # per managed supervisor proc
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
- if config.worker_v2_enabled:
48
- log.debug("Starting worker v2")
49
- Worker().start()
50
- else:
51
- from sovereign.worker import worker as worker_app
52
-
53
- log.debug("Starting worker")
54
- uvicorn.run(
55
- worker_app,
56
- log_level=asgi_config.log_level,
57
- access_log=False,
58
- timeout_keep_alive=asgi_config.keepalive,
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,3 @@
1
+ from sovereign.sources.poller import SourcePoller
2
+
3
+ __all__ = ["SourcePoller"]
@@ -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
@@ -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 []