sovereign 0.14.2__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 +17 -78
  2. sovereign/app.py +74 -59
  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 +271 -100
  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 +61 -0
  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 +123 -28
  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 +18 -0
  45. sovereign/utils/mock.py +60 -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 +52 -67
  67. sovereign/views/healthchecks.py +107 -20
  68. sovereign/views/interface.py +173 -117
  69. sovereign/worker.py +193 -0
  70. {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +81 -73
  71. sovereign-1.0.0a4.dist-info/RECORD +85 -0
  72. {sovereign-0.14.2.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 -715
  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 -298
  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 -64
  96. sovereign/views/admin.py +0 -120
  97. sovereign-0.14.2.dist-info/LICENSE.txt +0 -13
  98. sovereign-0.14.2.dist-info/RECORD +0 -45
  99. sovereign-0.14.2.dist-info/entry_points.txt +0 -10
@@ -1,44 +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
8
- from sovereign.utils.auth import authenticate
9
- from sovereign.schemas import (
5
+ from sovereign import cache, config, logs
6
+ from sovereign.cache.types import Entry
7
+ from sovereign.types import (
10
8
  DiscoveryRequest,
11
9
  DiscoveryResponse,
12
- ProcessedTemplate,
13
10
  )
14
-
15
-
16
- router = APIRouter()
17
-
18
- type_urls = {
19
- "v2": {
20
- "listeners": "type.googleapis.com/envoy.api.v2.Listener",
21
- "clusters": "type.googleapis.com/envoy.api.v2.Cluster",
22
- "endpoints": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
23
- "secrets": "type.googleapis.com/envoy.api.v2.auth.Secret",
24
- "routes": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
25
- "scoped-routes": "type.googleapis.com/envoy.api.v2.ScopedRouteConfiguration",
26
- },
27
- "v3": {
28
- "listeners": "type.googleapis.com/envoy.config.listener.v3.Listener",
29
- "clusters": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
30
- "endpoints": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
31
- "secrets": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
32
- "routes": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
33
- "scoped-routes": "type.googleapis.com/envoy.config.route.v3.ScopedRouteConfiguration",
34
- "runtime": "type.googleapis.com/envoy.service.runtime.v3.Runtime",
35
- },
36
- }
11
+ from sovereign.utils.auth import authenticate
12
+ from sovereign.v2.web import wait_for_discovery_response
13
+ from sovereign.views import reader
37
14
 
38
15
 
39
16
  def response_headers(
40
- discovery_request: DiscoveryRequest, response: ProcessedTemplate, xds: str
41
- ) -> Dict[str, str]:
17
+ discovery_request: DiscoveryRequest, response: Entry, xds: str
18
+ ) -> dict[str, str]:
42
19
  return {
43
20
  "X-Sovereign-Client-Build": discovery_request.envoy_version,
44
21
  "X-Sovereign-Client-Version": discovery_request.version_info,
@@ -49,6 +26,9 @@ def response_headers(
49
26
  }
50
27
 
51
28
 
29
+ router = APIRouter()
30
+
31
+
52
32
  @router.post(
53
33
  "/{version}/discovery:{xds_type}",
54
34
  summary="Envoy Discovery Service Endpoint",
@@ -62,46 +42,51 @@ def response_headers(
62
42
  async def discovery_response(
63
43
  version: str,
64
44
  xds_type: str,
65
- discovery_request: DiscoveryRequest = Body(...),
45
+ xds_req: DiscoveryRequest = Body(...),
66
46
  host: str = Header("no_host_provided"),
67
47
  ) -> Response:
68
- discovery_request.desired_controlplane = host
69
- response = perform_discovery(discovery_request, version, xds_type, skip_auth=False)
70
- logs.queue_log_fields(
71
- XDS_RESOURCES=discovery_request.resource_names,
72
- XDS_ENVOY_VERSION=discovery_request.envoy_version,
73
- XDS_CLIENT_VERSION=discovery_request.version_info,
74
- XDS_SERVER_VERSION=response.version,
75
- )
76
- headers = response_headers(discovery_request, response, xds_type)
48
+ authenticate(xds_req)
77
49
 
78
- if response.version == discovery_request.version_info:
79
- return not_modified(headers)
80
- elif getattr(response, "resources", None) == []:
81
- return Response(status_code=404, headers=headers)
82
- elif response.version != discovery_request.version_info:
83
- return Response(
84
- response.rendered, headers=headers, media_type="application/json"
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
85
57
  )
86
- return Response(content="Resources could not be determined", status_code=500)
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
+ )
87
63
 
64
+ def handle_response(entry: cache.Entry):
65
+ logs.access_logger.queue_log_fields(
66
+ XDS_SERVER_VERSION=entry.version,
67
+ )
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)
88
74
 
89
- def perform_discovery(
90
- req: DiscoveryRequest,
91
- api_version: str,
92
- resource_type: str,
93
- skip_auth: bool = False,
94
- ) -> ProcessedTemplate:
95
- if not skip_auth:
96
- authenticate(req)
97
- template = discovery.response(req, resource_type)
98
- type_url = type_urls.get(api_version, {}).get(resource_type)
99
- if type_url is not None:
100
- for resource in template.resources:
101
- if not resource.get("@type"):
102
- resource["@type"] = type_url
103
- return template
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)
104
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)
105
91
 
106
- def not_modified(headers: Dict[str, str]) -> Response:
107
- return Response(status_code=304, headers=headers)
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
- 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")
@@ -1,162 +1,211 @@
1
- from typing import List, Dict, Any
1
+ import json
2
+ import logging
2
3
  from collections import defaultdict
3
- from fastapi import APIRouter, Query, Path, Cookie
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 RedirectResponse, JSONResponse, Response
7
- from sovereign import html_templates, XDS_TEMPLATES
8
- from sovereign.discovery import DiscoveryTypes
9
- from sovereign import poller, json_response_class
10
- from sovereign.utils.mock import mock_discovery_request
11
- from sovereign.views.discovery import perform_discovery
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 DiscoveryTypes]
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
- async def ui_main(request: Request) -> Response:
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": "A template should be defined for every resource "
38
- "type that you want your envoy proxies to discover.",
39
- "doc_link": "https://vsyrakis.bitbucket.io/sovereign/docs/tutorial/templates/",
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
- @router.get(
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
  )
78
63
  async def resources(
79
64
  request: Request,
80
- xds_type: str = Path(
81
- "clusters", title="xDS type", description="The type of request"
82
- ),
65
+ xds_type: str = Path(title="xDS type", description="The type of request"),
83
66
  region: str = Query(
84
67
  None, title="The clients region to emulate in this XDS request"
85
68
  ),
86
69
  api_version: str = Query("v2", title="The desired Envoy API version"),
87
- service_cluster: str = Cookie(
88
- "*", title="The clients service cluster to emulate in this XDS request"
70
+ node_expression: str = Cookie(
71
+ "cluster=*", title="Node expression to filter resources with"
89
72
  ),
90
73
  envoy_version: str = Cookie(
91
74
  "__any__", title="The clients envoy version to emulate in this XDS request"
92
75
  ),
93
- ) -> Response:
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
+
94
83
  ret: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
95
84
  try:
96
- response = perform_discovery(
97
- req=mock_discovery_request(
98
- service_cluster=service_cluster,
99
- resource_names=[],
100
- version=envoy_version,
101
- region=region,
102
- ),
103
- api_version=api_version,
104
- resource_type=xds_type,
105
- 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(),
106
91
  )
107
- except KeyError:
108
- ret["resources"] = []
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
+
109
119
  else:
110
- ret["resources"] += response.deserialize_resources()
111
- return html_templates.TemplateResponse(
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,
112
127
  name="resources.html",
113
128
  media_type="text/html",
114
129
  context={
130
+ "show_debuginfo": True if debug else False,
131
+ "discovery_response": entry,
132
+ "discovery_request": mock_request,
115
133
  "resources": ret["resources"],
116
- "request": request,
117
134
  "resource_type": xds_type,
118
135
  "all_types": all_types,
119
136
  "version": envoy_version,
120
137
  "available_versions": list(XDS_TEMPLATES.keys()),
121
- "service_cluster": service_cluster,
122
- "available_service_clusters": poller.match_keys,
123
- "last_update": str(poller.last_updated),
138
+ "error": error,
139
+ "sovereign_version": __version__,
124
140
  },
125
141
  )
142
+ if clear_cookie:
143
+ resp.delete_cookie("node_expression", path="/ui/resources/")
144
+ return resp
126
145
 
127
146
 
147
+ # noinspection DuplicatedCode
128
148
  @router.get(
129
149
  "/resources/{xds_type}/{resource_name}",
130
150
  summary="Return JSON representation of a resource",
131
151
  )
132
152
  async def resource(
133
- xds_type: str = Path(
134
- "clusters", title="xDS type", description="The type of request"
135
- ),
153
+ xds_type: str = Path(title="xDS type", description="The type of request"),
136
154
  resource_name: str = Path(..., title="Name of the resource to view"),
137
155
  region: str = Query(
138
156
  None, title="The clients region to emulate in this XDS request"
139
157
  ),
140
158
  api_version: str = Query("v2", title="The desired Envoy API version"),
141
- service_cluster: str = Cookie(
142
- "*", title="The clients service cluster to emulate in this XDS request"
159
+ node_expression: str = Cookie(
160
+ "cluster=*", title="Node expression to filter resources with"
143
161
  ),
144
162
  envoy_version: str = Cookie(
145
163
  "__any__", title="The clients envoy version to emulate in this XDS request"
146
164
  ),
147
165
  ) -> Response:
148
- response = perform_discovery(
149
- req=mock_discovery_request(
150
- service_cluster=service_cluster,
151
- resource_names=[resource_name],
152
- version=envoy_version,
153
- region=region,
154
- ),
155
- api_version=api_version,
156
- resource_type=xds_type,
157
- skip_auth=True,
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",
158
208
  )
159
- return Response(response.rendered, media_type="application/json")
160
209
 
161
210
 
162
211
  @router.get(
@@ -170,36 +219,43 @@ async def virtual_hosts(
170
219
  None, title="The clients region to emulate in this XDS request"
171
220
  ),
172
221
  api_version: str = Query("v2", title="The desired Envoy API version"),
173
- service_cluster: str = Cookie(
174
- "*", title="The clients service cluster to emulate in this XDS request"
222
+ node_expression: str = Cookie(
223
+ "cluster=*", title="Node expression to filter resources with"
175
224
  ),
176
225
  envoy_version: str = Cookie(
177
226
  "__any__", title="The clients envoy version to emulate in this XDS request"
178
227
  ),
179
228
  ) -> Response:
180
- response = perform_discovery(
181
- req=mock_discovery_request(
182
- service_cluster=service_cluster,
183
- resource_names=[route_configuration],
184
- version=envoy_version,
185
- region=region,
186
- ),
187
- api_version=api_version,
188
- resource_type="routes",
189
- skip_auth=True,
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",
190
261
  )
191
- route_configs = [
192
- resource_
193
- for resource_ in response.deserialize_resources()
194
- if resource_["name"] == route_configuration
195
- ]
196
- for route_config in route_configs:
197
- for vhost in route_config["virtual_hosts"]:
198
- if vhost["name"] == virtual_host:
199
- safe_response = jsonable_encoder(vhost)
200
- try:
201
- return json_response_class(content=safe_response)
202
- except TypeError:
203
- return JSONResponse(content=safe_response)
204
- break
205
- return JSONResponse(content={})