sovereign 1.0.0b123__py3-none-any.whl → 1.0.0b134__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.
Files changed (38) hide show
  1. sovereign/app.py +1 -1
  2. sovereign/cache/__init__.py +182 -0
  3. sovereign/cache/backends/__init__.py +110 -0
  4. sovereign/cache/backends/s3.py +139 -0
  5. sovereign/cache/filesystem.py +42 -0
  6. sovereign/cache/types.py +15 -0
  7. sovereign/context.py +20 -18
  8. sovereign/events.py +49 -0
  9. sovereign/middlewares.py +1 -1
  10. sovereign/rendering.py +74 -35
  11. sovereign/schemas.py +112 -110
  12. sovereign/server.py +4 -3
  13. sovereign/sources/poller.py +20 -4
  14. sovereign/statistics.py +1 -1
  15. sovereign/templates/base.html +59 -46
  16. sovereign/templates/resources.html +40 -835
  17. sovereign/utils/mock.py +7 -3
  18. sovereign/views/healthchecks.py +1 -1
  19. sovereign/views/interface.py +34 -15
  20. sovereign/worker.py +87 -46
  21. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/METADATA +4 -5
  22. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/RECORD +33 -24
  23. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/WHEEL +1 -1
  24. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/entry_points.txt +3 -0
  25. sovereign_files/__init__.py +0 -0
  26. sovereign_files/static/darkmode.js +51 -0
  27. sovereign_files/static/node_expression.js +42 -0
  28. sovereign_files/static/resources.css +246 -0
  29. sovereign_files/static/resources.js +642 -0
  30. sovereign_files/static/sass/style.scss +33 -0
  31. sovereign_files/static/style.css +16143 -0
  32. sovereign_files/static/style.css.map +1 -0
  33. sovereign/cache.py +0 -133
  34. sovereign/static/node_expression.js +0 -16
  35. sovereign/static/sass/style.scss +0 -27
  36. sovereign/static/style.css +0 -13553
  37. sovereign-1.0.0b123.dist-info/LICENSE.txt +0 -13
  38. {sovereign → sovereign_files}/static/panel.js +0 -0
sovereign/utils/mock.py CHANGED
@@ -7,12 +7,16 @@ 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,
13
17
  resource_names: Optional[List[str] | str] = None,
14
18
  region: Optional[str] = "none",
15
- version: Optional[str] = "1.11.1",
19
+ version: Optional[str] = "<envoy_version>",
16
20
  metadata: Optional[Dict[str, str]] = None,
17
21
  error_message: Optional[str] = None,
18
22
  expressions: Optional[list[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]:
@@ -31,7 +31,7 @@ async def deep_check(response: Response) -> List[str]:
31
31
  for template in list(XDS_TEMPLATES["default"].keys()):
32
32
  try:
33
33
  req = mock_discovery_request("v3", template, expressions=["cluster=*"])
34
- cache.read(cache.client_id(req))
34
+ await cache.blocking_read(req)
35
35
  # pylint: disable=broad-except
36
36
  except Exception as e:
37
37
  ret.append(f"Failed {template}: {str(e)}")
@@ -7,10 +7,10 @@ from fastapi.encoders import jsonable_encoder
7
7
  from fastapi.requests import Request
8
8
  from fastapi.responses import HTMLResponse, JSONResponse, Response
9
9
 
10
- from sovereign import html_templates, cache
10
+ from sovereign import html_templates, cache, __version__
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
 
@@ -25,9 +25,7 @@ async def ui_main(request: Request) -> HTMLResponse:
25
25
  request=request,
26
26
  name="base.html",
27
27
  media_type="text/html",
28
- context={
29
- "all_types": all_types,
30
- },
28
+ context={"all_types": all_types, "sovereign_version": __version__},
31
29
  )
32
30
  except IndexError:
33
31
  return html_templates.TemplateResponse(
@@ -36,9 +34,12 @@ async def ui_main(request: Request) -> HTMLResponse:
36
34
  media_type="text/html",
37
35
  context={
38
36
  "title": "No resource types configured",
39
- "message": "A template should be defined for every resource "
40
- "type that you want your envoy proxies to discover.",
37
+ "message": (
38
+ "A template should be defined for every resource "
39
+ "type that you want your envoy proxies to discover."
40
+ ),
41
41
  "doc_link": "https://developer.atlassian.com/platform/sovereign/tutorial/templates/#templates",
42
+ "sovereign_version": __version__,
42
43
  },
43
44
  )
44
45
 
@@ -63,18 +64,31 @@ async def resources(
63
64
  ) -> HTMLResponse:
64
65
  ret: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
65
66
  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
- )
67
+ try:
68
+ mock_request = mock_discovery_request(
69
+ api_version,
70
+ xds_type,
71
+ version=envoy_version,
72
+ region=region,
73
+ expressions=node_expression.split(),
74
+ )
75
+ clear_cookie = False
76
+ error = None
77
+ except NodeExpressionError as e:
78
+ mock_request = mock_discovery_request(
79
+ api_version,
80
+ xds_type,
81
+ version=envoy_version,
82
+ region=region,
83
+ )
84
+ clear_cookie = True
85
+ error = str(e)
86
+
73
87
  response = await cache.blocking_read(mock_request)
74
88
  if response:
75
89
  ret["resources"] = json.loads(response.text).get("resources", [])
76
90
 
77
- return html_templates.TemplateResponse(
91
+ resp = html_templates.TemplateResponse(
78
92
  request=request,
79
93
  name="resources.html",
80
94
  media_type="text/html",
@@ -87,8 +101,13 @@ async def resources(
87
101
  "all_types": all_types,
88
102
  "version": envoy_version,
89
103
  "available_versions": list(XDS_TEMPLATES.keys()),
104
+ "error": error,
105
+ "sovereign_version": __version__,
90
106
  },
91
107
  )
108
+ if clear_cookie:
109
+ resp.delete_cookie("node_expression", path="/ui/resources/")
110
+ return resp
92
111
 
93
112
 
94
113
  @router.get(
sovereign/worker.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
- from typing import Optional
3
- from multiprocessing import Process, cpu_count
2
+ from typing import Optional, final
4
3
  from contextlib import asynccontextmanager
5
4
 
6
5
  from fastapi import FastAPI, Body
@@ -17,12 +16,50 @@ from sovereign import (
17
16
  )
18
17
  from sovereign.sources import SourcePoller
19
18
  from sovereign.schemas import RegisterClientRequest, DiscoveryRequest
20
- from sovereign.context import NEW_CONTEXT
19
+ from sovereign.events import bus, Topic
21
20
 
22
21
 
23
22
  ClientId = str
24
- ONDEMAND: asyncio.Queue[tuple[ClientId, DiscoveryRequest]] = asyncio.Queue(100)
25
- RENDER_SEMAPHORE = asyncio.Semaphore(cpu_count())
23
+ OnDemandJob = tuple[ClientId, DiscoveryRequest]
24
+
25
+
26
+ @final
27
+ class RenderQueue:
28
+ def __init__(self, maxsize: int = 0):
29
+ self._queue: asyncio.Queue[OnDemandJob] = asyncio.Queue(maxsize)
30
+ self._set: set[ClientId] = set()
31
+ self._lock = asyncio.Lock()
32
+
33
+ async def put(self, item: OnDemandJob):
34
+ id_ = item[0]
35
+ async with self._lock:
36
+ if id_ not in self._set:
37
+ await self._queue.put(item)
38
+ self._set.add(id_)
39
+
40
+ def put_nowait(self, item: OnDemandJob):
41
+ id_ = item[0]
42
+ if id_ in self._set:
43
+ return
44
+ if self._queue.full():
45
+ raise asyncio.QueueFull
46
+ self._queue.put_nowait(item)
47
+ self._set.add(id_)
48
+
49
+ async def get(self):
50
+ item = await self._queue.get()
51
+ async with self._lock:
52
+ self._set.remove(item[0])
53
+ return item
54
+
55
+ def full(self):
56
+ return self._queue.full()
57
+
58
+ def task_done(self):
59
+ self._queue.task_done()
60
+
61
+
62
+ ONDEMAND = RenderQueue()
26
63
 
27
64
 
28
65
  def hidden_field(*args, **kwargs):
@@ -60,70 +97,69 @@ if config.sources is not None:
60
97
  context_middleware.append(poller.add_to_context)
61
98
 
62
99
 
63
- def render(job: rendering.RenderJob):
64
- log.debug(f"Spawning render process for {job.id}")
65
- process = Process(target=rendering.generate, args=[job])
66
- process.start()
67
- return process
68
-
69
-
70
- async def submit_render(job: rendering.RenderJob):
71
- async with RENDER_SEMAPHORE:
72
- process = render(job)
73
- # Wait for the process to complete to ensure semaphore is held
74
- # until the actual rendering work is done
75
- await asyncio.get_event_loop().run_in_executor(None, process.join)
76
-
77
-
78
100
  async def render_on_event():
101
+ subscription = bus.subscribe(Topic.CONTEXT)
79
102
  while True:
80
103
  # block forever until new context arrives
81
- await NEW_CONTEXT.wait()
82
- log.debug("New context detected, re-rendering templates")
104
+ event = await subscription.get()
105
+ context_name = event.metadata.get("name")
106
+
107
+ log.debug(event.message)
83
108
  try:
84
109
  if registered := cache.clients():
85
- log.debug("New context detected, re-rendering templates")
86
- jobs = [
87
- rendering.RenderJob(
88
- id=client,
89
- request=request,
90
- context=template_context.get_context(request),
91
- )
92
- for client, request in registered
93
- ]
94
- tasks = [submit_render(job) for job in jobs]
95
- size = len(tasks)
110
+ size = len(registered)
96
111
  stats.increment("template.render_on_event", tags=[f"batch_size:{size}"])
97
- await asyncio.gather(*tasks)
98
- log.debug(f"Completed rendering {size} jobs")
112
+
113
+ for client, request in registered:
114
+ if context_name in request.template.depends_on:
115
+ log.debug(f"Rendering template for {request}")
116
+ job = rendering.RenderJob(
117
+ id=client,
118
+ request=request,
119
+ context=template_context.get_context(request),
120
+ )
121
+ job.submit()
122
+
99
123
  finally:
100
- NEW_CONTEXT.clear()
124
+ await asyncio.sleep(config.template_context.cooldown)
101
125
 
102
126
 
103
127
  async def render_on_demand():
104
128
  while True:
105
129
  id, request = await ONDEMAND.get()
106
130
  stats.increment("template.render_on_demand")
107
- log.debug("Received on-demand request to render templates")
131
+ log.debug(
132
+ f"Received on-demand request to render templates for {id} ({request})"
133
+ )
108
134
  job = rendering.RenderJob(
109
135
  id=id, request=request, context=template_context.get_context(request)
110
136
  )
111
- await submit_render(job)
137
+ job.submit()
112
138
  ONDEMAND.task_done()
113
139
 
114
140
 
141
+ async def monitor_render_queue():
142
+ """Periodically report render queue size metrics"""
143
+ while True:
144
+ await asyncio.sleep(10)
145
+ stats.gauge("template.on_demand_queue_size", ONDEMAND._queue.qsize())
146
+
147
+
115
148
  @asynccontextmanager
116
149
  async def lifespan(_: FastAPI):
117
150
  # Template Rendering
118
151
  log.debug("Starting rendering loops")
119
152
  asyncio.create_task(render_on_event())
120
153
  asyncio.create_task(render_on_demand())
154
+ asyncio.create_task(monitor_render_queue())
121
155
 
122
156
  # Template context
157
+ subscription = bus.subscribe(Topic.CONTEXT)
123
158
  log.debug("Starting context loop")
124
159
  template_context.middleware = context_middleware
125
160
  asyncio.create_task(template_context.start())
126
- await NEW_CONTEXT.wait() # first refresh finished
161
+ event = await subscription.get()
162
+ log.debug(event.message)
127
163
 
128
164
  # Source polling
129
165
  if poller is not None:
@@ -156,12 +192,17 @@ async def client_add(
156
192
  registration: RegisterClientRequest = Body(...),
157
193
  ):
158
194
  xds = registration.request
159
- if not cache.registered(xds):
160
- log.debug(f"Received registration for new client {xds}")
161
- ONDEMAND.put_nowait(await cache.register(xds))
162
- stats.increment("client.registration", tags=["status:registered"])
163
- return "Registering", 202
164
- else:
165
- log.debug("Client already registered")
195
+ if cache.registered(xds):
196
+ log.debug(f"Client already registered {xds=}")
166
197
  stats.increment("client.registration", tags=["status:exists"])
167
198
  return "Registered", 200
199
+ else:
200
+ id, req = cache.register(xds)
201
+ log.debug(f"Received registration for new client {xds}, {id=}")
202
+ try:
203
+ ONDEMAND.put_nowait((id, req))
204
+ except asyncio.QueueFull:
205
+ stats.increment("client.registration", tags=["status:queue_full"])
206
+ return "Slow down :(", 429
207
+ stats.increment("client.registration", tags=["status:registered"])
208
+ return "Registering", 202
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: sovereign
3
- Version: 1.0.0b123
3
+ Version: 1.0.0b134
4
4
  Summary: Envoy Proxy control-plane written in Python
5
- Home-page: https://pypi.org/project/sovereign/
6
5
  License: Apache-2.0
7
6
  Keywords: envoy,envoyproxy,control-plane,management,server
8
7
  Author: Vasili Syrakis
@@ -20,6 +19,7 @@ Classifier: Programming Language :: Python :: 3
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Programming Language :: Python :: 3.10
24
24
  Classifier: Programming Language :: Python :: 3.8
25
25
  Classifier: Programming Language :: Python :: 3.9
@@ -37,7 +37,6 @@ Requires-Dist: aiofiles (>=23.2.1,<24.0.0)
37
37
  Requires-Dist: boto3 (>=1.28.62,<2.0.0) ; extra == "boto"
38
38
  Requires-Dist: cachelib (>=0.10.2,<0.11.0)
39
39
  Requires-Dist: cachetools (>=5.3.2,<6.0.0)
40
- Requires-Dist: cashews[redis] (>=6.3.0,<7.0.0) ; extra == "caching"
41
40
  Requires-Dist: croniter (>=1.4.1,<2.0.0)
42
41
  Requires-Dist: cryptography (>=45.0.2)
43
42
  Requires-Dist: datadog (>=0.50.1) ; extra == "statsd"
@@ -49,7 +48,6 @@ Requires-Dist: jmespath (>=1.0.1,<2.0.0)
49
48
  Requires-Dist: orjson (>=3.9.15,<4.0.0) ; extra == "orjson"
50
49
  Requires-Dist: pydantic (>=2.7.2,<3.0.0)
51
50
  Requires-Dist: pydantic-settings (<2.6.0)
52
- Requires-Dist: redis (<=5.0.0)
53
51
  Requires-Dist: requests (>=2.32.4,<3.0.0)
54
52
  Requires-Dist: sentry-sdk (>=2.14.0,<3.0.0) ; extra == "sentry"
55
53
  Requires-Dist: starlette (>=0.47.2,<0.48.0)
@@ -60,6 +58,7 @@ Requires-Dist: ujson (>=5.8.0,<6.0.0) ; extra == "ujson"
60
58
  Requires-Dist: uvicorn (>=0.23.2,<0.24.0)
61
59
  Requires-Dist: uvloop (>=0.19.0,<0.20.0)
62
60
  Project-URL: Documentation, https://developer.atlassian.com/platform/sovereign/
61
+ Project-URL: Homepage, https://pypi.org/project/sovereign/
63
62
  Project-URL: Repository, https://bitbucket.org/atlassian/sovereign/src/master/
64
63
  Description-Content-Type: text/markdown
65
64
 
@@ -1,37 +1,38 @@
1
1
  sovereign/__init__.py,sha256=m8MVzaMSW4AvAqHvUAsXFdp8Oas5oQ8X7BcVt7Hfcik,1431
2
- sovereign/app.py,sha256=fsf4Jgni2G4EYJO0oQSWfGRZVDBvE2Yfick4n2YR6K4,4876
3
- sovereign/cache.py,sha256=P2LAskGxZSFgQ392aKz7auHty57HJNepQ7xl2-iWbBA,3842
2
+ sovereign/app.py,sha256=LUsy4NgOddTxyC35lqezyDTG-_HMJZ5Q4b1GRv5NpgM,4882
3
+ sovereign/cache/__init__.py,sha256=963_hdG4_HV8Kj-LYj7HM0VlL1sJF7F06dCPQdiHv4k,6228
4
+ sovereign/cache/backends/__init__.py,sha256=6mPpf9OzyCBvRqpZzIBNy6jrDimEmg8yIiUYpNVTkQk,3160
5
+ sovereign/cache/backends/s3.py,sha256=y8j-YGHWMWet_DGEH3dBtkolLHhgGvSS0nCtRuPbIb8,4991
6
+ sovereign/cache/filesystem.py,sha256=83hyaRVTcueLgghgE50E88azkF4hTyIhzii9Plw1KmQ,1354
7
+ sovereign/cache/types.py,sha256=RXLv5fTlDOOvkkycyYuDbq4WL7X7KaEslp-Thbz7o2k,243
4
8
  sovereign/constants.py,sha256=qdWD1lTvkaW5JGF7TmZhfksQHlRAJFVqbG7v6JQA9k8,46
5
- sovereign/context.py,sha256=aoGJ5k1n8ytCk7jggQ6XTx6Hx_ocy7cEkvGOFS3gzzc,8912
9
+ sovereign/context.py,sha256=hnohbF4R-L3Kve5JKWHo9UfA4WGBCtwfq5m6xgMT8ak,8788
6
10
  sovereign/dynamic_config/__init__.py,sha256=0hrI9Y-FzDywEM9Lu6i2mPFhs1c47C096R1B_-E3sKA,3161
7
11
  sovereign/dynamic_config/deser.py,sha256=N3iUvDpuNHWjxUbGFydMVKicx4o8DyfvNukorqnQdt8,1834
8
12
  sovereign/dynamic_config/loaders.py,sha256=gPkxTL7gep20HIMRvjgOqAdUWqtb3970VBCAcUrIM4c,2915
9
13
  sovereign/error_info.py,sha256=r2KXBYq9Fo7AI2pmIpATWFm0pykr2MqfrKH0WWW5Sfk,1488
14
+ sovereign/events.py,sha256=8WEIJo0C7mnx-pLjOTXPUFWYySvVXqDsj6p2e9LR9Jg,1203
10
15
  sovereign/logging/access_logger.py,sha256=G-R6kSPDQlrunSh34qXIT3LwbamAhdASuPgPOaXCRdM,2983
11
16
  sovereign/logging/application_logger.py,sha256=HjrGTi2zZ06AaToDVdSv4MNIF6aWN6vFW5heAdfqwlk,1800
12
17
  sovereign/logging/base_logger.py,sha256=ScOzHs8Rt1RZaUZGvaJSAlDEjD0BxkD5sLKSm2GgM0I,1243
13
18
  sovereign/logging/bootstrapper.py,sha256=gWFzIVsfeMdv7-d2Z6Fiw7J0xcuZzc4z2F4Iqn1KG30,1296
14
19
  sovereign/logging/types.py,sha256=rGqJAEVvgvzHy4aPfvEH6yQ-yblXNkEcWG7G8l9ALEA,282
15
- sovereign/middlewares.py,sha256=tQazHAtIdUc1hWhopg33x83-g-JcilU4HdjzoxFe6NU,3053
20
+ sovereign/middlewares.py,sha256=6w4JpvtNGvQA4rocQsYQjuu-ckhpKT6gKYA16T-kiqA,3082
16
21
  sovereign/modifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
22
  sovereign/modifiers/lib.py,sha256=Cx0VrpTKbSjb3YmHyG4Jy6YEaPlrwpeqNaom3zu1_hw,2885
18
- sovereign/rendering.py,sha256=MIA7se7-C4WTWf7xZSgqpf7NvhDT7NkZbR3_G9N1dHI,5015
23
+ sovereign/rendering.py,sha256=NmdUUrOZMffFOGNmZ4AhrBe1BxVsCeITrL1Hds6Iv0k,6546
19
24
  sovereign/response_class.py,sha256=beMAFV-4L6DwyWzJzy71GkEW4gb7fzH1jd8-Tul13cU,427
20
- sovereign/schemas.py,sha256=Jf_w2mAX1b31eCAtJuxSZgy9KMVRKntmnEbbCZGZCxQ,38067
21
- sovereign/server.py,sha256=uviXN0mX-Wbhv36xjWlHiybzIlZrJsCwRf4xyRnibXQ,2970
25
+ sovereign/schemas.py,sha256=iYb9l1ZUy2YLwQ41cyqM9qXyqPl40dyIIAuqk9cxMYA,37565
26
+ sovereign/server.py,sha256=GAuBj73fpfZH760dDgVZFfI_2aKj3QHFv1JaVm4Eqow,2979
22
27
  sovereign/sources/__init__.py,sha256=g9hEpFk8j5i1ApHQpbc9giTyJW41Ppgsqv5P9zGxOJk,78
23
28
  sovereign/sources/file.py,sha256=prUThsDCSPNwZaZpkKXhAm-GVRZWbBoGKGU0It4HHXs,690
24
29
  sovereign/sources/inline.py,sha256=pP77m7bHjqE3sSoqZthcuw1ARVMf9gooVwbz4B8OAek,1003
25
30
  sovereign/sources/lib.py,sha256=0hk_G6mKJrB65WokVZnqF5kdJ3vsQZMNPuJqJO0mBsI,1031
26
- sovereign/sources/poller.py,sha256=zpNUhQft-NoJbbxO1kCFp6jJSRSkBmf181xodnF_TiI,18469
27
- sovereign/static/node_expression.js,sha256=dL9QLM49jorqavf3Qtye6E1QTWYDT1rFI0tQR1HsiLQ,504
28
- sovereign/static/panel.js,sha256=i5mGExjv-I4Gtt9dQiTyFwPZa8pg5rXeuTeidXNUiTE,2695
29
- sovereign/static/sass/style.scss,sha256=tPHPEm3sZeBFGDyyn3pHcA-nbaKT-h-UsSTsf6dHNDU,1158
30
- sovereign/static/style.css,sha256=vG8HPsbCbPIZfHgy7gSeof97Pnp0okkyaXyJzIEEW-8,447517
31
- sovereign/statistics.py,sha256=QhDB0bs5kZDGjy248AOIv_bzNbz_c2U7xmJ0hoUNOmw,2033
32
- sovereign/templates/base.html,sha256=5vw3-NmN291pXRdArpCwhSce9bAYBWCJVRhvO5EmE9g,2296
31
+ sovereign/sources/poller.py,sha256=RPAyCKJ16k-195ugnR_UM6UtzpHOqvkzbpyY759WRLc,18994
32
+ sovereign/statistics.py,sha256=xyWY_VEyIHzSmptPzLDx7kmP4J3roZkJX_CHA6RZlHo,2038
33
+ sovereign/templates/base.html,sha256=MMhhvvClTixKibYfhXm8Ezx6ttu6Sqki44niciCPMO4,2990
33
34
  sovereign/templates/err.html,sha256=a3cEzOqyqWOIe3YxfTEjkxbTfxBxq1knD6GwzEFljfs,603
34
- sovereign/templates/resources.html,sha256=QaZ1S38JhAZg3-PfQS1cAKhCczVLXw9e4pztBrqr4qs,40217
35
+ sovereign/templates/resources.html,sha256=5MfXHW8s3tAWda66Q48zVgDhZNLwHGsdCKkKHLZohIs,10420
35
36
  sovereign/testing/loaders.py,sha256=mcmErhI9ZkJUBZl8jv2qP-PCBRFeAIgyBFlfCgU4Vvk,199
36
37
  sovereign/testing/modifiers.py,sha256=7_c2hWXn_sYJ6997N1_uSWtClOikcOzu1yRCY56-l-4,361
37
38
  sovereign/tracing.py,sha256=Xo3npgh6yesACSlynv9j6qnXxvYEBzXv5LL4Zkc1QDw,2446
@@ -47,7 +48,7 @@ sovereign/utils/crypto/suites/fernet_cipher.py,sha256=rP6M5ys1vctyadOxDGNFoyerWP
47
48
  sovereign/utils/dictupdate.py,sha256=Bi7QaC7en-k3EOepwNJqpOKRNBgp6ZsBZVOvH_0nMtc,2558
48
49
  sovereign/utils/eds.py,sha256=sCEDj1y-0Crs40cHZLiPGVb7ed1f8vFqgHLY5R2LMbw,4377
49
50
  sovereign/utils/entry_point_loader.py,sha256=BEVodk-um70RvT1nSOu_IB-hr1K4ppthXod0VZEiZJ8,526
50
- sovereign/utils/mock.py,sha256=VgaxArNnTi0z6cyodrcjiSNdQn4sFOcYG_VQYhvsisI,2308
51
+ sovereign/utils/mock.py,sha256=UyfURjsR0RVU7Xj_fiagI3IpcZjlEwaY7mx0iuxGK-k,2397
51
52
  sovereign/utils/resources.py,sha256=rPrWgcIt4YhV-Dz88_kr5WrQNiSKt-jTlOZ8EIJxJx8,472
52
53
  sovereign/utils/templates.py,sha256=FE_H_oE7VrS3X_VN1z_g10b9-rpmi1_gL-cMxi5XtXU,1057
53
54
  sovereign/utils/timer.py,sha256=_dUtEasj0BKbWYuQ_T3HFIyjurXXj-La-dNSMAwKMSo,795
@@ -57,11 +58,19 @@ sovereign/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
58
  sovereign/views/api.py,sha256=jKVjSvi0Tr1HHix3hc0H8yGZoyDind2sJ4w7a4pJvy0,2168
58
59
  sovereign/views/crypto.py,sha256=7y0eHWtt-bbr2CwHEkH7odPaJ1IEviU-71U-MYJD0Kc,3360
59
60
  sovereign/views/discovery.py,sha256=B_D1ckfbN1dSKBvuFCTyfB79GUUriCADTB53OwZ8D4Q,2409
60
- sovereign/views/healthchecks.py,sha256=TaXbxkX679jyQ8v5FxtBa2Qa0Z7KuqQ10WgAqfuVGUc,1743
61
- sovereign/views/interface.py,sha256=FmQ7LiUPLSvkEDOKCncrnKMD9g1lJKu-DQNbbyi8mqk,6346
62
- sovereign/worker.py,sha256=sOVHVVv63I08_KIdaKR0hUnYLMXwoqh9jmsCw8albZE,5230
63
- sovereign-1.0.0b123.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
- sovereign-1.0.0b123.dist-info/METADATA,sha256=ZiGaNA-XWo08b2EghMAbSDPRhKeu3_s8ls2o-pXqJNU,6304
65
- sovereign-1.0.0b123.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
- sovereign-1.0.0b123.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
- sovereign-1.0.0b123.dist-info/RECORD,,
61
+ sovereign/views/healthchecks.py,sha256=ByJORGPxkaq0P2Mb1uy6qciwH0qZpCaThXEdRaHwrws,1741
62
+ sovereign/views/interface.py,sha256=o6DaOqoh2M09_SsZrCOxr9rCVxMUZHXRXj4TNq800Ho,6999
63
+ sovereign/worker.py,sha256=HD1otdAxgPiQrkyy_FNmCZDG_Qb9XOpiPQk_-KRP4Uk,6162
64
+ sovereign_files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
+ sovereign_files/static/darkmode.js,sha256=3ip-eKGctDvNhN7UgmaHhzls7r5qIY-Jvh2EpefHbQ0,1449
66
+ sovereign_files/static/node_expression.js,sha256=GKxKTSRc_96IbL3H4L_31ueJFXq4N7scm5R1RNqxP24,1489
67
+ sovereign_files/static/panel.js,sha256=i5mGExjv-I4Gtt9dQiTyFwPZa8pg5rXeuTeidXNUiTE,2695
68
+ sovereign_files/static/resources.css,sha256=Rt_ir_FkoI-VIAOqPhk0vILy8kB2egAYbQU26SOs1io,4500
69
+ sovereign_files/static/resources.js,sha256=-TaXZ6tohyKA1SkX5YwrTcV5M8mOZ68cvEXpvZWznTo,24506
70
+ sovereign_files/static/sass/style.scss,sha256=LdGXXuHi_tyMc7XhijIOrlIxyfLt827AAs2Z7DYpFpg,990
71
+ sovereign_files/static/style.css,sha256=kmvkJ2820RKehWxhddkucbgFkvnpUgBMteOtpEuXjvQ,601347
72
+ sovereign_files/static/style.css.map,sha256=h1ufjfDVX-8z-FuJqFG2-U9AVdi66U-e8uyiGdUZjDw,66576
73
+ sovereign-1.0.0b134.dist-info/METADATA,sha256=y-8pqkfWk5pLl0lm07XUzKl4PxmWOTMD63DRXgw1ghI,6268
74
+ sovereign-1.0.0b134.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
75
+ sovereign-1.0.0b134.dist-info/entry_points.txt,sha256=BhOMKka7D5WOnfEB_XWWFDNlejKOB4zax-HGu-_uL9k,1473
76
+ sovereign-1.0.0b134.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -3,6 +3,9 @@ sovereign=sovereign.server:main
3
3
  sovereign-web=sovereign.server:web
4
4
  sovereign-worker=sovereign.server:worker
5
5
 
6
+ [sovereign.cache.backends]
7
+ s3=sovereign.cache.backends.s3:S3Backend
8
+
6
9
  [sovereign.deserializers]
7
10
  jinja=sovereign.dynamic_config.deser:JinjaDeserializer
8
11
  jinja2=sovereign.dynamic_config.deser:JinjaDeserializer
File without changes
@@ -0,0 +1,51 @@
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ const darkmode = "theme-dark";
3
+ const lightmode = "theme-light";
4
+ const toggle = document.getElementById('dark-mode-toggle');
5
+ const htmlTag = document.documentElement;
6
+
7
+ function preferredTheme() {
8
+ const preference = localStorage.getItem("theme");
9
+ if (preference) {
10
+ return preference;
11
+ }
12
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
13
+ return "dark";
14
+ } else {
15
+ return "light";
16
+ };
17
+ }
18
+
19
+ function currentTheme() {
20
+ if (htmlTag.classList.contains(darkmode)) {
21
+ return "dark"
22
+ } else {
23
+ return "light"
24
+ }
25
+ }
26
+
27
+ function setTheme(theme) {
28
+ localStorage.setItem("theme", theme);
29
+ if (theme === "dark") {
30
+ htmlTag.classList.remove(lightmode);
31
+ htmlTag.classList.add(darkmode);
32
+ toggle.textContent = '🌘';
33
+ } else {
34
+ htmlTag.classList.remove(darkmode);
35
+ htmlTag.classList.add(lightmode);
36
+ toggle.textContent = '🌞';
37
+ }
38
+ }
39
+
40
+ setTheme(preferredTheme());
41
+
42
+ toggle.addEventListener("click", function() {
43
+ let current = currentTheme();
44
+ console.log("Current theme: " + current);
45
+ if (current === "dark") {
46
+ setTheme("light");
47
+ } else {
48
+ setTheme("dark");
49
+ }
50
+ });
51
+ });
@@ -0,0 +1,42 @@
1
+ const input = document.getElementById('filterInput');
2
+ const inputMessage = document.getElementById('filterMessage');
3
+ const form = document.getElementById('filterForm');
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
+
13
+ window.addEventListener('DOMContentLoaded', () => {
14
+ const match = document.cookie.match(/(?:^|; )node_expression=([^;]*)/);
15
+ if (match) {
16
+ input.value = match[1];
17
+ }
18
+ });
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.className = "help is-dark";
25
+ inputMessage.innerHTML = "";
26
+ } else if (result === true) {
27
+ input.className = "input is-success";
28
+ inputMessage.className = "help is-success";
29
+ inputMessage.innerHTML = "Press enter to apply filter expression";
30
+ } else {
31
+ input.className = "input is-danger";
32
+ inputMessage.className = "help is-danger";
33
+ inputMessage.innerHTML = "The node filter expression may have no effect, or be invalid";
34
+ }
35
+ });
36
+
37
+ form.addEventListener('submit', (event) => {
38
+ event.preventDefault();
39
+ const value = input.value.trim();
40
+ document.cookie = `node_expression=${value}; path=/ui/resources/; max-age=31536000`;
41
+ location.reload();
42
+ });