sovereign 0.19.3__py3-none-any.whl → 1.0.0a4__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 (99) hide show
  1. sovereign/__init__.py +13 -81
  2. sovereign/app.py +62 -48
  3. sovereign/cache/__init__.py +245 -0
  4. sovereign/cache/backends/__init__.py +110 -0
  5. sovereign/cache/backends/s3.py +161 -0
  6. sovereign/cache/filesystem.py +74 -0
  7. sovereign/cache/types.py +17 -0
  8. sovereign/configuration.py +607 -0
  9. sovereign/constants.py +1 -0
  10. sovereign/context.py +270 -104
  11. sovereign/dynamic_config/__init__.py +112 -0
  12. sovereign/dynamic_config/deser.py +78 -0
  13. sovereign/dynamic_config/loaders.py +120 -0
  14. sovereign/error_info.py +2 -3
  15. sovereign/events.py +49 -0
  16. sovereign/logging/access_logger.py +85 -0
  17. sovereign/logging/application_logger.py +54 -0
  18. sovereign/logging/base_logger.py +41 -0
  19. sovereign/logging/bootstrapper.py +36 -0
  20. sovereign/logging/types.py +10 -0
  21. sovereign/middlewares.py +8 -7
  22. sovereign/modifiers/lib.py +2 -1
  23. sovereign/rendering.py +124 -0
  24. sovereign/rendering_common.py +91 -0
  25. sovereign/response_class.py +18 -0
  26. sovereign/server.py +112 -35
  27. sovereign/statistics.py +19 -21
  28. sovereign/templates/base.html +59 -46
  29. sovereign/templates/resources.html +203 -102
  30. sovereign/testing/loaders.py +9 -0
  31. sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
  32. sovereign/tracing.py +103 -0
  33. sovereign/types.py +304 -0
  34. sovereign/utils/auth.py +27 -13
  35. sovereign/utils/crypto/__init__.py +0 -0
  36. sovereign/utils/crypto/crypto.py +135 -0
  37. sovereign/utils/crypto/suites/__init__.py +21 -0
  38. sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
  39. sovereign/utils/crypto/suites/base_cipher.py +21 -0
  40. sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
  41. sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
  42. sovereign/utils/dictupdate.py +3 -2
  43. sovereign/utils/eds.py +40 -22
  44. sovereign/utils/entry_point_loader.py +2 -2
  45. sovereign/utils/mock.py +56 -17
  46. sovereign/utils/resources.py +17 -0
  47. sovereign/utils/templates.py +4 -2
  48. sovereign/utils/timer.py +5 -3
  49. sovereign/utils/version_info.py +8 -0
  50. sovereign/utils/weighted_clusters.py +2 -1
  51. sovereign/v2/__init__.py +0 -0
  52. sovereign/v2/data/data_store.py +621 -0
  53. sovereign/v2/data/render_discovery_response.py +24 -0
  54. sovereign/v2/data/repositories.py +90 -0
  55. sovereign/v2/data/utils.py +33 -0
  56. sovereign/v2/data/worker_queue.py +273 -0
  57. sovereign/v2/jobs/refresh_context.py +117 -0
  58. sovereign/v2/jobs/render_discovery_job.py +145 -0
  59. sovereign/v2/logging.py +81 -0
  60. sovereign/v2/types.py +41 -0
  61. sovereign/v2/web.py +101 -0
  62. sovereign/v2/worker.py +199 -0
  63. sovereign/views/__init__.py +7 -0
  64. sovereign/views/api.py +82 -0
  65. sovereign/views/crypto.py +46 -15
  66. sovereign/views/discovery.py +55 -119
  67. sovereign/views/healthchecks.py +107 -20
  68. sovereign/views/interface.py +171 -111
  69. sovereign/worker.py +193 -0
  70. {sovereign-0.19.3.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +80 -76
  71. sovereign-1.0.0a4.dist-info/RECORD +85 -0
  72. {sovereign-0.19.3.dist-info → sovereign-1.0.0a4.dist-info}/WHEEL +1 -1
  73. sovereign-1.0.0a4.dist-info/entry_points.txt +46 -0
  74. sovereign_files/__init__.py +0 -0
  75. sovereign_files/static/darkmode.js +51 -0
  76. sovereign_files/static/node_expression.js +42 -0
  77. sovereign_files/static/panel.js +76 -0
  78. sovereign_files/static/resources.css +246 -0
  79. sovereign_files/static/resources.js +642 -0
  80. sovereign_files/static/sass/style.scss +33 -0
  81. sovereign_files/static/style.css +16143 -0
  82. sovereign_files/static/style.css.map +1 -0
  83. sovereign/config_loader.py +0 -225
  84. sovereign/discovery.py +0 -175
  85. sovereign/logs.py +0 -131
  86. sovereign/schemas.py +0 -780
  87. sovereign/sources/__init__.py +0 -3
  88. sovereign/sources/file.py +0 -21
  89. sovereign/sources/inline.py +0 -38
  90. sovereign/sources/lib.py +0 -40
  91. sovereign/sources/poller.py +0 -294
  92. sovereign/static/sass/style.scss +0 -27
  93. sovereign/static/style.css +0 -13553
  94. sovereign/templates/ul_filter.html +0 -22
  95. sovereign/utils/crypto.py +0 -103
  96. sovereign/views/admin.py +0 -120
  97. sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
  98. sovereign-0.19.3.dist-info/RECORD +0 -47
  99. sovereign-0.19.3.dist-info/entry_points.txt +0 -10
@@ -1,63 +1,21 @@
1
- from typing import Dict
2
-
3
1
  from fastapi import Body, Header
4
- from fastapi.routing import APIRouter
5
2
  from fastapi.responses import Response
3
+ from fastapi.routing import APIRouter
6
4
 
7
- from sovereign import discovery, logs, config
8
- from sovereign.utils.auth import authenticate
9
- from sovereign.utils.version_info import compute_hash
10
- from sovereign.schemas import (
5
+ from sovereign import cache, config, logs
6
+ from sovereign.cache.types import Entry
7
+ from sovereign.types import (
11
8
  DiscoveryRequest,
12
9
  DiscoveryResponse,
13
- ProcessedTemplate,
14
10
  )
15
-
16
- discovery_cache = config.discovery_cache
17
-
18
- if discovery_cache.enabled:
19
- from cashews import cache
20
-
21
- cache.setup(
22
- f"{discovery_cache.protocol}{discovery_cache.host}:{discovery_cache.port}",
23
- password=discovery_cache.password.get_secret_value(),
24
- client_side=discovery_cache.client_side,
25
- wait_for_connection_timeout=discovery_cache.wait_for_connection_timeout,
26
- socket_connect_timeout=discovery_cache.socket_connect_timeout,
27
- socket_timeout=discovery_cache.socket_timeout,
28
- max_connections=discovery_cache.max_connections,
29
- retry_on_timeout=discovery_cache.retry_on_timeout,
30
- suppress=discovery_cache.suppress,
31
- socket_keepalive=discovery_cache.socket_keepalive,
32
- enable=discovery_cache.enabled,
33
- )
34
-
35
- router = APIRouter()
36
-
37
- type_urls = {
38
- "v2": {
39
- "listeners": "type.googleapis.com/envoy.api.v2.Listener",
40
- "clusters": "type.googleapis.com/envoy.api.v2.Cluster",
41
- "endpoints": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
42
- "secrets": "type.googleapis.com/envoy.api.v2.auth.Secret",
43
- "routes": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
44
- "scoped-routes": "type.googleapis.com/envoy.api.v2.ScopedRouteConfiguration",
45
- },
46
- "v3": {
47
- "listeners": "type.googleapis.com/envoy.config.listener.v3.Listener",
48
- "clusters": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
49
- "endpoints": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
50
- "secrets": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
51
- "routes": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
52
- "scoped-routes": "type.googleapis.com/envoy.config.route.v3.ScopedRouteConfiguration",
53
- "runtime": "type.googleapis.com/envoy.service.runtime.v3.Runtime",
54
- },
55
- }
11
+ from sovereign.utils.auth import authenticate
12
+ from sovereign.v2.web import wait_for_discovery_response
13
+ from sovereign.views import reader
56
14
 
57
15
 
58
16
  def response_headers(
59
- discovery_request: DiscoveryRequest, response: ProcessedTemplate, xds: str
60
- ) -> Dict[str, str]:
17
+ discovery_request: DiscoveryRequest, response: Entry, xds: str
18
+ ) -> dict[str, str]:
61
19
  return {
62
20
  "X-Sovereign-Client-Build": discovery_request.envoy_version,
63
21
  "X-Sovereign-Client-Version": discovery_request.version_info,
@@ -68,6 +26,9 @@ def response_headers(
68
26
  }
69
27
 
70
28
 
29
+ router = APIRouter()
30
+
31
+
71
32
  @router.post(
72
33
  "/{version}/discovery:{xds_type}",
73
34
  summary="Envoy Discovery Service Endpoint",
@@ -81,76 +42,51 @@ def response_headers(
81
42
  async def discovery_response(
82
43
  version: str,
83
44
  xds_type: str,
84
- discovery_request: DiscoveryRequest = Body(...),
45
+ xds_req: DiscoveryRequest = Body(...),
85
46
  host: str = Header("no_host_provided"),
86
47
  ) -> Response:
87
- discovery_request.desired_controlplane = host
88
- response = await perform_discovery(
89
- discovery_request, version, xds_type, skip_auth=False
90
- )
91
- logs.queue_log_fields(
92
- XDS_RESOURCES=discovery_request.resource_names,
93
- XDS_ENVOY_VERSION=discovery_request.envoy_version,
94
- XDS_CLIENT_VERSION=discovery_request.version_info,
95
- XDS_SERVER_VERSION=response.version,
96
- )
97
- if discovery_request.error_detail:
98
- logs.queue_log_fields(XDS_ERROR_DETAIL=discovery_request.error_detail.message)
99
- headers = response_headers(discovery_request, response, xds_type)
100
-
101
- if response.version == discovery_request.version_info:
102
- return not_modified(headers)
103
- elif getattr(response, "resources", None) == []:
104
- return Response(status_code=404, headers=headers)
105
- elif response.version != discovery_request.version_info:
106
- return Response(
107
- response.rendered, headers=headers, media_type="application/json"
48
+ authenticate(xds_req)
49
+
50
+ # Pack additional info into the request
51
+ xds_req.desired_controlplane = host
52
+ xds_req.resource_type = xds_type
53
+ xds_req.api_version = version
54
+ if xds_req.error_detail:
55
+ logs.access_logger.queue_log_fields(
56
+ XDS_ERROR_DETAIL=xds_req.error_detail.message
108
57
  )
109
- return Response(content="Resources could not be determined", status_code=500)
110
-
58
+ logs.access_logger.queue_log_fields(
59
+ XDS_RESOURCES=xds_req.resource_names,
60
+ XDS_ENVOY_VERSION=xds_req.envoy_version,
61
+ XDS_CLIENT_VERSION=xds_req.version_info,
62
+ )
111
63
 
112
- async def perform_discovery(
113
- req: DiscoveryRequest,
114
- api_version: str,
115
- resource_type: str,
116
- skip_auth: bool = False,
117
- ) -> ProcessedTemplate:
118
- if not skip_auth:
119
- authenticate(req)
120
- if discovery_cache.enabled:
121
- logs.queue_log_fields(CACHE_XDS_HIT=False)
122
- cache_key = compute_hash(
123
- [
124
- api_version,
125
- resource_type,
126
- req.envoy_version,
127
- req.resource_names,
128
- req.desired_controlplane,
129
- req.hide_private_keys,
130
- req.type_url,
131
- req.node.cluster,
132
- req.node.locality,
133
- req.node.metadata.get("auth", None),
134
- req.node.metadata.get("num_cpus", None),
135
- ]
64
+ def handle_response(entry: cache.Entry):
65
+ logs.access_logger.queue_log_fields(
66
+ XDS_SERVER_VERSION=entry.version,
136
67
  )
137
- if template := await cache.get(key=cache_key, default=None):
138
- logs.queue_log_fields(CACHE_XDS_HIT=True)
139
- return template # type: ignore[no-any-return]
140
- template = discovery.response(req, resource_type)
141
- type_url = type_urls.get(api_version, {}).get(resource_type)
142
- if type_url is not None:
143
- for resource in template.resources:
144
- if not resource.get("@type"):
145
- resource["@type"] = type_url
146
- if discovery_cache.enabled:
147
- await cache.set(
148
- key=cache_key,
149
- value=template,
150
- expire=discovery_cache.ttl,
151
- )
152
- return template # type: ignore[no-any-return]
153
-
154
-
155
- def not_modified(headers: Dict[str, str]) -> Response:
156
- return Response(status_code=304, headers=headers)
68
+ headers = response_headers(xds_req, entry, xds_type)
69
+ if entry.len == 0:
70
+ return Response(status_code=404, headers=headers)
71
+ if entry.version == xds_req.version_info:
72
+ return Response(status_code=304, headers=headers)
73
+ return Response(entry.text, media_type="application/json", headers=headers)
74
+
75
+ if config.worker_v2_enabled:
76
+ # we're set up to use v2 of the worker
77
+ response = await wait_for_discovery_response(xds_req)
78
+ if response is not None:
79
+ entry = Entry(
80
+ text=response.model_dump_json(indent=None),
81
+ len=len(response.resources),
82
+ version=response.version_info,
83
+ node=xds_req.node,
84
+ )
85
+ return handle_response(entry)
86
+
87
+ else:
88
+ entry: Entry | None
89
+ if entry := await reader.blocking_read(xds_req): # ty: ignore[possibly-missing-attribute]
90
+ return handle_response(entry)
91
+
92
+ return Response(content="Something went wrong", status_code=500)
@@ -1,38 +1,125 @@
1
- from typing import List
2
- from fastapi import Response
1
+ import asyncio
2
+
3
+ import pydantic
4
+ import requests
5
+ from fastapi import Query, Request, Response
6
+ from fastapi.responses import JSONResponse, PlainTextResponse
3
7
  from fastapi.routing import APIRouter
4
- from fastapi.responses import PlainTextResponse
5
- from sovereign import XDS_TEMPLATES, __version__, json_response_class
6
- from sovereign.utils.mock import mock_discovery_request
7
- from sovereign.views.discovery import perform_discovery
8
+ from typing_extensions import Annotated, Literal
8
9
 
10
+ from sovereign import __version__
11
+ from sovereign.configuration import XDS_TEMPLATES, config
12
+ from sovereign.response_class import json_response_class
13
+ from sovereign.utils.mock import mock_discovery_request
14
+ from sovereign.v2.web import wait_for_discovery_response
15
+ from sovereign.views import reader
9
16
 
10
17
  router = APIRouter()
11
18
 
19
+ State = Literal["FAIL"] | Literal["OK"]
20
+ Message = str
21
+ CheckResult = tuple[State, Message] | State
22
+
23
+
24
+ class DeepCheckResult(pydantic.BaseModel):
25
+ templates: dict[str, CheckResult] = pydantic.Field(default_factory=dict)
26
+ worker: CheckResult = pydantic.Field(default=("FAIL", "Worker unavailable"))
27
+
28
+ def response(self) -> PlainTextResponse:
29
+ return PlainTextResponse(content=self.message, status_code=self.status)
30
+
31
+ def json_response(self) -> JSONResponse:
32
+ return json_response_class(content=self.model_dump(), status_code=self.status)
33
+
34
+ @property
35
+ def message(self) -> str:
36
+ msg = "Templates:\n"
37
+ for template, result in sorted(self.templates.items()):
38
+ msg += f"* {template} {result}\n"
39
+ msg += f"Worker: {self.worker}\n"
40
+ return msg
41
+
42
+ @property
43
+ def status(self) -> int:
44
+ if self.is_err():
45
+ return 500
46
+ return 200
47
+
48
+ def is_err(self):
49
+ for result in self.templates.values():
50
+ if result[0] == "FAIL":
51
+ return True
52
+ if self.worker[0] == "FAIL":
53
+ return True
54
+ return False
55
+
12
56
 
13
57
  @router.get("/healthcheck", summary="Healthcheck (Does the server respond to HTTP?)")
14
58
  async def health_check() -> Response:
15
- return PlainTextResponse("OK")
59
+ return PlainTextResponse("OK", status_code=200)
16
60
 
17
61
 
18
62
  @router.get(
19
63
  "/deepcheck",
20
- summary="Deepcheck (Can the server render all configured templates?)",
21
- response_class=json_response_class,
64
+ summary="Deepcheck (Can the server render all default templates?)",
22
65
  )
23
- async def deep_check(response: Response) -> List[str]:
24
- response.status_code = 200
25
- ret = list()
66
+ async def deep_check(
67
+ request: Request,
68
+ worker_attempts: Annotated[
69
+ int,
70
+ Query(
71
+ description="How many times to try to contact the worker before giving up",
72
+ ),
73
+ ] = 5,
74
+ envoy_service_cluster: Annotated[
75
+ str,
76
+ Query(
77
+ description="Which service cluster to use when checking if a template can be rendered",
78
+ ),
79
+ ] = "*",
80
+ ) -> Response:
81
+ result = DeepCheckResult()
26
82
  for template in list(XDS_TEMPLATES["default"].keys()):
27
- try:
28
- req = mock_discovery_request(service_cluster="*")
29
- await perform_discovery(req, "v3", resource_type=template, skip_auth=True)
30
- # pylint: disable=broad-except
31
- except Exception as e:
32
- ret.append(f"Failed {template}: {str(e)}")
83
+ discovery_request = mock_discovery_request(
84
+ "v3",
85
+ template,
86
+ expressions=[f"cluster={envoy_service_cluster}"],
87
+ )
88
+
89
+ if config.worker_v2_enabled:
90
+ # we're set up to use v2 of the worker
91
+ response = await wait_for_discovery_response(discovery_request)
92
+ if response:
93
+ result.templates[template] = "OK"
94
+ else:
95
+ result.templates[template] = (
96
+ "FAIL",
97
+ f"Failed to render {template}",
98
+ )
99
+
100
+ result.worker = "OK"
33
101
  else:
34
- ret.append(f"Rendered {template} OK")
35
- return ret
102
+ try:
103
+ _ = await reader.blocking_read(discovery_request) # ty: ignore[possibly-missing-attribute]
104
+ result.templates[template] = "OK"
105
+ except Exception as e:
106
+ result.templates[template] = ("FAIL", f"Failed {template}: {str(e)}")
107
+
108
+ if not config.worker_v2_enabled:
109
+ for attempt in range(worker_attempts):
110
+ try:
111
+ worker_health = requests.get("http://localhost:9080/health")
112
+ if worker_health.ok:
113
+ result.worker = "OK"
114
+ break
115
+ except Exception as e:
116
+ result.worker = ("FAIL", str(e))
117
+ await asyncio.sleep(attempt)
118
+
119
+ if "json" in request.headers.get("Accept", ""):
120
+ return result.json_response()
121
+
122
+ return result.response()
36
123
 
37
124
 
38
125
  @router.get("/version", summary="Display the current version of Sovereign")