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.
- sovereign/app.py +1 -1
- sovereign/cache/__init__.py +182 -0
- sovereign/cache/backends/__init__.py +110 -0
- sovereign/cache/backends/s3.py +139 -0
- sovereign/cache/filesystem.py +42 -0
- sovereign/cache/types.py +15 -0
- sovereign/context.py +20 -18
- sovereign/events.py +49 -0
- sovereign/middlewares.py +1 -1
- sovereign/rendering.py +74 -35
- sovereign/schemas.py +112 -110
- sovereign/server.py +4 -3
- sovereign/sources/poller.py +20 -4
- sovereign/statistics.py +1 -1
- sovereign/templates/base.html +59 -46
- sovereign/templates/resources.html +40 -835
- sovereign/utils/mock.py +7 -3
- sovereign/views/healthchecks.py +1 -1
- sovereign/views/interface.py +34 -15
- sovereign/worker.py +87 -46
- {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/METADATA +4 -5
- {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/RECORD +33 -24
- {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/WHEEL +1 -1
- {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/entry_points.txt +3 -0
- sovereign_files/__init__.py +0 -0
- sovereign_files/static/darkmode.js +51 -0
- sovereign_files/static/node_expression.js +42 -0
- sovereign_files/static/resources.css +246 -0
- sovereign_files/static/resources.js +642 -0
- sovereign_files/static/sass/style.scss +33 -0
- sovereign_files/static/style.css +16143 -0
- sovereign_files/static/style.css.map +1 -0
- sovereign/cache.py +0 -133
- sovereign/static/node_expression.js +0 -16
- sovereign/static/sass/style.scss +0 -27
- sovereign/static/style.css +0 -13553
- sovereign-1.0.0b123.dist-info/LICENSE.txt +0 -13
- {sovereign → sovereign_files}/static/panel.js +0 -0
sovereign/rendering.py
CHANGED
|
@@ -7,7 +7,11 @@ 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
|
-
|
|
10
|
+
import traceback
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from multiprocessing import Process, Pipe, cpu_count
|
|
13
|
+
from multiprocessing.connection import Connection
|
|
14
|
+
from typing import Any
|
|
11
15
|
|
|
12
16
|
import yaml
|
|
13
17
|
import pydantic
|
|
@@ -21,12 +25,14 @@ try:
|
|
|
21
25
|
except ImportError:
|
|
22
26
|
SENTRY_INSTALLED = False
|
|
23
27
|
|
|
24
|
-
from sovereign import config, logs, cache, stats
|
|
28
|
+
from sovereign import config, logs, cache, stats, application_logger as log
|
|
25
29
|
from sovereign.schemas import (
|
|
26
30
|
DiscoveryRequest,
|
|
27
31
|
ProcessedTemplate,
|
|
28
32
|
)
|
|
29
33
|
|
|
34
|
+
# limit render jobs to number of cores
|
|
35
|
+
POOL = ThreadPoolExecutor(max_workers=cpu_count())
|
|
30
36
|
|
|
31
37
|
type_urls = {
|
|
32
38
|
"v2": {
|
|
@@ -54,42 +60,75 @@ class RenderJob(pydantic.BaseModel):
|
|
|
54
60
|
request: DiscoveryRequest
|
|
55
61
|
context: dict[str, Any]
|
|
56
62
|
|
|
63
|
+
def submit(self):
|
|
64
|
+
_ = POOL.submit(self._run)
|
|
65
|
+
|
|
66
|
+
def _run(self):
|
|
67
|
+
rx, tx = Pipe()
|
|
68
|
+
proc = Process(target=generate, args=[self, tx])
|
|
69
|
+
proc.start()
|
|
70
|
+
log.debug(
|
|
71
|
+
(
|
|
72
|
+
f"Spawning process for id={self.id} "
|
|
73
|
+
f"max_workers={POOL._max_workers} "
|
|
74
|
+
f"threads={len(POOL._threads)} "
|
|
75
|
+
f"shutdown={POOL._shutdown} "
|
|
76
|
+
f"queue_size={POOL._work_queue.qsize()}"
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
proc.join(timeout=30) # TODO: render timeout configurable
|
|
80
|
+
while rx.poll(timeout=30):
|
|
81
|
+
level, message = rx.recv()
|
|
82
|
+
logger = getattr(log, level)
|
|
83
|
+
logger(message)
|
|
84
|
+
|
|
57
85
|
|
|
58
|
-
def generate(job: RenderJob) -> None:
|
|
86
|
+
def generate(job: RenderJob, tx: Connection) -> None:
|
|
59
87
|
request = job.request
|
|
60
88
|
tags = [f"type:{request.resource_type}"]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
try:
|
|
90
|
+
with stats.timed("template.render_ms", tags=tags):
|
|
91
|
+
content = request.template(
|
|
92
|
+
discovery_request=request,
|
|
93
|
+
host_header=request.desired_controlplane,
|
|
94
|
+
resource_names=request.resources,
|
|
95
|
+
**job.context,
|
|
96
|
+
)
|
|
97
|
+
if not request.template.is_python_source:
|
|
98
|
+
assert isinstance(content, str)
|
|
99
|
+
content = deserialize_config(content)
|
|
100
|
+
assert isinstance(content, dict)
|
|
101
|
+
resources = filter_resources(content["resources"], request.resources)
|
|
102
|
+
add_type_urls(request.api_version, request.resource_type, resources)
|
|
103
|
+
response = ProcessedTemplate(resources=resources)
|
|
104
|
+
cache.write(
|
|
105
|
+
job.id,
|
|
106
|
+
cache.Entry(
|
|
107
|
+
text=response.model_dump_json(indent=None),
|
|
108
|
+
len=len(response.resources),
|
|
109
|
+
version=response.version_info,
|
|
110
|
+
node=request.node,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
tags.append("result:ok")
|
|
114
|
+
tx.send(("debug", f"Completed rendering of {request} for {job.id}"))
|
|
115
|
+
except Exception as e:
|
|
116
|
+
tx.send(
|
|
117
|
+
(
|
|
118
|
+
"error",
|
|
119
|
+
f"Failed to render job for {job.id}: " + str(traceback.format_exc()),
|
|
120
|
+
)
|
|
84
121
|
)
|
|
122
|
+
tags.append("result:err")
|
|
123
|
+
tags.append(f"error:{e.__class__.__name__.lower()}")
|
|
124
|
+
if SENTRY_INSTALLED and config.sentry_dsn:
|
|
125
|
+
sentry_sdk.capture_exception(e)
|
|
126
|
+
finally:
|
|
127
|
+
stats.increment("template.render", tags=tags)
|
|
128
|
+
tx.close()
|
|
85
129
|
|
|
86
130
|
|
|
87
|
-
def
|
|
88
|
-
for job in jobs:
|
|
89
|
-
generate(job)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def deserialize_config(content: str) -> Dict[str, Any]:
|
|
131
|
+
def deserialize_config(content: str) -> dict[str, Any]:
|
|
93
132
|
try:
|
|
94
133
|
envoy_configuration = yaml.safe_load(content)
|
|
95
134
|
except (ParserError, ScannerError) as e:
|
|
@@ -119,8 +158,8 @@ def deserialize_config(content: str) -> Dict[str, Any]:
|
|
|
119
158
|
|
|
120
159
|
|
|
121
160
|
def filter_resources(
|
|
122
|
-
generated:
|
|
123
|
-
) ->
|
|
161
|
+
generated: list[dict[str, Any]], requested: list[str]
|
|
162
|
+
) -> list[dict[str, Any]]:
|
|
124
163
|
"""
|
|
125
164
|
If Envoy specifically requested a resource, this removes everything
|
|
126
165
|
that does not match the name of the resource.
|
|
@@ -131,7 +170,7 @@ def filter_resources(
|
|
|
131
170
|
return [resource for resource in generated if resource_name(resource) in requested]
|
|
132
171
|
|
|
133
172
|
|
|
134
|
-
def resource_name(resource:
|
|
173
|
+
def resource_name(resource: dict[str, Any]) -> str:
|
|
135
174
|
name = resource.get("name") or resource.get("cluster_name")
|
|
136
175
|
if isinstance(name, str):
|
|
137
176
|
return name
|
sovereign/schemas.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
1
2
|
import os
|
|
2
3
|
import warnings
|
|
3
4
|
import importlib
|
|
5
|
+
import hashlib
|
|
4
6
|
import multiprocessing
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from enum import Enum
|
|
@@ -8,7 +10,18 @@ from os import getenv
|
|
|
8
10
|
from types import ModuleType
|
|
9
11
|
from dataclasses import dataclass
|
|
10
12
|
from functools import cached_property
|
|
11
|
-
from typing import
|
|
13
|
+
from typing import (
|
|
14
|
+
Any,
|
|
15
|
+
Dict,
|
|
16
|
+
List,
|
|
17
|
+
Mapping,
|
|
18
|
+
Optional,
|
|
19
|
+
Self,
|
|
20
|
+
Tuple,
|
|
21
|
+
Union,
|
|
22
|
+
Callable,
|
|
23
|
+
cast,
|
|
24
|
+
)
|
|
12
25
|
|
|
13
26
|
import yaml
|
|
14
27
|
import jmespath
|
|
@@ -32,9 +45,6 @@ from sovereign.utils import dictupdate
|
|
|
32
45
|
from sovereign.utils.version_info import compute_hash
|
|
33
46
|
|
|
34
47
|
missing_arguments = {"missing", "positional", "arguments:"}
|
|
35
|
-
BASIS = 2166136261
|
|
36
|
-
PRIME = 16777619
|
|
37
|
-
OVERFLOW = 0xFFFFFFFF
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
class CacheStrategy(str, Enum):
|
|
@@ -89,44 +99,10 @@ class StatsdConfig(BaseModel):
|
|
|
89
99
|
return ret
|
|
90
100
|
|
|
91
101
|
|
|
92
|
-
class DiscoveryCacheConfig(BaseModel):
|
|
93
|
-
enabled: bool = False
|
|
94
|
-
host: str = "localhost"
|
|
95
|
-
port: int = 6379
|
|
96
|
-
secure: bool = False
|
|
97
|
-
protocol: str = "redis://"
|
|
98
|
-
password: SecretStr = SecretStr("")
|
|
99
|
-
client_side: bool = True # True = Try in-memory cache before hitting redis
|
|
100
|
-
wait_for_connection_timeout: int = 5
|
|
101
|
-
socket_connect_timeout: int = 5
|
|
102
|
-
socket_timeout: int = 5
|
|
103
|
-
max_connections: int = 100
|
|
104
|
-
retry_on_timeout: bool = True # Retry connections if they timeout.
|
|
105
|
-
suppress: bool = False # False = Don't suppress connection errors. True = suppress connection errors
|
|
106
|
-
socket_keepalive: bool = True # Try to keep connections to redis around.
|
|
107
|
-
ttl: int = 60
|
|
108
|
-
extra_keys: Dict[str, Any] = {}
|
|
109
|
-
|
|
110
|
-
@model_validator(mode="after")
|
|
111
|
-
def set_default_protocol(self) -> Self:
|
|
112
|
-
if self.secure:
|
|
113
|
-
self.protocol = "rediss://"
|
|
114
|
-
return self
|
|
115
|
-
|
|
116
|
-
@model_validator(mode="before")
|
|
117
|
-
@classmethod
|
|
118
|
-
def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
119
|
-
if host := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_HOST"):
|
|
120
|
-
values["host"] = host
|
|
121
|
-
if port := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_PORT"):
|
|
122
|
-
values["port"] = int(port)
|
|
123
|
-
if password := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_PASSWORD"):
|
|
124
|
-
values["password"] = SecretStr(password)
|
|
125
|
-
return values
|
|
126
|
-
|
|
127
|
-
|
|
128
102
|
class XdsTemplate(BaseModel):
|
|
129
103
|
path: Union[str, Loadable]
|
|
104
|
+
resource_type: str
|
|
105
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
130
106
|
|
|
131
107
|
@property
|
|
132
108
|
def loadable(self):
|
|
@@ -348,37 +324,14 @@ class DiscoveryRequest(BaseModel):
|
|
|
348
324
|
def resources(self) -> Resources:
|
|
349
325
|
return Resources(self.resource_names)
|
|
350
326
|
|
|
351
|
-
|
|
352
|
-
def default_cache_rules(self):
|
|
353
|
-
return [
|
|
354
|
-
# Sovereign internal fields
|
|
355
|
-
"template.version",
|
|
356
|
-
"is_internal_request",
|
|
357
|
-
"desired_controlplane",
|
|
358
|
-
"resource_type",
|
|
359
|
-
"api_version",
|
|
360
|
-
"envoy_version",
|
|
361
|
-
# Envoy fields from the real Discovery Request
|
|
362
|
-
"resource_names",
|
|
363
|
-
"node.cluster",
|
|
364
|
-
"node.locality",
|
|
365
|
-
]
|
|
366
|
-
|
|
367
|
-
def cache_key(self, rules: Optional[list[str]] = None):
|
|
368
|
-
if rules is None:
|
|
369
|
-
rules = self.default_cache_rules
|
|
370
|
-
combined = 0
|
|
327
|
+
def cache_key(self, rules: list[str]) -> str:
|
|
371
328
|
map = self.model_dump()
|
|
329
|
+
hash = hashlib.sha256()
|
|
372
330
|
for expr in sorted(rules):
|
|
373
|
-
value = jmespath.search(expr, map)
|
|
331
|
+
value = cast(str, jmespath.search(expr, map))
|
|
374
332
|
val_str = f"{expr}={repr(value)}"
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
for c in val_str:
|
|
378
|
-
h = (h ^ ord(c)) * PRIME
|
|
379
|
-
h &= OVERFLOW
|
|
380
|
-
combined ^= h
|
|
381
|
-
return combined
|
|
333
|
+
hash.update(val_str.encode())
|
|
334
|
+
return hash.hexdigest()
|
|
382
335
|
|
|
383
336
|
@computed_field # type: ignore[prop-decorator]
|
|
384
337
|
@cached_property
|
|
@@ -511,7 +464,6 @@ class SovereignConfig(BaseSettings):
|
|
|
511
464
|
enable_access_logs: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
|
|
512
465
|
log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
|
|
513
466
|
ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
|
|
514
|
-
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
515
467
|
tracing: Optional["TracingConfig"] = None
|
|
516
468
|
model_config = SettingsConfigDict(
|
|
517
469
|
env_file=".env",
|
|
@@ -541,7 +493,8 @@ class SovereignConfig(BaseSettings):
|
|
|
541
493
|
} # Special key to hold templates from all versions
|
|
542
494
|
for version, templates in self.templates.items():
|
|
543
495
|
loaded_templates = {
|
|
544
|
-
_type: XdsTemplate(path=path
|
|
496
|
+
_type: XdsTemplate(path=path, resource_type=_type)
|
|
497
|
+
for _type, path in templates.items()
|
|
545
498
|
}
|
|
546
499
|
ret[str(version)] = loaded_templates
|
|
547
500
|
ret["__any__"].update(loaded_templates)
|
|
@@ -566,6 +519,7 @@ class SovereignConfig(BaseSettings):
|
|
|
566
519
|
class TemplateSpecification(BaseModel):
|
|
567
520
|
type: str
|
|
568
521
|
spec: Loadable
|
|
522
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
569
523
|
|
|
570
524
|
|
|
571
525
|
class NodeMatching(BaseSettings):
|
|
@@ -671,6 +625,7 @@ class ContextConfiguration(BaseSettings):
|
|
|
671
625
|
refresh_retry_interval_secs: int = Field(
|
|
672
626
|
10, alias="SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
|
|
673
627
|
)
|
|
628
|
+
cooldown: int = Field(15, alias="SOVEREIGN_CONTEXT_REFRESH_COOLDOWN")
|
|
674
629
|
model_config = SettingsConfigDict(
|
|
675
630
|
env_file=".env",
|
|
676
631
|
extra="ignore",
|
|
@@ -717,7 +672,6 @@ class SourcesConfiguration(BaseSettings):
|
|
|
717
672
|
refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
|
|
718
673
|
max_retries: int = Field(3, alias="SOVEREIGN_SOURCES_MAX_RETRIES")
|
|
719
674
|
retry_delay: int = Field(1, alias="SOVEREIGN_SOURCES_RETRY_DELAY")
|
|
720
|
-
cache_strategy: Optional[Any] = None
|
|
721
675
|
model_config = SettingsConfigDict(
|
|
722
676
|
env_file=".env",
|
|
723
677
|
extra="ignore",
|
|
@@ -852,29 +806,78 @@ class LegacyConfig(BaseSettings):
|
|
|
852
806
|
return None
|
|
853
807
|
|
|
854
808
|
|
|
809
|
+
class TemplateConfiguration(BaseModel):
|
|
810
|
+
default: list[TemplateSpecification]
|
|
811
|
+
versions: dict[str, list[TemplateSpecification]] = Field(default_factory=dict)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def default_hash_rules():
|
|
815
|
+
return [
|
|
816
|
+
# Sovereign internal fields
|
|
817
|
+
"template.version",
|
|
818
|
+
"is_internal_request",
|
|
819
|
+
"desired_controlplane",
|
|
820
|
+
"resource_type",
|
|
821
|
+
"api_version",
|
|
822
|
+
"envoy_version",
|
|
823
|
+
# Envoy fields from the real Discovery Request
|
|
824
|
+
"resource_names",
|
|
825
|
+
"node.cluster",
|
|
826
|
+
"node.locality",
|
|
827
|
+
]
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
class CacheBackendConfig(BaseModel):
|
|
831
|
+
type: str = Field(..., description="Cache backend type (e.g., 'redis', 's3')")
|
|
832
|
+
config: dict[str, Any] = Field(
|
|
833
|
+
default_factory=dict, description="Backend-specific configuration"
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
class CacheConfiguration(BaseModel):
|
|
838
|
+
hash_rules: list[str] = Field(
|
|
839
|
+
default_factory=default_hash_rules,
|
|
840
|
+
description="The set of JMES expressions against incoming Discovery Requests used to form a cache key.",
|
|
841
|
+
)
|
|
842
|
+
read_timeout: float = Field(
|
|
843
|
+
5.0,
|
|
844
|
+
description="How long to block when trying to read from the cache before giving up",
|
|
845
|
+
)
|
|
846
|
+
local_fs_path: str = Field(
|
|
847
|
+
"/var/run/sovereign_cache",
|
|
848
|
+
description="Local filesystem cache path. Used to provide fast responses to clients and reduce hits against remote cache backend.",
|
|
849
|
+
)
|
|
850
|
+
remote_backend: CacheBackendConfig | None = Field(
|
|
851
|
+
None, description="Remote cache backend configuration"
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
|
|
855
855
|
class SovereignConfigv2(BaseSettings):
|
|
856
|
-
|
|
856
|
+
# Config generation
|
|
857
|
+
templates: TemplateConfiguration
|
|
857
858
|
template_context: ContextConfiguration = ContextConfiguration()
|
|
859
|
+
|
|
860
|
+
# Web/Discovery
|
|
858
861
|
authentication: AuthConfiguration = AuthConfiguration()
|
|
859
|
-
logging: LoggingConfiguration = LoggingConfiguration()
|
|
860
|
-
statsd: StatsdConfig = StatsdConfig()
|
|
861
|
-
sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
|
|
862
|
-
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
863
862
|
|
|
864
|
-
#
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
863
|
+
# Cache
|
|
864
|
+
cache: CacheConfiguration = CacheConfiguration()
|
|
865
|
+
|
|
866
|
+
# Worker
|
|
868
867
|
worker_host: Optional[str] = Field("localhost", alias="SOVEREIGN_WORKER_HOST")
|
|
869
868
|
worker_port: Optional[int] = Field(9080, alias="SOVEREIGN_WORKER_PORT")
|
|
870
869
|
|
|
871
|
-
tracing: Optional[TracingConfig] = Field(default_factory=TracingConfig)
|
|
872
|
-
debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
|
|
873
|
-
|
|
874
870
|
# Supervisord settings
|
|
875
871
|
supervisord: SupervisordConfig = SupervisordConfig()
|
|
876
872
|
|
|
877
|
-
#
|
|
873
|
+
# Misc
|
|
874
|
+
tracing: Optional[TracingConfig] = Field(default_factory=TracingConfig)
|
|
875
|
+
debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
|
|
876
|
+
logging: LoggingConfiguration = LoggingConfiguration()
|
|
877
|
+
statsd: StatsdConfig = StatsdConfig()
|
|
878
|
+
sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
|
|
879
|
+
|
|
880
|
+
# Planned for removal/deprecated/blocked by circular context usage internally
|
|
878
881
|
sources: Optional[List[ConfiguredSource]] = Field(None, deprecated=True)
|
|
879
882
|
source_config: SourcesConfiguration = Field(
|
|
880
883
|
default_factory=SourcesConfiguration, deprecated=True
|
|
@@ -884,6 +887,8 @@ class SovereignConfigv2(BaseSettings):
|
|
|
884
887
|
)
|
|
885
888
|
modifiers: List[str] = Field(default_factory=list, deprecated=True)
|
|
886
889
|
global_modifiers: List[str] = Field(default_factory=list, deprecated=True)
|
|
890
|
+
|
|
891
|
+
# Deprecated, need to migrate off internally
|
|
887
892
|
legacy_fields: LegacyConfig = Field(default_factory=LegacyConfig, deprecated=True)
|
|
888
893
|
|
|
889
894
|
model_config = SettingsConfigDict(
|
|
@@ -898,16 +903,23 @@ class SovereignConfigv2(BaseSettings):
|
|
|
898
903
|
return self.authentication.auth_passwords.get_secret_value().split(",") or []
|
|
899
904
|
|
|
900
905
|
def xds_templates(self) -> Dict[str, Dict[str, XdsTemplate]]:
|
|
901
|
-
ret: Dict[str, Dict[str, XdsTemplate]] =
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
906
|
+
ret: Dict[str, Dict[str, XdsTemplate]] = defaultdict(dict)
|
|
907
|
+
for template in self.templates.default:
|
|
908
|
+
ret["default"][template.type] = XdsTemplate(
|
|
909
|
+
path=template.spec,
|
|
910
|
+
resource_type=template.type,
|
|
911
|
+
depends_on=template.depends_on,
|
|
912
|
+
)
|
|
913
|
+
for version, templates in self.templates.versions.items():
|
|
914
|
+
for template in templates:
|
|
915
|
+
loaded = XdsTemplate(
|
|
916
|
+
path=template.spec,
|
|
917
|
+
resource_type=template.type,
|
|
918
|
+
depends_on=template.depends_on,
|
|
919
|
+
)
|
|
920
|
+
ret[version][template.type] = loaded
|
|
921
|
+
ret["__any__"][template.type] = loaded
|
|
922
|
+
ret["__any__"].update(ret["default"])
|
|
911
923
|
return ret
|
|
912
924
|
|
|
913
925
|
def __str__(self) -> str:
|
|
@@ -921,7 +933,7 @@ class SovereignConfigv2(BaseSettings):
|
|
|
921
933
|
|
|
922
934
|
@staticmethod
|
|
923
935
|
def from_legacy_config(other: SovereignConfig) -> "SovereignConfigv2":
|
|
924
|
-
new_templates =
|
|
936
|
+
new_templates = TemplateConfiguration(default=list())
|
|
925
937
|
for version, templates in other.templates.items():
|
|
926
938
|
specs = list()
|
|
927
939
|
for type, path in templates.items():
|
|
@@ -934,14 +946,16 @@ class SovereignConfigv2(BaseSettings):
|
|
|
934
946
|
else:
|
|
935
947
|
# Just in case? Although this shouldn't happen
|
|
936
948
|
specs.append(TemplateSpecification(type=type, spec=path))
|
|
937
|
-
|
|
949
|
+
if version == "default":
|
|
950
|
+
new_templates.default = specs
|
|
951
|
+
else:
|
|
952
|
+
new_templates.versions[str(version)] = specs
|
|
938
953
|
|
|
939
954
|
return SovereignConfigv2(
|
|
940
955
|
sources=other.sources,
|
|
941
956
|
templates=new_templates,
|
|
942
957
|
source_config=SourcesConfiguration(
|
|
943
958
|
refresh_rate=other.sources_refresh_rate,
|
|
944
|
-
cache_strategy=None,
|
|
945
959
|
),
|
|
946
960
|
modifiers=other.modifiers,
|
|
947
961
|
global_modifiers=other.global_modifiers,
|
|
@@ -983,7 +997,6 @@ class SovereignConfigv2(BaseSettings):
|
|
|
983
997
|
dns_hard_fail=other.dns_hard_fail,
|
|
984
998
|
environment=other.environment,
|
|
985
999
|
),
|
|
986
|
-
discovery_cache=other.discovery_cache,
|
|
987
1000
|
)
|
|
988
1001
|
|
|
989
1002
|
|
|
@@ -1030,20 +1043,9 @@ def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
|
|
|
1030
1043
|
|
|
1031
1044
|
|
|
1032
1045
|
config_path = os.getenv("SOVEREIGN_CONFIG", "file:///etc/sovereign.yaml")
|
|
1033
|
-
|
|
1034
|
-
config = SovereignConfigv2(**parse_raw_configuration(config_path))
|
|
1035
|
-
except ValidationError:
|
|
1036
|
-
old_config = SovereignConfig(**parse_raw_configuration(config_path))
|
|
1037
|
-
config = SovereignConfigv2.from_legacy_config(old_config)
|
|
1046
|
+
config = SovereignConfigv2(**parse_raw_configuration(config_path))
|
|
1038
1047
|
|
|
1039
1048
|
XDS_TEMPLATES = config.xds_templates()
|
|
1040
|
-
try:
|
|
1041
|
-
default_templates = XDS_TEMPLATES["default"]
|
|
1042
|
-
except KeyError:
|
|
1043
|
-
warnings.warn(
|
|
1044
|
-
"Your configuration should contain default templates. For more details, see "
|
|
1045
|
-
"https://developer.atlassian.com/platform/sovereign/tutorial/templates/#versioning-templates"
|
|
1046
|
-
)
|
|
1047
1049
|
|
|
1048
1050
|
# Create an enum that bases all the available discovery types off what has been configured
|
|
1049
1051
|
discovery_types = (_type for _type in sorted(XDS_TEMPLATES["__any__"].keys()))
|
sovereign/server.py
CHANGED
|
@@ -6,16 +6,15 @@ from pathlib import Path
|
|
|
6
6
|
import uvicorn
|
|
7
7
|
|
|
8
8
|
from sovereign import application_logger as log
|
|
9
|
-
from sovereign.app import app
|
|
10
|
-
from sovereign.worker import worker as worker_app
|
|
11
9
|
from sovereign.schemas import SovereignAsgiConfig, SupervisordConfig
|
|
12
10
|
|
|
13
|
-
|
|
14
11
|
asgi_config = SovereignAsgiConfig()
|
|
15
12
|
supervisord_config = SupervisordConfig()
|
|
16
13
|
|
|
17
14
|
|
|
18
15
|
def web() -> None:
|
|
16
|
+
from sovereign.app import app
|
|
17
|
+
|
|
19
18
|
log.debug("Starting web server")
|
|
20
19
|
uvicorn.run(
|
|
21
20
|
app,
|
|
@@ -30,6 +29,8 @@ def web() -> None:
|
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def worker():
|
|
32
|
+
from sovereign.worker import worker as worker_app
|
|
33
|
+
|
|
33
34
|
log.debug("Starting worker")
|
|
34
35
|
uvicorn.run(
|
|
35
36
|
worker_app,
|
sovereign/sources/poller.py
CHANGED
|
@@ -13,7 +13,7 @@ from sovereign.schemas import ConfiguredSource, SourceData, Node, config
|
|
|
13
13
|
from sovereign.utils.entry_point_loader import EntryPointLoader
|
|
14
14
|
from sovereign.sources.lib import Source
|
|
15
15
|
from sovereign.modifiers.lib import Modifier, GlobalModifier
|
|
16
|
-
from sovereign.
|
|
16
|
+
from sovereign.events import bus, Topic, Event
|
|
17
17
|
|
|
18
18
|
from structlog.stdlib import BoundLogger
|
|
19
19
|
|
|
@@ -206,6 +206,7 @@ class SourcePoller:
|
|
|
206
206
|
self.source_data_modified: SourceData = SourceData()
|
|
207
207
|
self.last_updated = datetime.now()
|
|
208
208
|
self.instance_count = 0
|
|
209
|
+
self.initialized = False
|
|
209
210
|
|
|
210
211
|
self.cache: dict[str, dict[str, list[dict[str, Any]]]] = {}
|
|
211
212
|
self.registry: set[Any] = set()
|
|
@@ -483,17 +484,32 @@ class SourcePoller:
|
|
|
483
484
|
self.cache[node_value] = result
|
|
484
485
|
return result
|
|
485
486
|
|
|
486
|
-
def poll(self) -> None:
|
|
487
|
+
async def poll(self) -> None:
|
|
487
488
|
updated = self.refresh()
|
|
488
489
|
self.source_data_modified = self.apply_modifications(self.source_data)
|
|
490
|
+
if not self.initialized:
|
|
491
|
+
await bus.publish(
|
|
492
|
+
Topic.CONTEXT,
|
|
493
|
+
Event(
|
|
494
|
+
message="Sources initialized",
|
|
495
|
+
metadata={"name": "sources"},
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
self.initialized = True
|
|
489
499
|
if updated:
|
|
490
500
|
self.cache.clear()
|
|
491
|
-
|
|
501
|
+
await bus.publish(
|
|
502
|
+
Topic.CONTEXT,
|
|
503
|
+
Event(
|
|
504
|
+
message="Sources refreshed",
|
|
505
|
+
metadata={"name": "sources"},
|
|
506
|
+
),
|
|
507
|
+
)
|
|
492
508
|
|
|
493
509
|
async def poll_forever(self) -> None:
|
|
494
510
|
while True:
|
|
495
511
|
try:
|
|
496
|
-
self.poll()
|
|
512
|
+
await self.poll()
|
|
497
513
|
|
|
498
514
|
# If we have retry count, use exponential backoff for next attempt
|
|
499
515
|
if self.retry_count > 0:
|
sovereign/statistics.py
CHANGED
|
@@ -51,7 +51,7 @@ def configure_statsd() -> StatsDProxy:
|
|
|
51
51
|
module = DogStatsd()
|
|
52
52
|
if config.enabled and module:
|
|
53
53
|
module.host = config.host
|
|
54
|
-
module.port = config.port
|
|
54
|
+
module.port = int(config.port)
|
|
55
55
|
module.namespace = config.namespace
|
|
56
56
|
module.use_ms = config.use_ms
|
|
57
57
|
for tag, value in config.tags.items():
|