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.
- sovereign/__init__.py +13 -81
- sovereign/app.py +59 -48
- sovereign/cache/__init__.py +172 -0
- sovereign/cache/backends/__init__.py +110 -0
- sovereign/cache/backends/s3.py +143 -0
- sovereign/cache/filesystem.py +73 -0
- sovereign/cache/types.py +15 -0
- sovereign/configuration.py +573 -0
- sovereign/constants.py +1 -0
- sovereign/context.py +271 -104
- sovereign/dynamic_config/__init__.py +113 -0
- sovereign/dynamic_config/deser.py +78 -0
- sovereign/dynamic_config/loaders.py +120 -0
- sovereign/events.py +49 -0
- sovereign/logging/access_logger.py +85 -0
- sovereign/logging/application_logger.py +54 -0
- sovereign/logging/base_logger.py +41 -0
- sovereign/logging/bootstrapper.py +36 -0
- sovereign/logging/types.py +10 -0
- sovereign/middlewares.py +8 -7
- sovereign/modifiers/lib.py +1 -0
- sovereign/rendering.py +192 -0
- sovereign/response_class.py +18 -0
- sovereign/server.py +93 -35
- sovereign/sources/file.py +1 -1
- sovereign/sources/inline.py +1 -0
- sovereign/sources/lib.py +1 -0
- sovereign/sources/poller.py +296 -53
- sovereign/statistics.py +17 -20
- sovereign/templates/base.html +59 -46
- sovereign/templates/resources.html +203 -102
- sovereign/testing/loaders.py +8 -0
- sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
- sovereign/tracing.py +102 -0
- sovereign/types.py +299 -0
- sovereign/utils/auth.py +26 -13
- sovereign/utils/crypto/__init__.py +0 -0
- sovereign/utils/crypto/crypto.py +135 -0
- sovereign/utils/crypto/suites/__init__.py +21 -0
- sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
- sovereign/utils/crypto/suites/base_cipher.py +21 -0
- sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
- sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
- sovereign/utils/dictupdate.py +2 -1
- sovereign/utils/eds.py +37 -21
- sovereign/utils/mock.py +54 -16
- sovereign/utils/resources.py +17 -0
- sovereign/utils/version_info.py +8 -0
- sovereign/views/__init__.py +4 -0
- sovereign/views/api.py +61 -0
- sovereign/views/crypto.py +46 -15
- sovereign/views/discovery.py +37 -116
- sovereign/views/healthchecks.py +87 -18
- sovereign/views/interface.py +112 -112
- sovereign/worker.py +204 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +79 -76
- sovereign-1.0.0b148.dist-info/RECORD +77 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/WHEEL +1 -1
- sovereign-1.0.0b148.dist-info/entry_points.txt +38 -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/panel.js +76 -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/config_loader.py +0 -225
- sovereign/discovery.py +0 -175
- sovereign/logs.py +0 -131
- sovereign/schemas.py +0 -780
- sovereign/static/sass/style.scss +0 -27
- sovereign/static/style.css +0 -13553
- sovereign/templates/ul_filter.html +0 -22
- sovereign/utils/crypto.py +0 -103
- sovereign/views/admin.py +0 -120
- sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
- sovereign-0.19.3.dist-info/RECORD +0 -47
- sovereign-0.19.3.dist-info/entry_points.txt +0 -10
sovereign/server.py
CHANGED
|
@@ -1,49 +1,107 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
from sovereign.app import app
|
|
6
|
-
from sovereign.utils.entry_point_loader import EntryPointLoader
|
|
1
|
+
import warnings
|
|
2
|
+
import tempfile
|
|
3
|
+
import configparser
|
|
4
|
+
from pathlib import Path
|
|
7
5
|
|
|
6
|
+
import uvicorn
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
from sovereign import application_logger as log
|
|
9
|
+
from sovereign.configuration import SovereignAsgiConfig, SupervisordConfig
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
) -> None:
|
|
15
|
-
self.loader = EntryPointLoader(*self._HOOKS)
|
|
16
|
-
self.options = options or {}
|
|
17
|
-
self.application = application
|
|
18
|
-
super().__init__()
|
|
11
|
+
asgi_config = SovereignAsgiConfig()
|
|
12
|
+
supervisord_config = SupervisordConfig()
|
|
19
13
|
|
|
20
|
-
def load_config(self) -> None:
|
|
21
|
-
for key, value in self.options.items():
|
|
22
|
-
self.cfg.set(key.lower(), value)
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
def web() -> None:
|
|
16
|
+
from sovereign.app import app
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
log.debug("Starting web server")
|
|
19
|
+
uvicorn.run(
|
|
20
|
+
app,
|
|
21
|
+
fd=0,
|
|
22
|
+
log_level=asgi_config.log_level,
|
|
23
|
+
access_log=False,
|
|
24
|
+
timeout_keep_alive=asgi_config.keepalive,
|
|
25
|
+
host=asgi_config.host,
|
|
26
|
+
port=asgi_config.port,
|
|
27
|
+
workers=1, # per managed supervisor proc
|
|
28
|
+
)
|
|
31
29
|
|
|
32
|
-
def master_hook(server: Any, worker: Any) -> None:
|
|
33
|
-
for hook in hooks:
|
|
34
|
-
hook(server, worker)
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
def worker():
|
|
32
|
+
from sovereign.worker import worker as worker_app
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
log.debug("Starting worker")
|
|
35
|
+
uvicorn.run(
|
|
36
|
+
worker_app,
|
|
37
|
+
log_level=asgi_config.log_level,
|
|
38
|
+
access_log=False,
|
|
39
|
+
timeout_keep_alive=asgi_config.keepalive,
|
|
40
|
+
host="127.0.0.1",
|
|
41
|
+
port=9080,
|
|
42
|
+
workers=1, # per managed supervisor proc
|
|
43
|
+
)
|
|
40
44
|
|
|
41
45
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
def write_supervisor_conf() -> Path:
|
|
47
|
+
proc_env = {
|
|
48
|
+
"LANG": "en_US.UTF-8",
|
|
49
|
+
"LC_ALL": "en_US.UTF-8",
|
|
50
|
+
}
|
|
51
|
+
base = {
|
|
52
|
+
"autostart": "true",
|
|
53
|
+
"autorestart": "true",
|
|
54
|
+
"stdout_logfile": "/dev/stdout",
|
|
55
|
+
"stdout_logfile_maxbytes": "0",
|
|
56
|
+
"stderr_logfile": "/dev/stderr",
|
|
57
|
+
"stderr_logfile_maxbytes": "0",
|
|
58
|
+
"stopsignal": "QUIT",
|
|
59
|
+
"environment": ",".join(["=".join((k, v)) for k, v in proc_env.items()]),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
conf = configparser.RawConfigParser()
|
|
63
|
+
conf["supervisord"] = supervisord = {
|
|
64
|
+
"nodaemon": str(supervisord_config.nodaemon).lower(),
|
|
65
|
+
"loglevel": supervisord_config.loglevel,
|
|
66
|
+
"pidfile": supervisord_config.pidfile,
|
|
67
|
+
"logfile": supervisord_config.logfile,
|
|
68
|
+
"directory": supervisord_config.directory,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
conf["fcgi-program:web"] = web = {
|
|
72
|
+
**base,
|
|
73
|
+
"socket": f"tcp://{asgi_config.host}:{asgi_config.port}",
|
|
74
|
+
"numprocs": str(asgi_config.workers),
|
|
75
|
+
"process_name": "%(program_name)s-%(process_num)02d",
|
|
76
|
+
"command": "sovereign-web", # default niceness, higher CPU priority
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
conf["program:data"] = worker = {
|
|
80
|
+
**base,
|
|
81
|
+
"numprocs": "1",
|
|
82
|
+
"command": "nice -n 2 sovereign-worker", # run worker with reduced CPU priority (higher niceness value)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if user := asgi_config.user:
|
|
86
|
+
supervisord["user"] = user
|
|
87
|
+
web["user"] = user
|
|
88
|
+
worker["user"] = user
|
|
89
|
+
|
|
90
|
+
log.debug("Writing supervisor config")
|
|
91
|
+
with tempfile.NamedTemporaryFile("w", delete=False) as f:
|
|
92
|
+
conf.write(f)
|
|
93
|
+
log.debug("Supervisor config written out")
|
|
94
|
+
return Path(f.name)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
path = write_supervisor_conf()
|
|
99
|
+
with warnings.catch_warnings():
|
|
100
|
+
warnings.simplefilter("ignore")
|
|
101
|
+
from supervisor import supervisord
|
|
102
|
+
|
|
103
|
+
log.debug("Starting processes")
|
|
104
|
+
supervisord.main(["-c", path])
|
|
47
105
|
|
|
48
106
|
|
|
49
107
|
if __name__ == "__main__":
|
sovereign/sources/file.py
CHANGED
sovereign/sources/inline.py
CHANGED
sovereign/sources/lib.py
CHANGED
sovereign/sources/poller.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
1
3
|
import asyncio
|
|
2
4
|
import traceback
|
|
3
5
|
from copy import deepcopy
|
|
@@ -6,15 +8,16 @@ from datetime import timedelta, datetime
|
|
|
6
8
|
from typing import Iterable, Any, Dict, List, Union, Type, Optional
|
|
7
9
|
|
|
8
10
|
from glom import glom, PathAccessError
|
|
11
|
+
from sovereign.statistics import StatsDProxy
|
|
9
12
|
|
|
13
|
+
from sovereign.types import Node
|
|
14
|
+
from sovereign.configuration import ConfiguredSource, SourceData, config
|
|
10
15
|
from sovereign.utils.entry_point_loader import EntryPointLoader
|
|
11
16
|
from sovereign.sources.lib import Source
|
|
12
17
|
from sovereign.modifiers.lib import Modifier, GlobalModifier
|
|
13
|
-
from sovereign.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Node,
|
|
17
|
-
)
|
|
18
|
+
from sovereign.events import bus, Topic, Event
|
|
19
|
+
|
|
20
|
+
from structlog.stdlib import BoundLogger
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def is_debug_request(v: str, debug: bool = False) -> bool:
|
|
@@ -33,15 +36,153 @@ Mods = Dict[str, Type[Modifier]]
|
|
|
33
36
|
GMods = Dict[str, Type[GlobalModifier]]
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
def _deep_diff(old, new, path="") -> list[dict[str, Any]]:
|
|
40
|
+
changes: list[dict[str, Any]] = []
|
|
41
|
+
|
|
42
|
+
# handle add/remove
|
|
43
|
+
if (old, new) == (None, None):
|
|
44
|
+
return changes
|
|
45
|
+
elif old is None:
|
|
46
|
+
changes.append({"op": "add", "path": path, "value": new})
|
|
47
|
+
return changes
|
|
48
|
+
elif new is None:
|
|
49
|
+
changes.append({"op": "remove", "path": path, "old_value": old})
|
|
50
|
+
return changes
|
|
51
|
+
|
|
52
|
+
# handle completely different types
|
|
53
|
+
if type(old) is not type(new):
|
|
54
|
+
changes.append(
|
|
55
|
+
{"op": "change", "path": path, "old_value": old, "new_value": new}
|
|
56
|
+
)
|
|
57
|
+
return changes
|
|
58
|
+
|
|
59
|
+
# handle fields recursively
|
|
60
|
+
if isinstance(old, dict) and isinstance(new, dict):
|
|
61
|
+
all_keys = set(old.keys()) | set(new.keys())
|
|
62
|
+
|
|
63
|
+
for key in sorted(all_keys):
|
|
64
|
+
old_val = old.get(key)
|
|
65
|
+
new_val = new.get(key)
|
|
66
|
+
|
|
67
|
+
current_path = f"{path}.{key}" if path else key
|
|
68
|
+
|
|
69
|
+
if key not in old:
|
|
70
|
+
changes.append({"op": "add", "path": current_path, "value": new_val})
|
|
71
|
+
elif key not in new:
|
|
72
|
+
changes.append(
|
|
73
|
+
{"op": "remove", "path": current_path, "old_value": old_val}
|
|
74
|
+
)
|
|
75
|
+
elif old_val != new_val:
|
|
76
|
+
nested_changes = _deep_diff(old_val, new_val, current_path)
|
|
77
|
+
changes.extend(nested_changes)
|
|
78
|
+
|
|
79
|
+
# handle items recursively
|
|
80
|
+
elif isinstance(old, list) and isinstance(new, list):
|
|
81
|
+
max_len = max(len(old), len(new))
|
|
82
|
+
|
|
83
|
+
for i in range(max_len):
|
|
84
|
+
current_path = f"{path}[{i}]" if path else f"[{i}]"
|
|
85
|
+
|
|
86
|
+
if i >= len(old):
|
|
87
|
+
changes.append({"op": "add", "path": current_path, "value": new[i]})
|
|
88
|
+
elif i >= len(new):
|
|
89
|
+
changes.append(
|
|
90
|
+
{"op": "remove", "path": current_path, "old_value": old[i]}
|
|
91
|
+
)
|
|
92
|
+
elif old[i] != new[i]:
|
|
93
|
+
nested_changes = _deep_diff(old[i], new[i], current_path)
|
|
94
|
+
changes.extend(nested_changes)
|
|
95
|
+
|
|
96
|
+
# handle primitives
|
|
97
|
+
else:
|
|
98
|
+
if old != new:
|
|
99
|
+
changes.append(
|
|
100
|
+
{"op": "change", "path": path, "old_value": old, "new_value": new}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return changes
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def per_field_diff(old, new) -> list[dict[str, Any]]:
|
|
107
|
+
changes = []
|
|
108
|
+
max_len = max(len(old), len(new))
|
|
109
|
+
|
|
110
|
+
for i in range(max_len):
|
|
111
|
+
old_inst = old[i] if i < len(old) else None
|
|
112
|
+
new_inst = new[i] if i < len(new) else None
|
|
113
|
+
|
|
114
|
+
if old_inst is None:
|
|
115
|
+
changes.append({"op": "add", "path": f"[{i}]", "value": new_inst})
|
|
116
|
+
elif new_inst is None:
|
|
117
|
+
changes.append({"op": "remove", "path": f"[{i}]", "old_value": old_inst})
|
|
118
|
+
elif old_inst != new_inst:
|
|
119
|
+
# Use the deep diff with index prefix
|
|
120
|
+
field_changes = _deep_diff(old_inst, new_inst, f"[{i}]")
|
|
121
|
+
changes.extend(field_changes)
|
|
122
|
+
|
|
123
|
+
return changes
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _gen_uuid(diff_summary: dict[str, Any]) -> str:
|
|
127
|
+
blob = json.dumps(diff_summary, sort_keys=True, separators=("", ""))
|
|
128
|
+
return str(uuid.uuid5(uuid.NAMESPACE_DNS, blob))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def source_diff_summary(prev, curr) -> dict[str, Any]:
|
|
132
|
+
if prev is None:
|
|
133
|
+
summary = {
|
|
134
|
+
"type": "initial_load",
|
|
135
|
+
"scopes": {
|
|
136
|
+
scope: {"added": len(instances)}
|
|
137
|
+
for scope, instances in curr.scopes.items()
|
|
138
|
+
if instances
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
else:
|
|
142
|
+
summary = {"type": "update", "scopes": {}}
|
|
143
|
+
|
|
144
|
+
all_scopes = set(prev.scopes.keys()) | set(curr.scopes.keys())
|
|
145
|
+
|
|
146
|
+
for scope in sorted(all_scopes):
|
|
147
|
+
old = prev.scopes.get(scope, [])
|
|
148
|
+
new = curr.scopes.get(scope, [])
|
|
149
|
+
|
|
150
|
+
n_old = len(old)
|
|
151
|
+
n_new = len(new)
|
|
152
|
+
|
|
153
|
+
scope_changes: dict[str, Any] = {}
|
|
154
|
+
|
|
155
|
+
if n_old == 0 and n_new > 0:
|
|
156
|
+
scope_changes["added"] = n_new
|
|
157
|
+
elif n_old > 0 and n_new == 0:
|
|
158
|
+
scope_changes["removed"] = n_old
|
|
159
|
+
elif old != new:
|
|
160
|
+
detailed_changes = per_field_diff(old, new)
|
|
161
|
+
if detailed_changes:
|
|
162
|
+
scope_changes["field_changes"] = detailed_changes
|
|
163
|
+
scope_changes["count_change"] = n_new - n_old
|
|
164
|
+
|
|
165
|
+
if scope_changes:
|
|
166
|
+
summary["scopes"][scope] = scope_changes # type: ignore
|
|
167
|
+
|
|
168
|
+
if not summary["scopes"]:
|
|
169
|
+
summary = {"type": "no_changes"}
|
|
170
|
+
|
|
171
|
+
summary["uuid"] = _gen_uuid(summary)
|
|
172
|
+
return summary
|
|
173
|
+
|
|
174
|
+
|
|
36
175
|
class SourcePoller:
|
|
176
|
+
stats: StatsDProxy
|
|
177
|
+
|
|
37
178
|
def __init__(
|
|
38
179
|
self,
|
|
39
180
|
sources: List[ConfiguredSource],
|
|
40
181
|
matching_enabled: bool,
|
|
41
|
-
node_match_key: str,
|
|
42
|
-
source_match_key: str,
|
|
182
|
+
node_match_key: Optional[str],
|
|
183
|
+
source_match_key: Optional[str],
|
|
43
184
|
source_refresh_rate: int,
|
|
44
|
-
logger:
|
|
185
|
+
logger: BoundLogger,
|
|
45
186
|
stats: Any,
|
|
46
187
|
):
|
|
47
188
|
self.matching_enabled = matching_enabled
|
|
@@ -65,9 +206,17 @@ class SourcePoller:
|
|
|
65
206
|
self.global_modifiers: GMods = dict()
|
|
66
207
|
|
|
67
208
|
# initially set data and modify
|
|
68
|
-
self.source_data =
|
|
209
|
+
self.source_data: SourceData = SourceData()
|
|
210
|
+
self.source_data_modified: SourceData = SourceData()
|
|
69
211
|
self.last_updated = datetime.now()
|
|
70
212
|
self.instance_count = 0
|
|
213
|
+
self.initialized = False
|
|
214
|
+
|
|
215
|
+
self.cache: dict[str, dict[str, list[dict[str, Any]]]] = {}
|
|
216
|
+
self.registry: set[Any] = set()
|
|
217
|
+
|
|
218
|
+
# Retry state
|
|
219
|
+
self.retry_count = 0
|
|
71
220
|
|
|
72
221
|
@property
|
|
73
222
|
def data_is_stale(self) -> bool:
|
|
@@ -102,7 +251,7 @@ class SourcePoller:
|
|
|
102
251
|
ret = dict()
|
|
103
252
|
for entry_point in entry_points:
|
|
104
253
|
if entry_point.name in configured_modifiers:
|
|
105
|
-
self.logger(
|
|
254
|
+
self.logger.debug(f"Loading modifier {entry_point.name}")
|
|
106
255
|
ret[entry_point.name] = entry_point.load()
|
|
107
256
|
loaded = len(ret)
|
|
108
257
|
configured = len(configured_modifiers)
|
|
@@ -118,7 +267,7 @@ class SourcePoller:
|
|
|
118
267
|
ret = dict()
|
|
119
268
|
for entry_point in entry_points:
|
|
120
269
|
if entry_point.name in configured_modifiers:
|
|
121
|
-
self.logger(
|
|
270
|
+
self.logger.debug(f"Loading global modifier {entry_point.name}")
|
|
122
271
|
ret[entry_point.name] = entry_point.load()
|
|
123
272
|
|
|
124
273
|
loaded = len(ret)
|
|
@@ -130,55 +279,98 @@ class SourcePoller:
|
|
|
130
279
|
return ret
|
|
131
280
|
|
|
132
281
|
def apply_modifications(self, data: Optional[SourceData]) -> SourceData:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
282
|
+
if data is None:
|
|
283
|
+
data = self.source_data
|
|
284
|
+
if len(self.modifiers) or len(self.global_modifiers):
|
|
285
|
+
try:
|
|
286
|
+
with self.stats.timed("modifiers.apply_ms"):
|
|
287
|
+
data = deepcopy(data)
|
|
288
|
+
for scope, instances in data.scopes.items():
|
|
289
|
+
for g in self.global_modifiers.values():
|
|
290
|
+
global_modifier = g(instances)
|
|
291
|
+
global_modifier.apply()
|
|
292
|
+
data.scopes[scope] = global_modifier.join()
|
|
293
|
+
|
|
294
|
+
for instance in data.scopes[scope]:
|
|
295
|
+
for m in self.modifiers.values():
|
|
296
|
+
modifier = m(instance)
|
|
297
|
+
if modifier.match():
|
|
298
|
+
# Modifies the instance in-place
|
|
299
|
+
modifier.apply()
|
|
300
|
+
self.stats.increment("modifiers.apply.success")
|
|
301
|
+
|
|
302
|
+
except Exception:
|
|
303
|
+
self.stats.increment("modifiers.apply.failure")
|
|
304
|
+
raise
|
|
305
|
+
|
|
306
|
+
return data
|
|
307
|
+
|
|
308
|
+
def refresh(self) -> bool:
|
|
153
309
|
self.stats.increment("sources.attempt")
|
|
310
|
+
|
|
311
|
+
# Get retry config from global source config
|
|
312
|
+
max_retries = config.source_config.max_retries
|
|
313
|
+
|
|
154
314
|
try:
|
|
155
315
|
new = SourceData()
|
|
156
316
|
for source in self.sources:
|
|
157
|
-
|
|
317
|
+
scope = source.scope
|
|
318
|
+
if scope not in new.scopes:
|
|
319
|
+
new.scopes[scope] = []
|
|
320
|
+
new.scopes[scope].extend(source.get())
|
|
158
321
|
except Exception as e:
|
|
159
|
-
self.
|
|
160
|
-
|
|
322
|
+
self.retry_count += 1
|
|
323
|
+
self.logger.error(
|
|
324
|
+
event=f"Error while refreshing sources (attempt {self.retry_count}/{max_retries})",
|
|
161
325
|
traceback=[line for line in traceback.format_exc().split("\n")],
|
|
162
326
|
error=e.__class__.__name__,
|
|
163
327
|
detail=getattr(e, "detail", "-"),
|
|
328
|
+
retry_count=self.retry_count,
|
|
164
329
|
)
|
|
165
330
|
self.stats.increment("sources.error")
|
|
166
|
-
|
|
331
|
+
|
|
332
|
+
if self.retry_count >= max_retries:
|
|
333
|
+
# Reset retry count for next cycle
|
|
334
|
+
self.retry_count = 0
|
|
335
|
+
self.stats.increment("sources.error.final")
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
# Success - reset retry count
|
|
339
|
+
self.retry_count = 0
|
|
167
340
|
|
|
168
341
|
# Is the new data the same as what we currently have
|
|
169
342
|
if new == getattr(self, "source_data", None):
|
|
170
343
|
self.stats.increment("sources.unchanged")
|
|
171
344
|
self.last_updated = datetime.now()
|
|
172
|
-
return
|
|
345
|
+
return False
|
|
173
346
|
else:
|
|
174
347
|
self.stats.increment("sources.refreshed")
|
|
175
348
|
self.last_updated = datetime.now()
|
|
349
|
+
old_data = getattr(self, "source_data", None)
|
|
176
350
|
self.instance_count = len(
|
|
177
351
|
[instance for scope in new.scopes.values() for instance in scope]
|
|
178
352
|
)
|
|
179
|
-
|
|
353
|
+
|
|
354
|
+
if config.logging.log_source_diffs:
|
|
355
|
+
diff_summary = source_diff_summary(old_data, new)
|
|
356
|
+
# printing json directly because the logger is fucking stupid
|
|
357
|
+
print(
|
|
358
|
+
json.dumps(
|
|
359
|
+
dict(
|
|
360
|
+
event="Sources refreshed with changes",
|
|
361
|
+
level="info",
|
|
362
|
+
diff=diff_summary,
|
|
363
|
+
total_instances=self.instance_count,
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
self.source_data = new
|
|
369
|
+
return True
|
|
180
370
|
|
|
181
371
|
def extract_node_key(self, node: Union[Node, Dict[Any, Any]]) -> Any:
|
|
372
|
+
if self.node_match_key is None:
|
|
373
|
+
return
|
|
182
374
|
if "." not in self.node_match_key:
|
|
183
375
|
# key is not nested, don't need glom
|
|
184
376
|
node_value = getattr(node, self.node_match_key)
|
|
@@ -187,13 +379,13 @@ class SourcePoller:
|
|
|
187
379
|
node_value = glom(node, self.node_match_key)
|
|
188
380
|
except PathAccessError:
|
|
189
381
|
raise RuntimeError(
|
|
190
|
-
f'Failed to find key "{self.node_match_key}" in discoveryRequest({node})
|
|
191
|
-
f"See the docs for more info: "
|
|
192
|
-
f"https://vsyrakis.bitbucket.io/sovereign/docs/html/guides/node_matching.html"
|
|
382
|
+
f'Failed to find key "{self.node_match_key}" in discoveryRequest({node})'
|
|
193
383
|
)
|
|
194
384
|
return node_value
|
|
195
385
|
|
|
196
386
|
def extract_source_key(self, source: Dict[Any, Any]) -> Any:
|
|
387
|
+
if self.source_match_key is None:
|
|
388
|
+
return
|
|
197
389
|
if "." not in self.source_match_key:
|
|
198
390
|
# key is not nested, don't need glom
|
|
199
391
|
source_value = source[self.source_match_key]
|
|
@@ -202,9 +394,7 @@ class SourcePoller:
|
|
|
202
394
|
source_value = glom(source, self.source_match_key)
|
|
203
395
|
except PathAccessError:
|
|
204
396
|
raise RuntimeError(
|
|
205
|
-
f'Failed to find key "{self.source_match_key}" in instance({source})
|
|
206
|
-
f"See the docs for more info: "
|
|
207
|
-
f"https://vsyrakis.bitbucket.io/sovereign/docs/html/guides/node_matching.html"
|
|
397
|
+
f'Failed to find key "{self.source_match_key}" in instance({source})'
|
|
208
398
|
)
|
|
209
399
|
return source_value
|
|
210
400
|
|
|
@@ -221,18 +411,14 @@ class SourcePoller:
|
|
|
221
411
|
if self.data_is_stale:
|
|
222
412
|
# Log/emit metric and manually refresh sources.
|
|
223
413
|
self.stats.increment("sources.stale")
|
|
224
|
-
self.logger(
|
|
225
|
-
|
|
414
|
+
self.logger.debug(
|
|
415
|
+
"Sources have not been refreshed in 2 minutes",
|
|
226
416
|
last_update=self.last_updated,
|
|
227
417
|
instance_count=self.instance_count,
|
|
228
418
|
)
|
|
229
|
-
self.poll()
|
|
230
419
|
|
|
231
420
|
ret = SourceData()
|
|
232
|
-
|
|
233
421
|
if modify:
|
|
234
|
-
if not hasattr(self, "source_data_modified"):
|
|
235
|
-
self.poll()
|
|
236
422
|
data = self.source_data_modified
|
|
237
423
|
else:
|
|
238
424
|
data = self.source_data
|
|
@@ -257,6 +443,8 @@ class SourcePoller:
|
|
|
257
443
|
or is_debug_request(node_value)
|
|
258
444
|
)
|
|
259
445
|
if match:
|
|
446
|
+
if scope not in ret.scopes:
|
|
447
|
+
ret.scopes[scope] = []
|
|
260
448
|
ret.scopes[scope].append(instance)
|
|
261
449
|
return ret
|
|
262
450
|
|
|
@@ -284,11 +472,66 @@ class SourcePoller:
|
|
|
284
472
|
ret[source_value] = None
|
|
285
473
|
return list(ret.keys())
|
|
286
474
|
|
|
287
|
-
def
|
|
288
|
-
|
|
475
|
+
def add_to_context(self, request, output):
|
|
476
|
+
"""middleware for adding matched instances to context"""
|
|
477
|
+
node_value = self.extract_node_key(request.node)
|
|
478
|
+
self.registry.add(node_value)
|
|
479
|
+
|
|
480
|
+
if instances := self.cache.get(node_value, None):
|
|
481
|
+
output.update(instances)
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
result = self.get_filtered_instances(node_value)
|
|
485
|
+
output.update(result)
|
|
486
|
+
|
|
487
|
+
def get_filtered_instances(self, node_value):
|
|
488
|
+
matches = self.match_node(node_value=node_value)
|
|
489
|
+
result = {}
|
|
490
|
+
for scope, instances in matches.scopes.items():
|
|
491
|
+
if scope in ("default", None):
|
|
492
|
+
result["instances"] = instances
|
|
493
|
+
else:
|
|
494
|
+
result[scope] = instances
|
|
495
|
+
self.cache[node_value] = result
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
async def poll(self) -> None:
|
|
499
|
+
updated = self.refresh()
|
|
289
500
|
self.source_data_modified = self.apply_modifications(self.source_data)
|
|
501
|
+
if not self.initialized:
|
|
502
|
+
await bus.publish(
|
|
503
|
+
Topic.CONTEXT,
|
|
504
|
+
Event(
|
|
505
|
+
message="Sources initialized",
|
|
506
|
+
metadata={"name": "sources"},
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
self.initialized = True
|
|
510
|
+
if updated:
|
|
511
|
+
self.cache.clear()
|
|
512
|
+
await bus.publish(
|
|
513
|
+
Topic.CONTEXT,
|
|
514
|
+
Event(
|
|
515
|
+
message="Sources refreshed",
|
|
516
|
+
metadata={"name": "sources"},
|
|
517
|
+
),
|
|
518
|
+
)
|
|
290
519
|
|
|
291
520
|
async def poll_forever(self) -> None:
|
|
292
521
|
while True:
|
|
293
|
-
|
|
294
|
-
|
|
522
|
+
try:
|
|
523
|
+
await self.poll()
|
|
524
|
+
|
|
525
|
+
# If we have retry count, use exponential backoff for next attempt
|
|
526
|
+
if self.retry_count > 0:
|
|
527
|
+
retry_delay = config.source_config.retry_delay
|
|
528
|
+
delay = min(
|
|
529
|
+
retry_delay * (2 ** (self.retry_count - 1)),
|
|
530
|
+
self.source_refresh_rate, # Cap at normal refresh rate
|
|
531
|
+
)
|
|
532
|
+
await asyncio.sleep(delay)
|
|
533
|
+
else:
|
|
534
|
+
await asyncio.sleep(self.source_refresh_rate)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
self.logger.error(f"Unexpected error in poll loop: {e}")
|
|
537
|
+
await asyncio.sleep(self.source_refresh_rate)
|