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.
- sovereign/__init__.py +13 -81
- sovereign/app.py +62 -48
- sovereign/cache/__init__.py +245 -0
- sovereign/cache/backends/__init__.py +110 -0
- sovereign/cache/backends/s3.py +161 -0
- sovereign/cache/filesystem.py +74 -0
- sovereign/cache/types.py +17 -0
- sovereign/configuration.py +607 -0
- sovereign/constants.py +1 -0
- sovereign/context.py +270 -104
- sovereign/dynamic_config/__init__.py +112 -0
- sovereign/dynamic_config/deser.py +78 -0
- sovereign/dynamic_config/loaders.py +120 -0
- sovereign/error_info.py +2 -3
- sovereign/events.py +49 -0
- sovereign/logging/access_logger.py +85 -0
- sovereign/logging/application_logger.py +54 -0
- sovereign/logging/base_logger.py +41 -0
- sovereign/logging/bootstrapper.py +36 -0
- sovereign/logging/types.py +10 -0
- sovereign/middlewares.py +8 -7
- sovereign/modifiers/lib.py +2 -1
- sovereign/rendering.py +124 -0
- sovereign/rendering_common.py +91 -0
- sovereign/response_class.py +18 -0
- sovereign/server.py +112 -35
- sovereign/statistics.py +19 -21
- sovereign/templates/base.html +59 -46
- sovereign/templates/resources.html +203 -102
- sovereign/testing/loaders.py +9 -0
- sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
- sovereign/tracing.py +103 -0
- sovereign/types.py +304 -0
- sovereign/utils/auth.py +27 -13
- sovereign/utils/crypto/__init__.py +0 -0
- sovereign/utils/crypto/crypto.py +135 -0
- sovereign/utils/crypto/suites/__init__.py +21 -0
- sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
- sovereign/utils/crypto/suites/base_cipher.py +21 -0
- sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
- sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
- sovereign/utils/dictupdate.py +3 -2
- sovereign/utils/eds.py +40 -22
- sovereign/utils/entry_point_loader.py +2 -2
- sovereign/utils/mock.py +56 -17
- sovereign/utils/resources.py +17 -0
- sovereign/utils/templates.py +4 -2
- sovereign/utils/timer.py +5 -3
- sovereign/utils/version_info.py +8 -0
- sovereign/utils/weighted_clusters.py +2 -1
- sovereign/v2/__init__.py +0 -0
- sovereign/v2/data/data_store.py +621 -0
- sovereign/v2/data/render_discovery_response.py +24 -0
- sovereign/v2/data/repositories.py +90 -0
- sovereign/v2/data/utils.py +33 -0
- sovereign/v2/data/worker_queue.py +273 -0
- sovereign/v2/jobs/refresh_context.py +117 -0
- sovereign/v2/jobs/render_discovery_job.py +145 -0
- sovereign/v2/logging.py +81 -0
- sovereign/v2/types.py +41 -0
- sovereign/v2/web.py +101 -0
- sovereign/v2/worker.py +199 -0
- sovereign/views/__init__.py +7 -0
- sovereign/views/api.py +82 -0
- sovereign/views/crypto.py +46 -15
- sovereign/views/discovery.py +55 -119
- sovereign/views/healthchecks.py +107 -20
- sovereign/views/interface.py +171 -111
- sovereign/worker.py +193 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +80 -76
- sovereign-1.0.0a4.dist-info/RECORD +85 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0a4.dist-info}/WHEEL +1 -1
- sovereign-1.0.0a4.dist-info/entry_points.txt +46 -0
- sovereign_files/__init__.py +0 -0
- sovereign_files/static/darkmode.js +51 -0
- sovereign_files/static/node_expression.js +42 -0
- sovereign_files/static/panel.js +76 -0
- sovereign_files/static/resources.css +246 -0
- sovereign_files/static/resources.js +642 -0
- sovereign_files/static/sass/style.scss +33 -0
- sovereign_files/static/style.css +16143 -0
- sovereign_files/static/style.css.map +1 -0
- sovereign/config_loader.py +0 -225
- sovereign/discovery.py +0 -175
- sovereign/logs.py +0 -131
- sovereign/schemas.py +0 -780
- sovereign/sources/__init__.py +0 -3
- sovereign/sources/file.py +0 -21
- sovereign/sources/inline.py +0 -38
- sovereign/sources/lib.py +0 -40
- sovereign/sources/poller.py +0 -294
- sovereign/static/sass/style.scss +0 -27
- sovereign/static/style.css +0 -13553
- sovereign/templates/ul_filter.html +0 -22
- sovereign/utils/crypto.py +0 -103
- sovereign/views/admin.py +0 -120
- sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
- sovereign-0.19.3.dist-info/RECORD +0 -47
- sovereign-0.19.3.dist-info/entry_points.txt +0 -10
sovereign/views/interface.py
CHANGED
|
@@ -1,77 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
2
3
|
from collections import defaultdict
|
|
3
|
-
from
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Cookie, Path, Query
|
|
4
7
|
from fastapi.encoders import jsonable_encoder
|
|
5
8
|
from fastapi.requests import Request
|
|
6
|
-
from fastapi.responses import
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from sovereign
|
|
11
|
-
from sovereign.
|
|
9
|
+
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
|
10
|
+
from starlette.templating import Jinja2Templates
|
|
11
|
+
from structlog.typing import FilteringBoundLogger
|
|
12
|
+
|
|
13
|
+
from sovereign import __version__
|
|
14
|
+
from sovereign.cache import Entry
|
|
15
|
+
from sovereign.configuration import XDS_TEMPLATES, ConfiguredResourceTypes, config
|
|
16
|
+
from sovereign.response_class import json_response_class
|
|
17
|
+
from sovereign.utils.mock import NodeExpressionError, mock_discovery_request
|
|
18
|
+
from sovereign.utils.resources import get_package_file
|
|
19
|
+
from sovereign.v2.logging import get_named_logger
|
|
20
|
+
from sovereign.v2.web import wait_for_discovery_response
|
|
21
|
+
from sovereign.views import reader
|
|
12
22
|
|
|
13
23
|
router = APIRouter()
|
|
14
24
|
|
|
15
|
-
all_types = [t.value for t in
|
|
25
|
+
all_types = [t.value for t in ConfiguredResourceTypes]
|
|
26
|
+
|
|
27
|
+
html_templates = Jinja2Templates(
|
|
28
|
+
directory=str(get_package_file("sovereign", "templates"))
|
|
29
|
+
)
|
|
16
30
|
|
|
17
31
|
|
|
18
32
|
@router.get("/")
|
|
19
|
-
|
|
33
|
+
@router.get("/resources")
|
|
34
|
+
async def ui_main(request: Request) -> HTMLResponse:
|
|
20
35
|
try:
|
|
21
36
|
return html_templates.TemplateResponse(
|
|
37
|
+
request=request,
|
|
22
38
|
name="base.html",
|
|
23
39
|
media_type="text/html",
|
|
24
|
-
context={
|
|
25
|
-
"request": request,
|
|
26
|
-
"all_types": all_types,
|
|
27
|
-
"last_update": str(poller.last_updated),
|
|
28
|
-
},
|
|
40
|
+
context={"all_types": all_types, "sovereign_version": __version__},
|
|
29
41
|
)
|
|
30
42
|
except IndexError:
|
|
31
43
|
return html_templates.TemplateResponse(
|
|
44
|
+
request=request,
|
|
32
45
|
name="err.html",
|
|
33
46
|
media_type="text/html",
|
|
34
47
|
context={
|
|
35
|
-
"request": request,
|
|
36
48
|
"title": "No resource types configured",
|
|
37
|
-
"message":
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
"message": (
|
|
50
|
+
"A template should be defined for every resource "
|
|
51
|
+
"type that you want your envoy proxies to discover."
|
|
52
|
+
),
|
|
53
|
+
"doc_link": "https://developer.atlassian.com/platform/sovereign/tutorial/templates/#templates",
|
|
54
|
+
"sovereign_version": __version__,
|
|
40
55
|
},
|
|
41
56
|
)
|
|
42
57
|
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
"/set-version", summary="Filter the UI by a certain Envoy Version (stores a Cookie)"
|
|
46
|
-
)
|
|
47
|
-
async def set_envoy_version(
|
|
48
|
-
request: Request,
|
|
49
|
-
version: str = Query(
|
|
50
|
-
"__any__", title="The clients envoy version to emulate in this XDS request"
|
|
51
|
-
),
|
|
52
|
-
) -> Response:
|
|
53
|
-
url = request.headers.get("Referer", "/ui")
|
|
54
|
-
response = RedirectResponse(url=url)
|
|
55
|
-
response.set_cookie(key="envoy_version", value=version)
|
|
56
|
-
return response
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@router.get(
|
|
60
|
-
"/set-service-cluster",
|
|
61
|
-
summary="Filter the UI by a certain service cluster (stores a Cookie)",
|
|
62
|
-
)
|
|
63
|
-
async def set_service_cluster(
|
|
64
|
-
request: Request,
|
|
65
|
-
service_cluster: str = Query(
|
|
66
|
-
"__any__", title="The clients envoy version to emulate in this XDS request"
|
|
67
|
-
),
|
|
68
|
-
) -> Response:
|
|
69
|
-
url = request.headers.get("Referer", "/ui")
|
|
70
|
-
response = RedirectResponse(url=url)
|
|
71
|
-
response.set_cookie(key="service_cluster", value=service_cluster)
|
|
72
|
-
return response
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
# noinspection DuplicatedCode
|
|
75
60
|
@router.get(
|
|
76
61
|
"/resources/{xds_type}", summary="List available resources for a given xDS type"
|
|
77
62
|
)
|
|
@@ -82,47 +67,84 @@ async def resources(
|
|
|
82
67
|
None, title="The clients region to emulate in this XDS request"
|
|
83
68
|
),
|
|
84
69
|
api_version: str = Query("v2", title="The desired Envoy API version"),
|
|
85
|
-
|
|
86
|
-
"
|
|
70
|
+
node_expression: str = Cookie(
|
|
71
|
+
"cluster=*", title="Node expression to filter resources with"
|
|
87
72
|
),
|
|
88
73
|
envoy_version: str = Cookie(
|
|
89
74
|
"__any__", title="The clients envoy version to emulate in this XDS request"
|
|
90
75
|
),
|
|
91
|
-
|
|
76
|
+
debug: int = Query(0, title="Show debug information on errors"),
|
|
77
|
+
) -> HTMLResponse:
|
|
78
|
+
logger: FilteringBoundLogger = get_named_logger(
|
|
79
|
+
f"{__name__}.{resources.__qualname__} ({__file__})",
|
|
80
|
+
level=logging.DEBUG,
|
|
81
|
+
)
|
|
82
|
+
|
|
92
83
|
ret: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
93
84
|
try:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
),
|
|
101
|
-
api_version=api_version,
|
|
102
|
-
resource_type=xds_type,
|
|
103
|
-
skip_auth=True,
|
|
85
|
+
mock_request = mock_discovery_request(
|
|
86
|
+
api_version,
|
|
87
|
+
xds_type,
|
|
88
|
+
version=envoy_version,
|
|
89
|
+
region=region,
|
|
90
|
+
expressions=node_expression.split(),
|
|
104
91
|
)
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
clear_cookie = False
|
|
93
|
+
error = None
|
|
94
|
+
except NodeExpressionError as e:
|
|
95
|
+
mock_request = mock_discovery_request(
|
|
96
|
+
api_version,
|
|
97
|
+
xds_type,
|
|
98
|
+
version=envoy_version,
|
|
99
|
+
region=region,
|
|
100
|
+
)
|
|
101
|
+
clear_cookie = True
|
|
102
|
+
error = str(e)
|
|
103
|
+
|
|
104
|
+
logger.debug("Making mock request", mock_request=mock_request)
|
|
105
|
+
|
|
106
|
+
entry: Entry | None = None
|
|
107
|
+
|
|
108
|
+
if config.worker_v2_enabled:
|
|
109
|
+
# we're set up to use v2 of the worker
|
|
110
|
+
discovery_response = await wait_for_discovery_response(mock_request)
|
|
111
|
+
if discovery_response is not None:
|
|
112
|
+
entry = Entry(
|
|
113
|
+
text=discovery_response.model_dump_json(indent=None),
|
|
114
|
+
len=len(discovery_response.resources),
|
|
115
|
+
version=discovery_response.version_info,
|
|
116
|
+
node=mock_request.node,
|
|
117
|
+
)
|
|
118
|
+
|
|
107
119
|
else:
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
entry = await reader.blocking_read(mock_request) # ty: ignore[possibly-missing-attribute]
|
|
121
|
+
|
|
122
|
+
if entry:
|
|
123
|
+
ret["resources"] = json.loads(entry.text).get("resources", [])
|
|
124
|
+
|
|
125
|
+
resp = html_templates.TemplateResponse(
|
|
126
|
+
request=request,
|
|
110
127
|
name="resources.html",
|
|
111
128
|
media_type="text/html",
|
|
112
129
|
context={
|
|
130
|
+
"show_debuginfo": True if debug else False,
|
|
131
|
+
"discovery_response": entry,
|
|
132
|
+
"discovery_request": mock_request,
|
|
113
133
|
"resources": ret["resources"],
|
|
114
|
-
"request": request,
|
|
115
134
|
"resource_type": xds_type,
|
|
116
135
|
"all_types": all_types,
|
|
117
136
|
"version": envoy_version,
|
|
118
137
|
"available_versions": list(XDS_TEMPLATES.keys()),
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"last_update": str(poller.last_updated),
|
|
138
|
+
"error": error,
|
|
139
|
+
"sovereign_version": __version__,
|
|
122
140
|
},
|
|
123
141
|
)
|
|
142
|
+
if clear_cookie:
|
|
143
|
+
resp.delete_cookie("node_expression", path="/ui/resources/")
|
|
144
|
+
return resp
|
|
124
145
|
|
|
125
146
|
|
|
147
|
+
# noinspection DuplicatedCode
|
|
126
148
|
@router.get(
|
|
127
149
|
"/resources/{xds_type}/{resource_name}",
|
|
128
150
|
summary="Return JSON representation of a resource",
|
|
@@ -134,25 +156,56 @@ async def resource(
|
|
|
134
156
|
None, title="The clients region to emulate in this XDS request"
|
|
135
157
|
),
|
|
136
158
|
api_version: str = Query("v2", title="The desired Envoy API version"),
|
|
137
|
-
|
|
138
|
-
"
|
|
159
|
+
node_expression: str = Cookie(
|
|
160
|
+
"cluster=*", title="Node expression to filter resources with"
|
|
139
161
|
),
|
|
140
162
|
envoy_version: str = Cookie(
|
|
141
163
|
"__any__", title="The clients envoy version to emulate in this XDS request"
|
|
142
164
|
),
|
|
143
165
|
) -> Response:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
logger: FilteringBoundLogger = get_named_logger(
|
|
167
|
+
f"{__name__}.{resources.__qualname__} ({__file__})",
|
|
168
|
+
level=logging.DEBUG,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
mock_request = mock_discovery_request(
|
|
172
|
+
api_version,
|
|
173
|
+
xds_type,
|
|
174
|
+
version=envoy_version,
|
|
175
|
+
region=region,
|
|
176
|
+
expressions=node_expression.split(),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
logger.debug("Making mock request", mock_request=mock_request)
|
|
180
|
+
|
|
181
|
+
entry: Entry | None = None
|
|
182
|
+
|
|
183
|
+
if config.worker_v2_enabled:
|
|
184
|
+
# we're set up to use v2 of the worker
|
|
185
|
+
discovery_response = await wait_for_discovery_response(mock_request)
|
|
186
|
+
if discovery_response is not None:
|
|
187
|
+
entry = Entry(
|
|
188
|
+
text=discovery_response.model_dump_json(indent=None),
|
|
189
|
+
len=len(discovery_response.resources),
|
|
190
|
+
version=discovery_response.version_info,
|
|
191
|
+
node=mock_request.node,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
else:
|
|
195
|
+
entry = await reader.blocking_read(mock_request) # ty: ignore[possibly-missing-attribute]
|
|
196
|
+
|
|
197
|
+
if entry:
|
|
198
|
+
for res in json.loads(entry.text).get("resources", []):
|
|
199
|
+
if res.get("name", res.get("cluster_name")) == resource_name:
|
|
200
|
+
safe_response = jsonable_encoder(res)
|
|
201
|
+
try:
|
|
202
|
+
return json_response_class(content=safe_response)
|
|
203
|
+
except TypeError:
|
|
204
|
+
return JSONResponse(content=safe_response)
|
|
205
|
+
return Response(
|
|
206
|
+
json.dumps({"title": "No resources found", "status": 404}),
|
|
207
|
+
media_type="application/json+problem",
|
|
154
208
|
)
|
|
155
|
-
return Response(response.rendered, media_type="application/json")
|
|
156
209
|
|
|
157
210
|
|
|
158
211
|
@router.get(
|
|
@@ -166,36 +219,43 @@ async def virtual_hosts(
|
|
|
166
219
|
None, title="The clients region to emulate in this XDS request"
|
|
167
220
|
),
|
|
168
221
|
api_version: str = Query("v2", title="The desired Envoy API version"),
|
|
169
|
-
|
|
170
|
-
"
|
|
222
|
+
node_expression: str = Cookie(
|
|
223
|
+
"cluster=*", title="Node expression to filter resources with"
|
|
171
224
|
),
|
|
172
225
|
envoy_version: str = Cookie(
|
|
173
226
|
"__any__", title="The clients envoy version to emulate in this XDS request"
|
|
174
227
|
),
|
|
175
228
|
) -> Response:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
229
|
+
logger: FilteringBoundLogger = get_named_logger(
|
|
230
|
+
f"{__name__}.{virtual_hosts.__qualname__} ({__file__})",
|
|
231
|
+
level=logging.DEBUG,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
mock_request = mock_discovery_request(
|
|
235
|
+
api_version,
|
|
236
|
+
"routes",
|
|
237
|
+
version=envoy_version,
|
|
238
|
+
region=region,
|
|
239
|
+
expressions=node_expression.split(),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
logger.debug("Making mock request", mock_request=mock_request)
|
|
243
|
+
|
|
244
|
+
if response := await reader.blocking_read(mock_request): # ty: ignore[possibly-missing-attribute]
|
|
245
|
+
route_configs = [
|
|
246
|
+
resource_
|
|
247
|
+
for resource_ in json.loads(response.text).get("resources", [])
|
|
248
|
+
if resource_["name"] == route_configuration
|
|
249
|
+
]
|
|
250
|
+
for route_config in route_configs:
|
|
251
|
+
for vhost in route_config["virtual_hosts"]:
|
|
252
|
+
if vhost["name"] == virtual_host:
|
|
253
|
+
safe_response = jsonable_encoder(vhost)
|
|
254
|
+
try:
|
|
255
|
+
return json_response_class(content=safe_response)
|
|
256
|
+
except TypeError:
|
|
257
|
+
return JSONResponse(content=safe_response)
|
|
258
|
+
return Response(
|
|
259
|
+
json.dumps({"title": "No resources found", "status": 404}),
|
|
260
|
+
media_type="application/json+problem",
|
|
186
261
|
)
|
|
187
|
-
route_configs = [
|
|
188
|
-
resource_
|
|
189
|
-
for resource_ in response.deserialize_resources()
|
|
190
|
-
if resource_["name"] == route_configuration
|
|
191
|
-
]
|
|
192
|
-
for route_config in route_configs:
|
|
193
|
-
for vhost in route_config["virtual_hosts"]:
|
|
194
|
-
if vhost["name"] == virtual_host:
|
|
195
|
-
safe_response = jsonable_encoder(vhost)
|
|
196
|
-
try:
|
|
197
|
-
return json_response_class(content=safe_response)
|
|
198
|
-
except TypeError:
|
|
199
|
-
return JSONResponse(content=safe_response)
|
|
200
|
-
break
|
|
201
|
-
return JSONResponse(content={})
|
sovereign/worker.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import final
|
|
4
|
+
|
|
5
|
+
from fastapi import Body, FastAPI
|
|
6
|
+
|
|
7
|
+
from sovereign import (
|
|
8
|
+
application_logger as log,
|
|
9
|
+
)
|
|
10
|
+
from sovereign import (
|
|
11
|
+
cache,
|
|
12
|
+
disabled_ciphersuite,
|
|
13
|
+
rendering,
|
|
14
|
+
server_cipher_container,
|
|
15
|
+
stats,
|
|
16
|
+
)
|
|
17
|
+
from sovereign.configuration import config
|
|
18
|
+
from sovereign.context import TemplateContext
|
|
19
|
+
from sovereign.events import Topic, bus
|
|
20
|
+
from sovereign.types import DiscoveryRequest, RegisterClientRequest
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# noinspection PyUnusedLocal
|
|
24
|
+
def hidden_field(*args, **kwargs):
|
|
25
|
+
return "(value hidden)"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def inject_builtin_items(request, output):
|
|
29
|
+
output["__hide_from_ui"] = lambda v: v
|
|
30
|
+
output["crypto"] = server_cipher_container
|
|
31
|
+
if request.is_internal_request:
|
|
32
|
+
output["__hide_from_ui"] = hidden_field
|
|
33
|
+
output["crypto"] = disabled_ciphersuite
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
template_context = TemplateContext.from_config()
|
|
37
|
+
context_middleware = [inject_builtin_items]
|
|
38
|
+
template_context.middleware = context_middleware
|
|
39
|
+
writer = cache.CacheWriter()
|
|
40
|
+
|
|
41
|
+
ClientId = str
|
|
42
|
+
OnDemandJob = tuple[ClientId, DiscoveryRequest]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@final
|
|
46
|
+
class RenderQueue:
|
|
47
|
+
def __init__(self, maxsize: int = 0):
|
|
48
|
+
self._queue: asyncio.Queue[OnDemandJob] = asyncio.Queue(maxsize)
|
|
49
|
+
self._set: set[ClientId] = set()
|
|
50
|
+
self._lock = asyncio.Lock()
|
|
51
|
+
|
|
52
|
+
async def put(self, item: OnDemandJob):
|
|
53
|
+
cid = item[0]
|
|
54
|
+
async with self._lock:
|
|
55
|
+
if cid not in self._set:
|
|
56
|
+
await self._queue.put(item)
|
|
57
|
+
self._set.add(cid)
|
|
58
|
+
|
|
59
|
+
def put_nowait(self, item: OnDemandJob):
|
|
60
|
+
cid = item[0]
|
|
61
|
+
if cid in self._set:
|
|
62
|
+
return
|
|
63
|
+
if self._queue.full():
|
|
64
|
+
raise asyncio.QueueFull
|
|
65
|
+
self._queue.put_nowait(item)
|
|
66
|
+
self._set.add(cid)
|
|
67
|
+
|
|
68
|
+
async def get(self):
|
|
69
|
+
return await self._queue.get()
|
|
70
|
+
|
|
71
|
+
def full(self):
|
|
72
|
+
return self._queue.full()
|
|
73
|
+
|
|
74
|
+
async def task_done(self, cid):
|
|
75
|
+
async with self._lock:
|
|
76
|
+
self._set.remove(cid)
|
|
77
|
+
self._queue.task_done()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
ONDEMAND = RenderQueue()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
poller = None
|
|
84
|
+
if config.sources is not None:
|
|
85
|
+
if config.matching is not None:
|
|
86
|
+
matching_enabled = config.matching.enabled
|
|
87
|
+
node_key: str | None = config.matching.node_key
|
|
88
|
+
source_key: str | None = config.matching.source_key
|
|
89
|
+
else:
|
|
90
|
+
matching_enabled = False
|
|
91
|
+
node_key = None
|
|
92
|
+
source_key = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def render_on_event(ctx):
|
|
96
|
+
subscription = bus.subscribe(Topic.CONTEXT)
|
|
97
|
+
while True:
|
|
98
|
+
# block forever until new context arrives
|
|
99
|
+
event = await subscription.get()
|
|
100
|
+
context_name = event.metadata.get("name")
|
|
101
|
+
|
|
102
|
+
log.debug(event.message)
|
|
103
|
+
try:
|
|
104
|
+
if registered := writer.get_registered_clients():
|
|
105
|
+
size = len(registered)
|
|
106
|
+
stats.increment("template.render_on_event", tags=[f"batch_size:{size}"])
|
|
107
|
+
|
|
108
|
+
for client, request in registered:
|
|
109
|
+
if context_name in request.template.depends_on:
|
|
110
|
+
log.info(
|
|
111
|
+
f"Rendering template on-event for {request} because {context_name} was updated"
|
|
112
|
+
)
|
|
113
|
+
job = rendering.RenderJob(
|
|
114
|
+
id=client,
|
|
115
|
+
request=request,
|
|
116
|
+
context=ctx.get_context(request),
|
|
117
|
+
)
|
|
118
|
+
job.submit()
|
|
119
|
+
|
|
120
|
+
finally:
|
|
121
|
+
await asyncio.sleep(config.template_context.cooldown)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def render_on_demand(ctx):
|
|
125
|
+
while True:
|
|
126
|
+
cid, request = await ONDEMAND.get()
|
|
127
|
+
stats.increment("template.render_on_demand")
|
|
128
|
+
log.debug(
|
|
129
|
+
f"Received on-demand request to render templates for {cid} ({request})"
|
|
130
|
+
)
|
|
131
|
+
job = rendering.RenderJob(
|
|
132
|
+
id=cid, request=request, context=ctx.get_context(request)
|
|
133
|
+
)
|
|
134
|
+
_ = job.submit()
|
|
135
|
+
await ONDEMAND.task_done(cid)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# noinspection PyProtectedMember
|
|
139
|
+
async def monitor_render_queue():
|
|
140
|
+
"""Periodically report render queue size metrics"""
|
|
141
|
+
while True:
|
|
142
|
+
await asyncio.sleep(10)
|
|
143
|
+
stats.gauge("template.on_demand_queue_size", ONDEMAND._queue.qsize())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@asynccontextmanager
|
|
147
|
+
async def lifespan(_: FastAPI):
|
|
148
|
+
# Template Rendering
|
|
149
|
+
log.debug("Starting rendering loops")
|
|
150
|
+
asyncio.create_task(render_on_event(template_context))
|
|
151
|
+
asyncio.create_task(render_on_demand(template_context))
|
|
152
|
+
asyncio.create_task(monitor_render_queue())
|
|
153
|
+
|
|
154
|
+
# Template context
|
|
155
|
+
subscription = bus.subscribe(Topic.CONTEXT)
|
|
156
|
+
log.debug("Starting context loop")
|
|
157
|
+
asyncio.create_task(template_context.start())
|
|
158
|
+
event = await subscription.get()
|
|
159
|
+
log.debug(event.message)
|
|
160
|
+
|
|
161
|
+
log.debug("Worker lifespan initialized")
|
|
162
|
+
yield
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
worker = FastAPI(lifespan=lifespan)
|
|
166
|
+
if dsn := config.sentry_dsn.get_secret_value():
|
|
167
|
+
try:
|
|
168
|
+
# noinspection PyUnusedImports
|
|
169
|
+
import sentry_sdk
|
|
170
|
+
|
|
171
|
+
# noinspection PyUnusedImports
|
|
172
|
+
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
|
173
|
+
|
|
174
|
+
sentry_sdk.init(dsn)
|
|
175
|
+
worker.add_middleware(SentryAsgiMiddleware) # type: ignore
|
|
176
|
+
except ImportError: # pragma: no cover
|
|
177
|
+
log.error("Sentry DSN configured but failed to attach to worker")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@worker.get("/health")
|
|
181
|
+
def health():
|
|
182
|
+
return "OK"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@worker.put("/client")
|
|
186
|
+
async def client_add(
|
|
187
|
+
registration: RegisterClientRequest = Body(...),
|
|
188
|
+
):
|
|
189
|
+
log.info(f"Received registration: {registration.request}")
|
|
190
|
+
xds = registration.request
|
|
191
|
+
client_id, req = writer.register(xds)
|
|
192
|
+
ONDEMAND.put_nowait((client_id, req))
|
|
193
|
+
return "Registered", 200
|