sovereign 1.0.0b125__py3-none-any.whl → 1.0.0b126__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/rendering.py CHANGED
@@ -7,6 +7,8 @@ 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 threading
11
+ from multiprocessing import Process, Semaphore, cpu_count
10
12
  from typing import Any, Dict, List
11
13
 
12
14
  import yaml
@@ -27,6 +29,8 @@ from sovereign.schemas import (
27
29
  ProcessedTemplate,
28
30
  )
29
31
 
32
+ # limit render jobs to number of cores
33
+ RENDER_SEMAPHORE = Semaphore(cpu_count())
30
34
 
31
35
  type_urls = {
32
36
  "v2": {
@@ -54,39 +58,52 @@ class RenderJob(pydantic.BaseModel):
54
58
  request: DiscoveryRequest
55
59
  context: dict[str, Any]
56
60
 
61
+ def spawn(self):
62
+ t = threading.Thread(target=self._run)
63
+ t.start()
64
+
65
+ def _run(self):
66
+ with RENDER_SEMAPHORE:
67
+ proc = Process(target=generate, args=[self])
68
+ proc.start()
69
+ proc.join()
70
+
57
71
 
58
72
  def generate(job: RenderJob) -> None:
59
73
  request = job.request
60
74
  tags = [f"type:{request.resource_type}"]
61
- stats.increment("template.render", tags=tags)
62
- with stats.timed("template.render_ms", tags=tags):
63
- content = request.template(
64
- discovery_request=request,
65
- host_header=request.desired_controlplane,
66
- resource_names=request.resources,
67
- **job.context,
68
- )
69
- if not request.template.is_python_source:
70
- assert isinstance(content, str)
71
- content = deserialize_config(content)
72
- assert isinstance(content, dict)
73
- resources = filter_resources(content["resources"], request.resources)
74
- add_type_urls(request.api_version, request.resource_type, resources)
75
- response = ProcessedTemplate(resources=resources)
76
- cache.write(
77
- job.id,
78
- cache.Entry(
79
- text=response.model_dump_json(indent=None),
80
- len=len(response.resources),
81
- version=response.version_info,
82
- node=request.node,
83
- ),
84
- )
85
-
86
-
87
- def batch_generate(jobs: list[RenderJob]) -> None:
88
- for job in jobs:
89
- generate(job)
75
+ try:
76
+ with stats.timed("template.render_ms", tags=tags):
77
+ content = request.template(
78
+ discovery_request=request,
79
+ host_header=request.desired_controlplane,
80
+ resource_names=request.resources,
81
+ **job.context,
82
+ )
83
+ if not request.template.is_python_source:
84
+ assert isinstance(content, str)
85
+ content = deserialize_config(content)
86
+ assert isinstance(content, dict)
87
+ resources = filter_resources(content["resources"], request.resources)
88
+ add_type_urls(request.api_version, request.resource_type, resources)
89
+ response = ProcessedTemplate(resources=resources)
90
+ cache.write(
91
+ job.id,
92
+ cache.Entry(
93
+ text=response.model_dump_json(indent=None),
94
+ len=len(response.resources),
95
+ version=response.version_info,
96
+ node=request.node,
97
+ ),
98
+ )
99
+ tags.append("result:ok")
100
+ except Exception as e:
101
+ tags.append("result:err")
102
+ tags.append(f"error:{e.__class__.__name__.lower()}")
103
+ if SENTRY_INSTALLED and config.sentry_dsn:
104
+ sentry_sdk.capture_exception(e)
105
+ finally:
106
+ stats.increment("template.render", tags=tags)
90
107
 
91
108
 
92
109
  def deserialize_config(content: str) -> Dict[str, Any]:
@@ -1,6 +1,15 @@
1
1
  const input = document.getElementById('filterInput');
2
+ const inputMessage = document.getElementById('filterMessage');
2
3
  const form = document.getElementById('filterForm');
3
4
 
5
+ function validateInput(inputString) {
6
+ if (!inputString || inputString.trim() === '') {
7
+ return "empty";
8
+ }
9
+ const validationRegex = /^(?:(?:id|cluster|metadata\.[\w\.\=\-]+|locality\.?(?:zone|sub_zone|region))=[a-zA-Z0-9_-]+ ?)*$/;
10
+ return validationRegex.test(inputString);
11
+ }
12
+
4
13
  window.addEventListener('DOMContentLoaded', () => {
5
14
  const match = document.cookie.match(/(?:^|; )node_expression=([^;]*)/);
6
15
  if (match) {
@@ -8,6 +17,20 @@ window.addEventListener('DOMContentLoaded', () => {
8
17
  }
9
18
  });
10
19
 
20
+ input.addEventListener('input', (event) => {
21
+ const result = validateInput(event.target.value);
22
+ if (result === "empty") {
23
+ input.className = "input is-dark";
24
+ inputMessage.innerHTML = "";
25
+ } else if (result === true) {
26
+ input.className = "input is-success";
27
+ inputMessage.innerHTML = "";
28
+ } else {
29
+ input.className = "input is-danger";
30
+ inputMessage.innerHTML = "The node filter expression may have no effect, or be invalid";
31
+ }
32
+ });
33
+
11
34
  form.addEventListener('submit', (event) => {
12
35
  event.preventDefault();
13
36
  const value = input.value.trim();
@@ -271,6 +271,14 @@
271
271
 
272
272
  {%- block body %}
273
273
  <div class="content">
274
+ {% if error %}
275
+ <span class="panel-icon">
276
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
277
+ </span>
278
+ <div class="notification is-danger">
279
+ {{ error }}
280
+ </div>
281
+ {% endif %}
274
282
  <p class="content">
275
283
  <div class="columns">
276
284
  <div class="column is-narrow">
@@ -303,6 +311,7 @@
303
311
  <form id="filterForm">
304
312
  <input id="filterInput" class="input is-dark" type="text" placeholder="Node filter expression"/>
305
313
  </form>
314
+ <p id="filterMessage" class="help is-danger"></p>
306
315
  </div>
307
316
  <div class="column is-narrow">
308
317
  <div class="tooltip">
@@ -322,6 +331,7 @@
322
331
  </div>
323
332
  </div>
324
333
 
334
+
325
335
  {% set count = resources|length %}
326
336
  {% if count > 0 %}
327
337
  <nav class="panel is-dark" id="resources">
sovereign/utils/mock.py CHANGED
@@ -7,6 +7,10 @@ from sovereign.schemas import DiscoveryRequest, Node, Locality, Status
7
7
  scrub = re.compile(r"[^a-zA-Z_\.]")
8
8
 
9
9
 
10
+ class NodeExpressionError(Exception):
11
+ pass
12
+
13
+
10
14
  def mock_discovery_request(
11
15
  api_version: Optional[str] = "V3",
12
16
  resource_type: Optional[str] = None,
@@ -54,7 +58,7 @@ def set_node_expressions(node, expressions):
54
58
  field, value = re.split(r"\s*=\s*", expr, maxsplit=1)
55
59
  value = f'"{value}"'
56
60
  except ValueError:
57
- raise ValueError(f"Invalid expression format: {expr}")
61
+ raise NodeExpressionError(f"Invalid node filter format: {expr}")
58
62
 
59
63
  field = scrub.sub("", field)
60
64
  parts = field.split(".")
@@ -62,7 +66,7 @@ def set_node_expressions(node, expressions):
62
66
  try:
63
67
  value = ast.literal_eval(value)
64
68
  except Exception as e:
65
- raise ValueError(f"invalid value: {value}") from e
69
+ raise NodeExpressionError(f"Invalid node filter value: {value}") from e
66
70
 
67
71
  current = node
68
72
  for part in parts[:-1]:
@@ -10,7 +10,7 @@ from fastapi.responses import HTMLResponse, JSONResponse, Response
10
10
  from sovereign import html_templates, cache
11
11
  from sovereign.schemas import DiscoveryTypes, XDS_TEMPLATES
12
12
  from sovereign.response_class import json_response_class
13
- from sovereign.utils.mock import mock_discovery_request
13
+ from sovereign.utils.mock import NodeExpressionError, mock_discovery_request
14
14
 
15
15
  router = APIRouter()
16
16
 
@@ -63,18 +63,31 @@ async def resources(
63
63
  ) -> HTMLResponse:
64
64
  ret: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
65
65
  response = None
66
- mock_request = mock_discovery_request(
67
- api_version,
68
- xds_type,
69
- version=envoy_version,
70
- region=region,
71
- expressions=node_expression.split(),
72
- )
66
+ try:
67
+ mock_request = mock_discovery_request(
68
+ api_version,
69
+ xds_type,
70
+ version=envoy_version,
71
+ region=region,
72
+ expressions=node_expression.split(),
73
+ )
74
+ clear_cookie = False
75
+ error = None
76
+ except NodeExpressionError as e:
77
+ mock_request = mock_discovery_request(
78
+ api_version,
79
+ xds_type,
80
+ version=envoy_version,
81
+ region=region,
82
+ )
83
+ clear_cookie = True
84
+ error = str(e)
85
+
73
86
  response = await cache.blocking_read(mock_request)
74
87
  if response:
75
88
  ret["resources"] = json.loads(response.text).get("resources", [])
76
89
 
77
- return html_templates.TemplateResponse(
90
+ resp = html_templates.TemplateResponse(
78
91
  request=request,
79
92
  name="resources.html",
80
93
  media_type="text/html",
@@ -87,8 +100,12 @@ async def resources(
87
100
  "all_types": all_types,
88
101
  "version": envoy_version,
89
102
  "available_versions": list(XDS_TEMPLATES.keys()),
103
+ "error": error,
90
104
  },
91
105
  )
106
+ if clear_cookie:
107
+ resp.delete_cookie("node_expression", path="/ui/resources/")
108
+ return resp
92
109
 
93
110
 
94
111
  @router.get(
sovereign/worker.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  from typing import Optional, final
3
- from multiprocessing import Process, cpu_count
4
3
  from contextlib import asynccontextmanager
5
4
 
6
5
  from fastapi import FastAPI, Body
@@ -61,7 +60,6 @@ class RenderQueue:
61
60
 
62
61
 
63
62
  ONDEMAND = RenderQueue()
64
- RENDER_SEMAPHORE = asyncio.Semaphore(cpu_count())
65
63
 
66
64
 
67
65
  def hidden_field(*args, **kwargs):
@@ -99,42 +97,23 @@ if config.sources is not None:
99
97
  context_middleware.append(poller.add_to_context)
100
98
 
101
99
 
102
- def render(job: rendering.RenderJob):
103
- log.debug(f"Spawning render process for {job.id}")
104
- process = Process(target=rendering.generate, args=[job])
105
- process.start()
106
- return process
107
-
108
-
109
- async def submit_render(job: rendering.RenderJob):
110
- async with RENDER_SEMAPHORE:
111
- process = render(job)
112
- # Wait for the process to complete to ensure semaphore is held
113
- # until the actual rendering work is done
114
- await asyncio.get_event_loop().run_in_executor(None, process.join)
115
-
116
-
117
100
  async def render_on_event():
118
101
  while True:
119
102
  # block forever until new context arrives
120
- await NEW_CONTEXT.wait()
103
+ _ = await NEW_CONTEXT.wait()
121
104
  log.debug("New context detected, re-rendering templates")
122
105
  try:
123
106
  if registered := cache.clients():
124
107
  log.debug("New context detected, re-rendering templates")
125
- jobs = [
126
- rendering.RenderJob(
108
+ size = len(registered)
109
+ stats.increment("template.render_on_event", tags=[f"batch_size:{size}"])
110
+ for client, request in registered:
111
+ job = rendering.RenderJob(
127
112
  id=client,
128
113
  request=request,
129
114
  context=template_context.get_context(request),
130
115
  )
131
- for client, request in registered
132
- ]
133
- tasks = [submit_render(job) for job in jobs]
134
- size = len(tasks)
135
- stats.increment("template.render_on_event", tags=[f"batch_size:{size}"])
136
- await asyncio.gather(*tasks)
137
- log.debug(f"Completed rendering {size} jobs")
116
+ job.spawn()
138
117
  finally:
139
118
  NEW_CONTEXT.clear()
140
119
 
@@ -147,16 +126,24 @@ async def render_on_demand():
147
126
  job = rendering.RenderJob(
148
127
  id=id, request=request, context=template_context.get_context(request)
149
128
  )
150
- await submit_render(job)
129
+ job.spawn()
151
130
  ONDEMAND.task_done()
152
131
 
153
132
 
133
+ async def monitor_render_queue():
134
+ """Periodically report render queue size metrics"""
135
+ while True:
136
+ await asyncio.sleep(10)
137
+ stats.gauge("template.on_demand_queue_size", ONDEMAND._queue.qsize())
138
+
139
+
154
140
  @asynccontextmanager
155
141
  async def lifespan(_: FastAPI):
156
142
  # Template Rendering
157
143
  log.debug("Starting rendering loops")
158
144
  asyncio.create_task(render_on_event())
159
145
  asyncio.create_task(render_on_demand())
146
+ asyncio.create_task(monitor_render_queue())
160
147
 
161
148
  # Template context
162
149
  log.debug("Starting context loop")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 1.0.0b125
3
+ Version: 1.0.0b126
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
@@ -15,7 +15,7 @@ sovereign/logging/types.py,sha256=rGqJAEVvgvzHy4aPfvEH6yQ-yblXNkEcWG7G8l9ALEA,28
15
15
  sovereign/middlewares.py,sha256=6w4JpvtNGvQA4rocQsYQjuu-ckhpKT6gKYA16T-kiqA,3082
16
16
  sovereign/modifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  sovereign/modifiers/lib.py,sha256=Cx0VrpTKbSjb3YmHyG4Jy6YEaPlrwpeqNaom3zu1_hw,2885
18
- sovereign/rendering.py,sha256=MIA7se7-C4WTWf7xZSgqpf7NvhDT7NkZbR3_G9N1dHI,5015
18
+ sovereign/rendering.py,sha256=hxlu_K7h1fVNYRLVA4-YM_KY4rhaEOzHq-4dCVwgdWE,5688
19
19
  sovereign/response_class.py,sha256=beMAFV-4L6DwyWzJzy71GkEW4gb7fzH1jd8-Tul13cU,427
20
20
  sovereign/schemas.py,sha256=Jf_w2mAX1b31eCAtJuxSZgy9KMVRKntmnEbbCZGZCxQ,38067
21
21
  sovereign/server.py,sha256=uviXN0mX-Wbhv36xjWlHiybzIlZrJsCwRf4xyRnibXQ,2970
@@ -24,14 +24,14 @@ sovereign/sources/file.py,sha256=prUThsDCSPNwZaZpkKXhAm-GVRZWbBoGKGU0It4HHXs,690
24
24
  sovereign/sources/inline.py,sha256=pP77m7bHjqE3sSoqZthcuw1ARVMf9gooVwbz4B8OAek,1003
25
25
  sovereign/sources/lib.py,sha256=0hk_G6mKJrB65WokVZnqF5kdJ3vsQZMNPuJqJO0mBsI,1031
26
26
  sovereign/sources/poller.py,sha256=zpNUhQft-NoJbbxO1kCFp6jJSRSkBmf181xodnF_TiI,18469
27
- sovereign/static/node_expression.js,sha256=dL9QLM49jorqavf3Qtye6E1QTWYDT1rFI0tQR1HsiLQ,504
27
+ sovereign/static/node_expression.js,sha256=TXN9TAyutktR8IUrLbtaqupigKQ-_AhBoulS4KKrEjI,1311
28
28
  sovereign/static/panel.js,sha256=i5mGExjv-I4Gtt9dQiTyFwPZa8pg5rXeuTeidXNUiTE,2695
29
29
  sovereign/static/sass/style.scss,sha256=tPHPEm3sZeBFGDyyn3pHcA-nbaKT-h-UsSTsf6dHNDU,1158
30
30
  sovereign/static/style.css,sha256=vG8HPsbCbPIZfHgy7gSeof97Pnp0okkyaXyJzIEEW-8,447517
31
31
  sovereign/statistics.py,sha256=QhDB0bs5kZDGjy248AOIv_bzNbz_c2U7xmJ0hoUNOmw,2033
32
32
  sovereign/templates/base.html,sha256=5vw3-NmN291pXRdArpCwhSce9bAYBWCJVRhvO5EmE9g,2296
33
33
  sovereign/templates/err.html,sha256=a3cEzOqyqWOIe3YxfTEjkxbTfxBxq1knD6GwzEFljfs,603
34
- sovereign/templates/resources.html,sha256=QaZ1S38JhAZg3-PfQS1cAKhCczVLXw9e4pztBrqr4qs,40217
34
+ sovereign/templates/resources.html,sha256=dDjpUHgZ13Su8sCG2Jxz0EJ_hrc3ei8SGqMVx7MvXX0,40527
35
35
  sovereign/testing/loaders.py,sha256=mcmErhI9ZkJUBZl8jv2qP-PCBRFeAIgyBFlfCgU4Vvk,199
36
36
  sovereign/testing/modifiers.py,sha256=7_c2hWXn_sYJ6997N1_uSWtClOikcOzu1yRCY56-l-4,361
37
37
  sovereign/tracing.py,sha256=Xo3npgh6yesACSlynv9j6qnXxvYEBzXv5LL4Zkc1QDw,2446
@@ -47,7 +47,7 @@ sovereign/utils/crypto/suites/fernet_cipher.py,sha256=rP6M5ys1vctyadOxDGNFoyerWP
47
47
  sovereign/utils/dictupdate.py,sha256=Bi7QaC7en-k3EOepwNJqpOKRNBgp6ZsBZVOvH_0nMtc,2558
48
48
  sovereign/utils/eds.py,sha256=sCEDj1y-0Crs40cHZLiPGVb7ed1f8vFqgHLY5R2LMbw,4377
49
49
  sovereign/utils/entry_point_loader.py,sha256=BEVodk-um70RvT1nSOu_IB-hr1K4ppthXod0VZEiZJ8,526
50
- sovereign/utils/mock.py,sha256=VgaxArNnTi0z6cyodrcjiSNdQn4sFOcYG_VQYhvsisI,2308
50
+ sovereign/utils/mock.py,sha256=j9zaLT39MSuF7vGKvhGRJtrXSTU7WFB1rirUWWBGYm4,2388
51
51
  sovereign/utils/resources.py,sha256=rPrWgcIt4YhV-Dz88_kr5WrQNiSKt-jTlOZ8EIJxJx8,472
52
52
  sovereign/utils/templates.py,sha256=FE_H_oE7VrS3X_VN1z_g10b9-rpmi1_gL-cMxi5XtXU,1057
53
53
  sovereign/utils/timer.py,sha256=_dUtEasj0BKbWYuQ_T3HFIyjurXXj-La-dNSMAwKMSo,795
@@ -58,10 +58,10 @@ sovereign/views/api.py,sha256=jKVjSvi0Tr1HHix3hc0H8yGZoyDind2sJ4w7a4pJvy0,2168
58
58
  sovereign/views/crypto.py,sha256=7y0eHWtt-bbr2CwHEkH7odPaJ1IEviU-71U-MYJD0Kc,3360
59
59
  sovereign/views/discovery.py,sha256=B_D1ckfbN1dSKBvuFCTyfB79GUUriCADTB53OwZ8D4Q,2409
60
60
  sovereign/views/healthchecks.py,sha256=TaXbxkX679jyQ8v5FxtBa2Qa0Z7KuqQ10WgAqfuVGUc,1743
61
- sovereign/views/interface.py,sha256=FmQ7LiUPLSvkEDOKCncrnKMD9g1lJKu-DQNbbyi8mqk,6346
62
- sovereign/worker.py,sha256=iMeUQfN6hFoarqteG0eYpzVDj_Izknpe6QnVY-l7U6U,6373
63
- sovereign-1.0.0b125.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
- sovereign-1.0.0b125.dist-info/METADATA,sha256=lE6OWxPME5zhTc3YNQhJbVPxYq4C2rVBQSZJc5JTxlw,6304
65
- sovereign-1.0.0b125.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
- sovereign-1.0.0b125.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
- sovereign-1.0.0b125.dist-info/RECORD,,
61
+ sovereign/views/interface.py,sha256=KQKf9qOaVc_zWOFQ1WXEJKlTEhdQO4fQhIu5c_vCdPw,6843
62
+ sovereign/worker.py,sha256=Y4le54cZU1Enj8scu1G1sM_KPVPUmZ7vmTuxpd59X2Q,5855
63
+ sovereign-1.0.0b126.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
+ sovereign-1.0.0b126.dist-info/METADATA,sha256=wxIU-o096f2VZKm70ZrYp24K7Yr_RrD0evUr7aBcKbY,6304
65
+ sovereign-1.0.0b126.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
+ sovereign-1.0.0b126.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
+ sovereign-1.0.0b126.dist-info/RECORD,,