sovereign 0.19.3__py3-none-any.whl → 1.0.0b148__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 (80) hide show
  1. sovereign/__init__.py +13 -81
  2. sovereign/app.py +59 -48
  3. sovereign/cache/__init__.py +172 -0
  4. sovereign/cache/backends/__init__.py +110 -0
  5. sovereign/cache/backends/s3.py +143 -0
  6. sovereign/cache/filesystem.py +73 -0
  7. sovereign/cache/types.py +15 -0
  8. sovereign/configuration.py +573 -0
  9. sovereign/constants.py +1 -0
  10. sovereign/context.py +271 -104
  11. sovereign/dynamic_config/__init__.py +113 -0
  12. sovereign/dynamic_config/deser.py +78 -0
  13. sovereign/dynamic_config/loaders.py +120 -0
  14. sovereign/events.py +49 -0
  15. sovereign/logging/access_logger.py +85 -0
  16. sovereign/logging/application_logger.py +54 -0
  17. sovereign/logging/base_logger.py +41 -0
  18. sovereign/logging/bootstrapper.py +36 -0
  19. sovereign/logging/types.py +10 -0
  20. sovereign/middlewares.py +8 -7
  21. sovereign/modifiers/lib.py +1 -0
  22. sovereign/rendering.py +192 -0
  23. sovereign/response_class.py +18 -0
  24. sovereign/server.py +93 -35
  25. sovereign/sources/file.py +1 -1
  26. sovereign/sources/inline.py +1 -0
  27. sovereign/sources/lib.py +1 -0
  28. sovereign/sources/poller.py +296 -53
  29. sovereign/statistics.py +17 -20
  30. sovereign/templates/base.html +59 -46
  31. sovereign/templates/resources.html +203 -102
  32. sovereign/testing/loaders.py +8 -0
  33. sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
  34. sovereign/tracing.py +102 -0
  35. sovereign/types.py +299 -0
  36. sovereign/utils/auth.py +26 -13
  37. sovereign/utils/crypto/__init__.py +0 -0
  38. sovereign/utils/crypto/crypto.py +135 -0
  39. sovereign/utils/crypto/suites/__init__.py +21 -0
  40. sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
  41. sovereign/utils/crypto/suites/base_cipher.py +21 -0
  42. sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
  43. sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
  44. sovereign/utils/dictupdate.py +2 -1
  45. sovereign/utils/eds.py +37 -21
  46. sovereign/utils/mock.py +54 -16
  47. sovereign/utils/resources.py +17 -0
  48. sovereign/utils/version_info.py +8 -0
  49. sovereign/views/__init__.py +4 -0
  50. sovereign/views/api.py +61 -0
  51. sovereign/views/crypto.py +46 -15
  52. sovereign/views/discovery.py +37 -116
  53. sovereign/views/healthchecks.py +87 -18
  54. sovereign/views/interface.py +112 -112
  55. sovereign/worker.py +204 -0
  56. {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +79 -76
  57. sovereign-1.0.0b148.dist-info/RECORD +77 -0
  58. {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/WHEEL +1 -1
  59. sovereign-1.0.0b148.dist-info/entry_points.txt +38 -0
  60. sovereign_files/__init__.py +0 -0
  61. sovereign_files/static/darkmode.js +51 -0
  62. sovereign_files/static/node_expression.js +42 -0
  63. sovereign_files/static/panel.js +76 -0
  64. sovereign_files/static/resources.css +246 -0
  65. sovereign_files/static/resources.js +642 -0
  66. sovereign_files/static/sass/style.scss +33 -0
  67. sovereign_files/static/style.css +16143 -0
  68. sovereign_files/static/style.css.map +1 -0
  69. sovereign/config_loader.py +0 -225
  70. sovereign/discovery.py +0 -175
  71. sovereign/logs.py +0 -131
  72. sovereign/schemas.py +0 -780
  73. sovereign/static/sass/style.scss +0 -27
  74. sovereign/static/style.css +0 -13553
  75. sovereign/templates/ul_filter.html +0 -22
  76. sovereign/utils/crypto.py +0 -103
  77. sovereign/views/admin.py +0 -120
  78. sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
  79. sovereign-0.19.3.dist-info/RECORD +0 -47
  80. sovereign-0.19.3.dist-info/entry_points.txt +0 -10
@@ -0,0 +1,25 @@
1
+ from typing import Any
2
+
3
+ from .base_cipher import CipherSuite
4
+
5
+
6
+ class DisabledCipher(CipherSuite):
7
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
8
+ pass
9
+
10
+ def __str__(self) -> str:
11
+ return "disabled"
12
+
13
+ def encrypt(self, _: str) -> str:
14
+ return "Unavailable (No Secret Key)"
15
+
16
+ def decrypt(self, _: str) -> str:
17
+ return "Unavailable (No Secret Key)"
18
+
19
+ @property
20
+ def key_available(self) -> bool:
21
+ return False
22
+
23
+ @classmethod
24
+ def generate_key(cls) -> bytes:
25
+ return b"Unavailable (No key to generate)"
@@ -0,0 +1,29 @@
1
+ import base64
2
+ import os
3
+
4
+ from cryptography.fernet import Fernet
5
+
6
+ from .base_cipher import CipherSuite
7
+
8
+
9
+ class FernetCipher(CipherSuite):
10
+ def __str__(self) -> str:
11
+ return "fernet"
12
+
13
+ def __init__(self, secret_key: str):
14
+ self.fernet = Fernet(secret_key)
15
+
16
+ def encrypt(self, data: str) -> str:
17
+ return self.fernet.encrypt(data.encode()).decode("utf-8")
18
+
19
+ def decrypt(self, data: str) -> str:
20
+ return self.fernet.decrypt(data).decode("utf-8")
21
+
22
+ @property
23
+ def key_available(self) -> bool:
24
+ return True
25
+
26
+ @classmethod
27
+ def generate_key(cls) -> bytes:
28
+ # Generate 256 bit length key
29
+ return base64.urlsafe_b64encode(os.urandom(32))
@@ -1,6 +1,7 @@
1
1
  # type: ignore
2
2
  # pylint: disable=no-name-in-module,too-many-branches
3
- """ Stolen from the saltstack library """
3
+ """Stolen from the saltstack library"""
4
+
4
5
  from collections.abc import Mapping
5
6
  import copy
6
7
 
sovereign/utils/eds.py CHANGED
@@ -2,8 +2,8 @@ import random
2
2
  from typing import Dict, Any, Optional, List
3
3
  from copy import deepcopy
4
4
  from starlette.exceptions import HTTPException
5
- from sovereign import config
6
- from sovereign.schemas import DiscoveryRequest
5
+ from sovereign.configuration import config
6
+ from sovereign.types import DiscoveryRequest
7
7
  from sovereign.utils.templates import resolve
8
8
 
9
9
  HARD_FAIL_ON_DNS_FAILURE = config.legacy_fields.dns_hard_fail
@@ -26,12 +26,16 @@ def _upstream_kwargs(
26
26
  if hard_fail:
27
27
  raise
28
28
  ip_addresses = [upstream["address"]]
29
- return {
29
+ ret = {
30
30
  "addrs": ip_addresses,
31
31
  "port": upstream["port"],
32
32
  "region": default_region or upstream.get("region", "unknown"),
33
33
  "zone": proxy_region,
34
34
  }
35
+ if "health_check_config" in upstream:
36
+ ret["health_check_config"] = upstream["health_check_config"]
37
+
38
+ return ret
35
39
 
36
40
 
37
41
  def total_zones(endpoints: List[Dict[str, Dict[str, Any]]]) -> int:
@@ -84,14 +88,21 @@ def locality_lb_endpoints(
84
88
  return ret
85
89
 
86
90
 
87
- def lb_endpoints(addrs: List[str], port: int, region: str, zone: str) -> Dict[str, Any]:
91
+ def lb_endpoints(
92
+ addrs: List[str],
93
+ port: int,
94
+ region: str,
95
+ zone: str,
96
+ health_check_config: Optional[Dict[str, Any]] = None,
97
+ ) -> Dict[str, Any]:
88
98
  """
89
99
  Creates an envoy endpoint.LbEndpoints proto
90
100
 
91
- :param addrs: The IP addresses or hostname(s) of the upstream.
92
- :param port: The port that the upstream should be accessed on.
93
- :param region: The region of the upstream.
94
- :param zone: The region of the proxy asking for the endpoint configuration.
101
+ :param addrs: The IP addresses or hostname(s) of the upstream.
102
+ :param port: The port that the upstream should be accessed on.
103
+ :param region: The region of the upstream.
104
+ :param zone: The region of the proxy asking for the endpoint configuration.
105
+ :param health_check_config: Optional health check config for the upstream.
95
106
  """
96
107
  if PRIORITY_MAPPING is None:
97
108
  raise RuntimeError(
@@ -100,20 +111,25 @@ def lb_endpoints(addrs: List[str], port: int, region: str, zone: str) -> Dict[st
100
111
  )
101
112
  node_priorities = PRIORITY_MAPPING.get(zone, {})
102
113
  priority = node_priorities.get(region, 10)
114
+
115
+ endpoints = []
116
+ for addr in addrs:
117
+ endpoint = {
118
+ "endpoint": {
119
+ "address": {
120
+ "socket_address": {
121
+ "address": addr,
122
+ "port_value": port,
123
+ }
124
+ },
125
+ }
126
+ }
127
+ if health_check_config:
128
+ endpoint["endpoint"]["health_check_config"] = health_check_config
129
+ endpoints.append(endpoint)
130
+
103
131
  return {
104
132
  "priority": priority,
105
133
  "locality": {"zone": region},
106
- "lb_endpoints": [
107
- {
108
- "endpoint": {
109
- "address": {
110
- "socket_address": {
111
- "address": addr,
112
- "port_value": port,
113
- }
114
- }
115
- }
116
- }
117
- for addr in addrs
118
- ],
134
+ "lb_endpoints": endpoints,
119
135
  }
sovereign/utils/mock.py CHANGED
@@ -1,36 +1,74 @@
1
+ import re
2
+ import ast
1
3
  from typing import Optional, Dict, List
2
4
  from random import randint
3
- from sovereign.schemas import DiscoveryRequest, Node, Locality, Resources, Status
5
+ from sovereign.types import DiscoveryRequest, Node, Locality, Status
6
+
7
+ scrub = re.compile(r"[^a-zA-Z_\.]")
8
+
9
+
10
+ class NodeExpressionError(Exception):
11
+ pass
4
12
 
5
13
 
6
14
  def mock_discovery_request(
7
- service_cluster: Optional[str] = None,
8
- resource_names: Optional[List[str]] = None,
9
- region: str = "none",
10
- version: str = "1.11.1",
15
+ api_version: Optional[str] = "V3",
16
+ resource_type: Optional[str] = None,
17
+ resource_names: Optional[List[str] | str] = None,
18
+ region: Optional[str] = "none",
19
+ version: Optional[str] = "<envoy_version>",
11
20
  metadata: Optional[Dict[str, str]] = None,
12
21
  error_message: Optional[str] = None,
22
+ expressions: Optional[list[str]] = None,
13
23
  ) -> DiscoveryRequest:
14
24
  if resource_names is None:
15
- resource_names = Resources()
16
- else:
17
- resource_names = Resources(resource_names)
25
+ resource_names = []
26
+ if isinstance(resource_names, str):
27
+ resource_names = [resource_names]
28
+ if expressions is None:
29
+ expressions = []
30
+ base_node = Node(
31
+ id="sovereign-interface",
32
+ cluster="*",
33
+ build_version=f"<randomHash>/{version}/Clean/RELEASE",
34
+ locality=Locality(zone=region),
35
+ ).model_dump()
36
+ set_node_expressions(base_node, expressions)
18
37
  request = DiscoveryRequest(
19
38
  type_url=None,
20
- node=Node(
21
- id="mock",
22
- cluster=service_cluster or "",
23
- build_version=f"e5f864a82d4f27110359daa2fbdcb12d99e415b9/{version}/Clean/RELEASE",
24
- locality=Locality(zone=region, region="example", sub_zone="a"),
25
- ),
39
+ node=Node.model_validate(base_node),
26
40
  version_info=str(randint(100000, 1000000000)),
27
41
  resource_names=resource_names,
28
- hide_private_keys=True,
29
- desired_controlplane="controlplane",
42
+ is_internal_request=True,
43
+ desired_controlplane="__sovereign__",
30
44
  error_detail=Status(code=200, message="None", details=["None"]),
45
+ api_version=api_version,
46
+ resource_type=resource_type,
31
47
  )
32
48
  if isinstance(metadata, dict):
33
49
  request.node.metadata = metadata
34
50
  if error_message:
35
51
  request.error_detail = Status(code=666, message=error_message, details=["foo"])
36
52
  return request
53
+
54
+
55
+ def set_node_expressions(node, expressions):
56
+ for expr in expressions:
57
+ try:
58
+ field, value = re.split(r"\s*=\s*", expr, maxsplit=1)
59
+ value = f'"{value}"'
60
+ except ValueError:
61
+ raise NodeExpressionError(f"Invalid node filter format: {expr}")
62
+
63
+ field = scrub.sub("", field)
64
+ parts = field.split(".")
65
+
66
+ try:
67
+ value = ast.literal_eval(value)
68
+ except Exception as e:
69
+ raise NodeExpressionError(f"Invalid node filter value: {value}") from e
70
+
71
+ current = node
72
+ for part in parts[:-1]:
73
+ current = current.setdefault(part, {})
74
+ current[parts[-1]] = value
@@ -0,0 +1,17 @@
1
+ from functools import cache
2
+ import importlib.resources as res
3
+ from importlib.resources.abc import Traversable
4
+
5
+
6
+ @cache
7
+ def get_package(name: str) -> Traversable:
8
+ return res.files(name)
9
+
10
+
11
+ def get_package_file(package_name: str, filename: str) -> Traversable:
12
+ return get_package(package_name).joinpath(filename)
13
+
14
+
15
+ def get_package_file_bytes(package_name: str, filename: str) -> bytes:
16
+ file = get_package_file(package_name, filename)
17
+ return file.read_bytes()
@@ -11,3 +11,11 @@ def compute_hash(*args: Any) -> str:
11
11
  zlib.crc32(data) & 0xFFFFFFFF
12
12
  ) # same numeric value across all py versions & platforms
13
13
  return str(version_info)
14
+
15
+
16
+ def compute_hash_int(*args: Any) -> int:
17
+ data: bytes = repr(args).encode()
18
+ version_info = (
19
+ zlib.crc32(data) & 0xFFFFFFFF
20
+ ) # same numeric value across all py versions & platforms
21
+ return version_info
@@ -0,0 +1,4 @@
1
+ from sovereign import cache
2
+
3
+
4
+ reader = cache.CacheReader()
sovereign/views/api.py ADDED
@@ -0,0 +1,61 @@
1
+ import json
2
+ from typing import Optional
3
+
4
+ from fastapi import APIRouter, Query, Path
5
+ from fastapi.responses import Response
6
+
7
+ from sovereign.views import reader
8
+ from sovereign.configuration import ConfiguredResourceTypes
9
+ from sovereign.utils.mock import mock_discovery_request
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ def _traverse(data, prefix, expressions):
15
+ for key, value in data.items():
16
+ path = f"{prefix}.{key}" if prefix else key
17
+ if isinstance(value, dict):
18
+ yield from _traverse(value, path, expressions)
19
+ else:
20
+ yield f"{path}={value}"
21
+
22
+
23
+ def expand_metadata_to_expr(m):
24
+ exprs = []
25
+ yield from _traverse(m, "", exprs)
26
+
27
+
28
+ @router.get("/resources/{resource_type}", summary="Get resources for a given type")
29
+ async def resource(
30
+ resource_type: ConfiguredResourceTypes = Path(title="xDS Resource type"),
31
+ resource_name: Optional[str] = Query(None, title="Resource name"),
32
+ api_version: Optional[str] = Query("v3", title="Envoy API version"),
33
+ service_cluster: Optional[str] = Query("*", title="Envoy Service cluster"),
34
+ region: Optional[str] = Query(None, title="Locality Zone"),
35
+ version: Optional[str] = Query(None, title="Envoy Semantic Version"),
36
+ metadata: Optional[str] = Query(None, title="Envoy node metadata to filter by"),
37
+ ) -> Response:
38
+ expressions = [f"cluster={service_cluster}"]
39
+ try:
40
+ data = {"metadata": json.loads(metadata or "{}")}
41
+ for expr in expand_metadata_to_expr(data):
42
+ expressions.append(expr)
43
+ except Exception:
44
+ pass
45
+ kwargs = {
46
+ "api_version": api_version,
47
+ "resource_type": ConfiguredResourceTypes(resource_type).value,
48
+ "resource_names": resource_name,
49
+ "version": version,
50
+ "region": region,
51
+ "expressions": expressions,
52
+ }
53
+ req = mock_discovery_request(**{k: v for k, v in kwargs.items() if v is not None}) # type: ignore
54
+ response = await reader.blocking_read(req)
55
+ if content := getattr(response, "text", None):
56
+ return Response(content, media_type="application/json")
57
+ else:
58
+ return Response(
59
+ json.dumps({"title": "No resources found", "status": 404}),
60
+ media_type="application/json+problem",
61
+ )
sovereign/views/crypto.py CHANGED
@@ -1,31 +1,38 @@
1
- from typing import Dict
2
- from pydantic import BaseModel, Field
1
+ from typing import Any, Dict, Optional
2
+
3
3
  from fastapi import APIRouter, Body
4
4
  from fastapi.responses import JSONResponse
5
- from sovereign import json_response_class, cipher_suite
6
- from sovereign.utils.crypto import generate_key
5
+ from pydantic import BaseModel, Field
6
+
7
+ from sovereign import logs, server_cipher_container
8
+ from sovereign.response_class import json_response_class
9
+ from sovereign.configuration import EncryptionConfig
10
+ from sovereign.utils.crypto.crypto import CipherContainer
11
+ from sovereign.utils.crypto.suites import EncryptionType
7
12
 
8
13
  router = APIRouter()
9
14
 
10
15
 
11
16
  class EncryptionRequest(BaseModel):
12
17
  data: str = Field(..., title="Text to be encrypted", min_length=1, max_length=65535)
13
- key: str = Field(
18
+ key: Optional[str] = Field(
14
19
  None,
15
- title="Optional Fernet encryption key to use to encrypt",
20
+ title="Optional encryption key to use to encrypt",
16
21
  min_length=44,
17
22
  max_length=44,
18
23
  )
24
+ encryption_type: str = Field(default="fernet", title="Encryption type to be used")
19
25
 
20
26
 
21
27
  class DecryptionRequest(BaseModel):
22
28
  data: str = Field(..., title="Text to be decrypted", min_length=1, max_length=65535)
23
29
  key: str = Field(
24
30
  ...,
25
- title="Fernet encryption key to use to decrypt",
31
+ title="Encryption key to use to decrypt",
26
32
  min_length=44,
27
33
  max_length=44,
28
34
  )
35
+ encryption_type: str = Field(default="fernet", title="Encryption type to be used")
29
36
 
30
37
 
31
38
  class DecryptableRequest(BaseModel):
@@ -37,17 +44,37 @@ class DecryptableRequest(BaseModel):
37
44
  summary="Decrypt provided encrypted data using a provided key",
38
45
  response_class=json_response_class,
39
46
  )
40
- async def _decrypt(request: DecryptionRequest = Body(None)) -> Dict[str, str]:
41
- return {"result": cipher_suite.decrypt(request.data, request.key)}
47
+ async def _decrypt(request: DecryptionRequest = Body(None)) -> dict[str, Any]:
48
+ user_cipher_container = CipherContainer.from_encryption_configs(
49
+ encryption_configs=[
50
+ EncryptionConfig(
51
+ encryption_key=request.key,
52
+ encryption_type=EncryptionType(request.encryption_type),
53
+ )
54
+ ],
55
+ logger=logs.application_logger.logger,
56
+ )
57
+ return {**user_cipher_container.decrypt_with_type(request.data)}
42
58
 
43
59
 
44
60
  @router.post(
45
61
  "/encrypt",
46
- summary="Encrypt provided data using this servers key",
62
+ summary="Encrypt provided data using this servers key or provided key",
47
63
  response_class=json_response_class,
48
64
  )
49
- async def _encrypt(request: EncryptionRequest = Body(None)) -> Dict[str, str]:
50
- return {"result": cipher_suite.encrypt(data=request.data, key=request.key)}
65
+ async def _encrypt(request: EncryptionRequest = Body(None)) -> dict[str, Any]:
66
+ if request.key:
67
+ user_cipher_container = CipherContainer.from_encryption_configs(
68
+ encryption_configs=[
69
+ EncryptionConfig(
70
+ encryption_key=request.key,
71
+ encryption_type=EncryptionType(request.encryption_type),
72
+ )
73
+ ],
74
+ logger=logs.application_logger.logger,
75
+ )
76
+ return {**user_cipher_container.encrypt(request.data)}
77
+ return {**server_cipher_container.encrypt(request.data)}
51
78
 
52
79
 
53
80
  @router.post(
@@ -56,7 +83,7 @@ async def _encrypt(request: EncryptionRequest = Body(None)) -> Dict[str, str]:
56
83
  response_class=json_response_class,
57
84
  )
58
85
  async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse:
59
- cipher_suite.decrypt(request.data)
86
+ server_cipher_container.decrypt(request.data)
60
87
  return json_response_class({})
61
88
 
62
89
 
@@ -65,5 +92,9 @@ async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse
65
92
  summary="Generate a new asymmetric encryption key",
66
93
  response_class=json_response_class,
67
94
  )
68
- def _generate_key() -> Dict[str, str]:
69
- return {"result": generate_key()}
95
+ def _generate_key(encryption_type: str = "fernet") -> Dict[str, str]:
96
+ cipher_suite = CipherContainer.get_cipher_suite(EncryptionType(encryption_type))
97
+ return {
98
+ "key": cipher_suite.generate_key().decode(),
99
+ "encryption_type": encryption_type,
100
+ }
@@ -1,63 +1,20 @@
1
- from typing import Dict
2
-
3
1
  from fastapi import Body, Header
4
- from fastapi.routing import APIRouter
5
2
  from fastapi.responses import Response
3
+ from fastapi.routing import APIRouter
6
4
 
7
- from sovereign import discovery, logs, config
5
+ from sovereign import cache, logs
6
+ from sovereign.views import reader
7
+ from sovereign.cache.types import Entry
8
8
  from sovereign.utils.auth import authenticate
9
- from sovereign.utils.version_info import compute_hash
10
- from sovereign.schemas import (
9
+ from sovereign.types import (
11
10
  DiscoveryRequest,
12
11
  DiscoveryResponse,
13
- ProcessedTemplate,
14
12
  )
15
13
 
16
- discovery_cache = config.discovery_cache
17
-
18
- if discovery_cache.enabled:
19
- from cashews import cache
20
-
21
- cache.setup(
22
- f"{discovery_cache.protocol}{discovery_cache.host}:{discovery_cache.port}",
23
- password=discovery_cache.password.get_secret_value(),
24
- client_side=discovery_cache.client_side,
25
- wait_for_connection_timeout=discovery_cache.wait_for_connection_timeout,
26
- socket_connect_timeout=discovery_cache.socket_connect_timeout,
27
- socket_timeout=discovery_cache.socket_timeout,
28
- max_connections=discovery_cache.max_connections,
29
- retry_on_timeout=discovery_cache.retry_on_timeout,
30
- suppress=discovery_cache.suppress,
31
- socket_keepalive=discovery_cache.socket_keepalive,
32
- enable=discovery_cache.enabled,
33
- )
34
-
35
- router = APIRouter()
36
-
37
- type_urls = {
38
- "v2": {
39
- "listeners": "type.googleapis.com/envoy.api.v2.Listener",
40
- "clusters": "type.googleapis.com/envoy.api.v2.Cluster",
41
- "endpoints": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
42
- "secrets": "type.googleapis.com/envoy.api.v2.auth.Secret",
43
- "routes": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
44
- "scoped-routes": "type.googleapis.com/envoy.api.v2.ScopedRouteConfiguration",
45
- },
46
- "v3": {
47
- "listeners": "type.googleapis.com/envoy.config.listener.v3.Listener",
48
- "clusters": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
49
- "endpoints": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
50
- "secrets": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
51
- "routes": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
52
- "scoped-routes": "type.googleapis.com/envoy.config.route.v3.ScopedRouteConfiguration",
53
- "runtime": "type.googleapis.com/envoy.service.runtime.v3.Runtime",
54
- },
55
- }
56
-
57
14
 
58
15
  def response_headers(
59
- discovery_request: DiscoveryRequest, response: ProcessedTemplate, xds: str
60
- ) -> Dict[str, str]:
16
+ discovery_request: DiscoveryRequest, response: Entry, xds: str
17
+ ) -> dict[str, str]:
61
18
  return {
62
19
  "X-Sovereign-Client-Build": discovery_request.envoy_version,
63
20
  "X-Sovereign-Client-Version": discovery_request.version_info,
@@ -68,6 +25,9 @@ def response_headers(
68
25
  }
69
26
 
70
27
 
28
+ router = APIRouter()
29
+
30
+
71
31
  @router.post(
72
32
  "/{version}/discovery:{xds_type}",
73
33
  summary="Envoy Discovery Service Endpoint",
@@ -81,76 +41,37 @@ def response_headers(
81
41
  async def discovery_response(
82
42
  version: str,
83
43
  xds_type: str,
84
- discovery_request: DiscoveryRequest = Body(...),
44
+ xds_req: DiscoveryRequest = Body(...),
85
45
  host: str = Header("no_host_provided"),
86
46
  ) -> Response:
87
- discovery_request.desired_controlplane = host
88
- response = await perform_discovery(
89
- discovery_request, version, xds_type, skip_auth=False
90
- )
91
- logs.queue_log_fields(
92
- XDS_RESOURCES=discovery_request.resource_names,
93
- XDS_ENVOY_VERSION=discovery_request.envoy_version,
94
- XDS_CLIENT_VERSION=discovery_request.version_info,
95
- XDS_SERVER_VERSION=response.version,
96
- )
97
- if discovery_request.error_detail:
98
- logs.queue_log_fields(XDS_ERROR_DETAIL=discovery_request.error_detail.message)
99
- headers = response_headers(discovery_request, response, xds_type)
100
-
101
- if response.version == discovery_request.version_info:
102
- return not_modified(headers)
103
- elif getattr(response, "resources", None) == []:
104
- return Response(status_code=404, headers=headers)
105
- elif response.version != discovery_request.version_info:
106
- return Response(
107
- response.rendered, headers=headers, media_type="application/json"
47
+ authenticate(xds_req)
48
+
49
+ # Pack additional info into the request
50
+ xds_req.desired_controlplane = host
51
+ xds_req.resource_type = xds_type
52
+ xds_req.api_version = version
53
+ if xds_req.error_detail:
54
+ logs.access_logger.queue_log_fields(
55
+ XDS_ERROR_DETAIL=xds_req.error_detail.message
108
56
  )
109
- return Response(content="Resources could not be determined", status_code=500)
110
-
57
+ logs.access_logger.queue_log_fields(
58
+ XDS_RESOURCES=xds_req.resource_names,
59
+ XDS_ENVOY_VERSION=xds_req.envoy_version,
60
+ XDS_CLIENT_VERSION=xds_req.version_info,
61
+ )
111
62
 
112
- async def perform_discovery(
113
- req: DiscoveryRequest,
114
- api_version: str,
115
- resource_type: str,
116
- skip_auth: bool = False,
117
- ) -> ProcessedTemplate:
118
- if not skip_auth:
119
- authenticate(req)
120
- if discovery_cache.enabled:
121
- logs.queue_log_fields(CACHE_XDS_HIT=False)
122
- cache_key = compute_hash(
123
- [
124
- api_version,
125
- resource_type,
126
- req.envoy_version,
127
- req.resource_names,
128
- req.desired_controlplane,
129
- req.hide_private_keys,
130
- req.type_url,
131
- req.node.cluster,
132
- req.node.locality,
133
- req.node.metadata.get("auth", None),
134
- req.node.metadata.get("num_cpus", None),
135
- ]
136
- )
137
- if template := await cache.get(key=cache_key, default=None):
138
- logs.queue_log_fields(CACHE_XDS_HIT=True)
139
- return template # type: ignore[no-any-return]
140
- template = discovery.response(req, resource_type)
141
- type_url = type_urls.get(api_version, {}).get(resource_type)
142
- if type_url is not None:
143
- for resource in template.resources:
144
- if not resource.get("@type"):
145
- resource["@type"] = type_url
146
- if discovery_cache.enabled:
147
- await cache.set(
148
- key=cache_key,
149
- value=template,
150
- expire=discovery_cache.ttl,
63
+ def handle_response(entry: cache.Entry):
64
+ logs.access_logger.queue_log_fields(
65
+ XDS_SERVER_VERSION=entry.version,
151
66
  )
152
- return template # type: ignore[no-any-return]
67
+ headers = response_headers(xds_req, entry, xds_type)
68
+ if entry.len == 0:
69
+ return Response(status_code=404, headers=headers)
70
+ if entry.version == xds_req.version_info:
71
+ return Response(status_code=304, headers=headers)
72
+ return Response(entry.text, media_type="application/json", headers=headers)
153
73
 
74
+ if entry := await reader.blocking_read(xds_req):
75
+ return handle_response(entry)
154
76
 
155
- def not_modified(headers: Dict[str, str]) -> Response:
156
- return Response(status_code=304, headers=headers)
77
+ return Response(content="Something went wrong", status_code=500)