@yarkivaev/scada 2.3.113 → 2.3.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +53 -0
  2. package/charts/scada/Chart.yaml +2 -2
  3. package/charts/scada/templates/_helpers.tpl +12 -4
  4. package/charts/scada/templates/clickhouse.yaml +3 -3
  5. package/charts/scada/templates/collector.yaml +2 -2
  6. package/charts/scada/templates/namespace.yaml +6 -0
  7. package/charts/scada/templates/postgres.yaml +3 -3
  8. package/charts/scada/templates/processes.yaml +88 -0
  9. package/charts/scada/templates/rabbitmq-config.yaml +91 -0
  10. package/charts/scada/templates/rabbitmq.yaml +54 -107
  11. package/charts/scada/templates/site-server.yaml +5 -3
  12. package/charts/scada/templates/supervisor-sink.yaml +8 -2
  13. package/charts/scada/values.schema.json +22 -0
  14. package/charts/scada/values.yaml +22 -0
  15. package/index.js +3 -0
  16. package/package.json +5 -3
  17. package/src/application/deploy/collectorProcesses.js +42 -0
  18. package/src/application/deploy/expandFlows.js +21 -10
  19. package/src/application/deploy/publishedStreams.js +30 -0
  20. package/src/application/deploy/releaseEdges.js +13 -16
  21. package/src/application/deploy/releaseNodes.js +13 -0
  22. package/src/application/deploy/releaseValues.js +5 -15
  23. package/src/application/deploy/releases.js +3 -1
  24. package/src/application/deploy/subscriptions.js +20 -0
  25. package/src/application/supervisorSink.js +6 -0
  26. package/src/domain/segment/segmentCoalesce.js +26 -10
  27. package/src/infrastructure/deploy/executeRelease.js +60 -0
  28. package/src/infrastructure/ingest/codecs/segmentCodec.js +10 -3
  29. package/src/infrastructure/ingest/pipelines/segmentPipeline.js +3 -1
  30. package/src/infrastructure/messaging/rabbitmq/definitions.js +24 -0
  31. package/src/infrastructure/messaging/rabbitmq/reconcile.js +104 -0
@@ -0,0 +1,104 @@
1
+ /** This module is self-contained so clients can ship it to an existing Node.js pod. */
2
+ function canonical(value) {
3
+ if (Array.isArray(value)) { return value.map(canonical); }
4
+ if (value && typeof value === 'object') {
5
+ return Object.fromEntries(Object.keys(value).sort().map((key) => {
6
+ return [key, canonical(value[key])];
7
+ }));
8
+ }
9
+ return value;
10
+ }
11
+
12
+ /** Runtime counters and other fields outside the declaration do not require a write. */
13
+ function same(actual, desired) {
14
+ return actual && Object.keys(desired).every((key) => {
15
+ return JSON.stringify(canonical(actual[key])) === JSON.stringify(canonical(desired[key]));
16
+ });
17
+ }
18
+
19
+ function apiAt({ base, username, password }) {
20
+ if (!base || typeof username !== 'string' || typeof password !== 'string') {
21
+ throw new Error('management base, username and password are required');
22
+ }
23
+ const auth = Buffer.from(`${username}:${password}`).toString('base64');
24
+ return async (method, path, body) => {
25
+ const response = await fetch(`${base.replace(/\/$/u, '')}/api/${path}`, { method,
26
+ headers: { authorization: `Basic ${auth}`, 'content-type': 'application/json' },
27
+ body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(15000) });
28
+ if (!response.ok) { throw new Error(`broker ${method} ${path}: HTTP ${response.status}`); }
29
+ const text = await response.text();
30
+ return text ? JSON.parse(text) : null;
31
+ };
32
+ }
33
+
34
+ /** A conflicting immutable declaration fails in place, preserving the stored queue. */
35
+ async function declare(api, path, desired) {
36
+ const current = await api('GET', path);
37
+ const changes = desired.filter((item) => {
38
+ return !same(current.find((row) => { return row.name === item.name; }), item);
39
+ });
40
+ await Promise.all(changes.map((item) => { return api('PUT', `${path}/${encodeURIComponent(item.name)}`, item); }));
41
+ return changes.length;
42
+ }
43
+
44
+ function identity(item) {
45
+ return { source: item.source, destination: item.destination, kind: item.destination_type || 'queue',
46
+ key: item.routing_key, arguments: item.arguments || {} };
47
+ }
48
+
49
+ function bindingPath(path, item) {
50
+ const kind = item.destination_type === 'exchange' ? 'e' : 'q';
51
+ return `${path}/e/${encodeURIComponent(item.source)}/${kind}/${encodeURIComponent(item.destination)}`;
52
+ }
53
+
54
+ async function bindings(api, path, desired, ownsBinding) {
55
+ const current = await api('GET', path);
56
+ function includes(rows, item) {
57
+ return rows.some((row) => { return same(identity(row), identity(item)); });
58
+ }
59
+ const missing = desired.filter((item) => { return !includes(current, item); });
60
+ const stale = current.filter((item) => { return item.source && ownsBinding?.(item) && !includes(desired, item); });
61
+ await Promise.all(missing.map((item) => {
62
+ return api('POST', bindingPath(path, item), { routing_key: item.routing_key, arguments: item.arguments || {} });
63
+ }));
64
+ await Promise.all(stale.map((item) => {
65
+ return api('DELETE', `${bindingPath(path, item)}/${encodeURIComponent(item.properties_key)}`);
66
+ }));
67
+ return missing.length + stale.length;
68
+ }
69
+
70
+ async function shovels(api, path, desired, ownsShovel) {
71
+ const current = await api('GET', path);
72
+ const changed = desired.filter((item) => {
73
+ const found = current.find((row) => { return row.name === item.name; });
74
+ return !same(found, { value: item.value });
75
+ });
76
+ const stale = current.filter((item) => {
77
+ return ownsShovel?.(item) && !desired.some((row) => { return row.name === item.name; });
78
+ });
79
+ await Promise.all(changed.map((item) => {
80
+ return api('PUT', `${path}/${encodeURIComponent(item.name)}`, { value: item.value });
81
+ }));
82
+ await Promise.all(stale.map((item) => { return api('DELETE', `${path}/${encodeURIComponent(item.name)}`); }));
83
+ return changed.length + stale.length;
84
+ }
85
+
86
+ /** Apply only topology belonging to the selected virtual host. Users and permissions are bootstrap data. */
87
+ export default async function reconcileRabbitmq(desired, options) {
88
+ const { vhost, ownsBinding, ownsShovel } = options;
89
+ if (!vhost) { throw new Error('a virtual host is required'); }
90
+ const api = apiAt(options);
91
+ const host = encodeURIComponent(vhost);
92
+ const objects = [...desired.exchanges, ...desired.queues, ...desired.bindings, ...desired.parameters];
93
+ if (objects.some((item) => { return item.vhost !== undefined && item.vhost !== vhost; })) {
94
+ throw new Error('topology contains a different virtual host');
95
+ }
96
+ if (desired.parameters.some((item) => { return item.component !== 'shovel'; })) {
97
+ throw new Error('only shovel parameters can be reconciled');
98
+ }
99
+ const exchanges = await declare(api, `exchanges/${host}`, desired.exchanges);
100
+ const queues = await declare(api, `queues/${host}`, desired.queues);
101
+ const links = await bindings(api, `bindings/${host}`, desired.bindings, ownsBinding);
102
+ const parameters = await shovels(api, `parameters/shovel/${host}`, desired.parameters, ownsShovel);
103
+ return exchanges + queues + links + parameters;
104
+ }