@tetherto/mdk-core 0.0.0 → 0.12.1
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.
- package/LICENSE +202 -0
- package/README.md +572 -0
- package/index.js +519 -0
- package/lib/address-policy.js +124 -0
- package/lib/local-discovery.js +321 -0
- package/lib/services/actions.service.js +68 -0
- package/lib/services/alerts.service.js +207 -0
- package/lib/services/comments.service.js +118 -0
- package/lib/services/log-history.service.js +307 -0
- package/lib/services/logs.service.js +249 -0
- package/lib/services/pool-utils/constants.js +20 -0
- package/lib/services/pool-utils/time.js +49 -0
- package/lib/services/pool.service.js +397 -0
- package/lib/services/provisioning.service.js +420 -0
- package/lib/services/settings.service.js +26 -0
- package/lib/services/snaps.service.js +181 -0
- package/lib/services/stats.service.js +205 -0
- package/lib/stats/README.md +80 -0
- package/lib/stats/index.js +57 -0
- package/lib/stats/ops/alerts_aggr.js +40 -0
- package/lib/stats/ops/alerts_group_cnt.js +43 -0
- package/lib/stats/ops/arr_concat.js +27 -0
- package/lib/stats/ops/array_obj_calc.js +41 -0
- package/lib/stats/ops/avg.js +42 -0
- package/lib/stats/ops/cnt.js +19 -0
- package/lib/stats/ops/group.js +38 -0
- package/lib/stats/ops/group_avg.js +53 -0
- package/lib/stats/ops/group_cnt.js +38 -0
- package/lib/stats/ops/group_max.js +40 -0
- package/lib/stats/ops/group_multiple_stats.js +40 -0
- package/lib/stats/ops/group_sum.js +40 -0
- package/lib/stats/ops/index.js +35 -0
- package/lib/stats/ops/nested_obj_concat.js +54 -0
- package/lib/stats/ops/obj_concat.js +33 -0
- package/lib/stats/ops/sum.js +27 -0
- package/lib/stats/utils.js +99 -0
- package/lib/templates/alerts.js +128 -0
- package/lib/templates/stats.js +476 -0
- package/lib/things/constants.js +65 -0
- package/lib/things/container.js +84 -0
- package/lib/things/miner.js +190 -0
- package/lib/things/powermeter.js +30 -0
- package/lib/things/sensor.js +18 -0
- package/lib/things/thing.js +93 -0
- package/lib/utils.js +603 -0
- package/lib/worker-infra.js +221 -0
- package/package.json +64 -1
- package/services.js +246 -0
- package/utils/compose-yaml.js +56 -0
- package/utils/constants.js +30 -0
- package/utils/index.js +29 -0
- package/utils/initialize.js +148 -0
- package/utils/service-bootstrap.js +271 -0
- package/worker.js +9 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('mdk:worker-infra')
|
|
4
|
+
const StoreFacility = require('@tetherto/hp-svc-facs-store')
|
|
5
|
+
const LogsService = require('./services/logs.service')
|
|
6
|
+
const LogHistoryService = require('./services/log-history.service')
|
|
7
|
+
const SettingsService = require('./services/settings.service')
|
|
8
|
+
const CommentsService = require('./services/comments.service')
|
|
9
|
+
const AlertsService = require('./services/alerts.service')
|
|
10
|
+
const StatsService = require('./services/stats.service')
|
|
11
|
+
const SnapsService = require('./services/snaps.service')
|
|
12
|
+
const ActionsService = require('./services/actions.service')
|
|
13
|
+
const DeviceProvisioningService = require('./services/provisioning.service')
|
|
14
|
+
const gLibStats = require('./stats')
|
|
15
|
+
|
|
16
|
+
const THING_CONF_DEFAULTS = {
|
|
17
|
+
collectSnapsItvMs: 60000,
|
|
18
|
+
rotateLogsItvMs: 120000,
|
|
19
|
+
refreshLogsCacheItvMs: 60000,
|
|
20
|
+
statsRtdItvMs: 30000,
|
|
21
|
+
thingQueryConcurrency: 25,
|
|
22
|
+
storeSnapItvMs: 300000,
|
|
23
|
+
collectSnapTimeoutMs: 120000,
|
|
24
|
+
logKeepCount: 3
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TF_MS = { m: 60000, h: 3600000, D: 86400000 }
|
|
28
|
+
|
|
29
|
+
function _timeframeIntervals () {
|
|
30
|
+
return gLibStats.defaults.timeframes.map(([tf]) => {
|
|
31
|
+
const n = parseInt(tf, 10)
|
|
32
|
+
const unit = tf.slice(String(n).length)
|
|
33
|
+
return [tf, n * (TF_MS[unit] || 60000)]
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the injectable service set for a runtime-hosted worker:
|
|
39
|
+
* store → provisioning (+ one-time seeding) → logs/settings/comments/
|
|
40
|
+
* alerts/stats/log-history/actions/snaps.
|
|
41
|
+
*
|
|
42
|
+
* Device access is late-bound: services capture deviceCall(), which routes
|
|
43
|
+
* through the accessor installed by bindRuntime(runtime) once the
|
|
44
|
+
* WorkerRuntime exists. Boot order is therefore
|
|
45
|
+
* createWorkerInfra → new WorkerRuntime({ services }) → bindRuntime →
|
|
46
|
+
* runtime.start → startTimers
|
|
47
|
+
* and teardown is stop({ runtime }).
|
|
48
|
+
*
|
|
49
|
+
* opts:
|
|
50
|
+
* storeDir (required) persistent store directory
|
|
51
|
+
* deviceType (required) e.g. miner-wm-m56s
|
|
52
|
+
* deviceTags provisioning tags, e.g. ['whatsminer']
|
|
53
|
+
* specTags alert/stat spec lookup tags, e.g. ['miner']
|
|
54
|
+
* baseType stats base type, e.g. 'miner'
|
|
55
|
+
* alertsLib worker alerts template ({ specs })
|
|
56
|
+
* statsLib worker stats template ({ conf, specs })
|
|
57
|
+
* thingConf cadence/behavior overrides merged over THING_CONF_DEFAULTS;
|
|
58
|
+
* `allowedAddressCidrs` (e.g. ['10.0.0.0/8']) is the operator
|
|
59
|
+
* allowlist for device addresses set by remote registerThing/
|
|
60
|
+
* updateThing (default: any address except loopback,
|
|
61
|
+
* link-local/metadata, unspecified, multicast)
|
|
62
|
+
* provisioningConf extra provisioning conf (pools, ...)
|
|
63
|
+
* seedDevices registerThing payloads applied once when the store is empty
|
|
64
|
+
* writeActions whitelist entries [[action, votes], ...]
|
|
65
|
+
* addlStats extra stat cadences [[tf, itvMs], ...];
|
|
66
|
+
* defaults to [['rtd', thingConf.statsRtdItvMs]]
|
|
67
|
+
*/
|
|
68
|
+
async function createWorkerInfra (opts) {
|
|
69
|
+
if (!opts || !opts.storeDir) throw new Error('ERR_STORE_DIR_REQUIRED')
|
|
70
|
+
if (!opts.deviceType) throw new Error('ERR_DEVICE_TYPE_REQUIRED')
|
|
71
|
+
|
|
72
|
+
const thingConf = { ...THING_CONF_DEFAULTS, ...opts.thingConf }
|
|
73
|
+
|
|
74
|
+
const store = new StoreFacility({}, { storeDir: opts.storeDir }, {})
|
|
75
|
+
await new Promise((resolve, reject) => store.start((err) => (err ? reject(err) : resolve())))
|
|
76
|
+
|
|
77
|
+
const db = await store.getBee({ name: 'main' }, { keyEncoding: 'utf-8' })
|
|
78
|
+
await db.ready()
|
|
79
|
+
|
|
80
|
+
let _getDeviceContext = null
|
|
81
|
+
function deviceCall (deviceId, fn) {
|
|
82
|
+
if (!_getDeviceContext) throw new Error('ERR_RUNTIME_NOT_BOUND')
|
|
83
|
+
const ctx = _getDeviceContext(deviceId)
|
|
84
|
+
if (!ctx) throw new Error(`ERR_DEVICE_UNAVAILABLE: ${deviceId}`)
|
|
85
|
+
return fn(ctx.device)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const logs = new LogsService({
|
|
89
|
+
store,
|
|
90
|
+
metaLogs: db.sub('meta_logs_00'),
|
|
91
|
+
conf: thingConf
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const provisioning = new DeviceProvisioningService({
|
|
95
|
+
db: db.sub('things'),
|
|
96
|
+
logs,
|
|
97
|
+
deviceType: opts.deviceType,
|
|
98
|
+
deviceTags: opts.deviceTags || [],
|
|
99
|
+
conf: {
|
|
100
|
+
allowDuplicateIPs: thingConf.allowDuplicateIPs,
|
|
101
|
+
allowedAddressCidrs: thingConf.allowedAddressCidrs,
|
|
102
|
+
...opts.provisioningConf
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
await provisioning.init()
|
|
106
|
+
|
|
107
|
+
let seeded = 0
|
|
108
|
+
if (!provisioning.listDeviceIds().length && Array.isArray(opts.seedDevices)) {
|
|
109
|
+
for (const seed of opts.seedDevices) {
|
|
110
|
+
// Seeds are operator config, not remote input: no address policy.
|
|
111
|
+
await provisioning.registerThing(seed, { trusted: true })
|
|
112
|
+
seeded++
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const settings = new SettingsService({ settingsDb: db.sub('settings') })
|
|
117
|
+
|
|
118
|
+
const comments = new CommentsService({
|
|
119
|
+
loadThing: (req) => provisioning.loadThing(req),
|
|
120
|
+
saveThing: (dev) => provisioning.saveThing(dev),
|
|
121
|
+
checkThingExists: (req) => provisioning.checkThingExists(req),
|
|
122
|
+
generateId: () => provisioning.generateId(),
|
|
123
|
+
// Set once the transport forwards an authenticated identity as ctx.user.
|
|
124
|
+
requireAuthenticatedUser: thingConf.requireAuthenticatedCommentUser === true
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const specTags = opts.specTags || []
|
|
128
|
+
|
|
129
|
+
// snaps ↔ alerts are mutually recursive (alerts persist what snaps collect,
|
|
130
|
+
// snaps evaluate alerts per snapshot) — resolved via this late binding.
|
|
131
|
+
let snaps = null
|
|
132
|
+
|
|
133
|
+
const alerts = new AlertsService({
|
|
134
|
+
logs,
|
|
135
|
+
lib: opts.alertsLib,
|
|
136
|
+
conf: { thing: thingConf },
|
|
137
|
+
specTags,
|
|
138
|
+
listDevices: () => (snaps ? snaps.listLast() : [])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const stats = new StatsService({
|
|
142
|
+
lib: opts.statsLib,
|
|
143
|
+
specTags,
|
|
144
|
+
baseType: opts.baseType,
|
|
145
|
+
listDeviceIds: () => provisioning.listDeviceIds(),
|
|
146
|
+
getDeviceMeta: (id) => provisioning.getDeviceMeta(id),
|
|
147
|
+
getLast: (id) => (snaps ? snaps.getLast(id) : undefined),
|
|
148
|
+
getRealtimeData: (id) => deviceCall(id, (device) => device.getRealtimeData()),
|
|
149
|
+
processAlerts: (dev) => alerts.processThingAlerts(dev),
|
|
150
|
+
logs,
|
|
151
|
+
conf: thingConf
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const logHistory = new LogHistoryService({
|
|
155
|
+
logs,
|
|
156
|
+
getDeviceInfo: (id) => provisioning.getDevice(id),
|
|
157
|
+
statTimeframes: gLibStats.defaults.timeframes,
|
|
158
|
+
conf: thingConf
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const actions = new ActionsService({
|
|
162
|
+
listDevices: () => provisioning.listDevices({ limit: 100000 }),
|
|
163
|
+
validateWriteAction: (dev, action, params) =>
|
|
164
|
+
deviceCall(dev.id, (device) => device.validateWriteAction(action, ...params))
|
|
165
|
+
})
|
|
166
|
+
if (Array.isArray(opts.writeActions)) actions.whitelistActions(opts.writeActions)
|
|
167
|
+
|
|
168
|
+
snaps = new SnapsService({
|
|
169
|
+
listDeviceIds: () => provisioning.listDeviceIds(),
|
|
170
|
+
getDeviceMeta: (id) => provisioning.getDeviceMeta(id),
|
|
171
|
+
collectSnap: (id) => deviceCall(id, (device) => device.getSnap()),
|
|
172
|
+
processAlerts: (dev) => alerts.processThingAlerts(dev),
|
|
173
|
+
saveAlerts: () => alerts.saveAlerts(),
|
|
174
|
+
logs,
|
|
175
|
+
conf: thingConf
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const services = { logs, logHistory, settings, comments, alerts, stats, snaps, actions, provisioning }
|
|
179
|
+
|
|
180
|
+
const addlStats = opts.addlStats || [['rtd', thingConf.statsRtdItvMs]]
|
|
181
|
+
const timers = []
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
store,
|
|
185
|
+
db,
|
|
186
|
+
services,
|
|
187
|
+
seeded,
|
|
188
|
+
thingConf,
|
|
189
|
+
deviceCall,
|
|
190
|
+
bindRuntime (runtime) {
|
|
191
|
+
_getDeviceContext = (deviceId) => runtime.getDeviceContext(deviceId)
|
|
192
|
+
},
|
|
193
|
+
startTimers () {
|
|
194
|
+
snaps.start()
|
|
195
|
+
timers.push(
|
|
196
|
+
setInterval(() => { logs.rotateLogs().catch((e) => debug('rotateLogs: %s', e.message)) }, thingConf.rotateLogsItvMs),
|
|
197
|
+
setInterval(() => { logs.refreshLogsCache().catch((e) => debug('refreshLogsCache: %s', e.message)) }, thingConf.refreshLogsCacheItvMs)
|
|
198
|
+
)
|
|
199
|
+
for (const [tf, itvMs] of addlStats) {
|
|
200
|
+
timers.push(setInterval(() => {
|
|
201
|
+
Promise.resolve(stats.buildStats(`stat-${tf}`)).catch((e) => debug('stat-%s: %s', tf, e.message))
|
|
202
|
+
}, itvMs))
|
|
203
|
+
}
|
|
204
|
+
for (const [tf, ms] of _timeframeIntervals()) {
|
|
205
|
+
timers.push(setInterval(() => {
|
|
206
|
+
Promise.resolve(stats.buildStats(`stat-${tf}`, new Date())).catch((e) => debug('stat-%s: %s', tf, e.message))
|
|
207
|
+
}, ms))
|
|
208
|
+
}
|
|
209
|
+
for (const timer of timers) timer.unref()
|
|
210
|
+
},
|
|
211
|
+
async stop ({ runtime } = {}) {
|
|
212
|
+
snaps.stop()
|
|
213
|
+
for (const timer of timers) clearInterval(timer)
|
|
214
|
+
timers.length = 0
|
|
215
|
+
if (runtime) await runtime.stop()
|
|
216
|
+
await new Promise((resolve) => store.stop(() => resolve()))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { createWorkerInfra, THING_CONF_DEFAULTS }
|
package/package.json
CHANGED
|
@@ -1 +1,64 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@tetherto/mdk-core",
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"description": "MDK bootstrap utilities and worker-infra services — kernel/gateway lifecycle plus injectable log/comment/alert/stats/settings/provisioning services for runtime-hosted workers",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"tether",
|
|
8
|
+
"mdk"
|
|
9
|
+
],
|
|
10
|
+
"author": "Tether",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"lint": "standard",
|
|
17
|
+
"lint:fix": "standard --fix",
|
|
18
|
+
"test": "npm run lint && npm run test:unit && npm run test:integration",
|
|
19
|
+
"test:unit": "NODE_ENV=test brittle tests/unit/**/*.js",
|
|
20
|
+
"test:integration": "NODE_ENV=test brittle 'tests/integration/**/*.test.js'",
|
|
21
|
+
"test:coverage": "NODE_ENV=test brittle --coverage 'tests/**/*.test.js'"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@tetherto/mdk-gateway": "^0.12.1",
|
|
25
|
+
"@tetherto/mdk-kernel": "^0.12.1",
|
|
26
|
+
"@bitfinex/bfx-facs-http": "^1.1.3",
|
|
27
|
+
"@bitfinex/bfx-facs-interval": "^1.0.0",
|
|
28
|
+
"@bitfinex/bfx-facs-scheduler": "^1.0.0",
|
|
29
|
+
"@bitfinex/lib-js-util-base": "^2.1.0",
|
|
30
|
+
"@bitfinex/lib-js-util-promise": "^1.1.0",
|
|
31
|
+
"@tetherto/hp-svc-facs-store": "^1.0.0",
|
|
32
|
+
"@tetherto/svc-facs-action-approver": "^1.0.0",
|
|
33
|
+
"async": "3.2.6",
|
|
34
|
+
"debug": "4.4.1",
|
|
35
|
+
"mingo": "6.5.6",
|
|
36
|
+
"uuid": "14.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"brittle": "3.18.0",
|
|
40
|
+
"hyperdht": "6.34.1",
|
|
41
|
+
"standard": "17.1.0"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/tetherto/mdk.git",
|
|
46
|
+
"directory": "backend/core/mdk"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"index.js",
|
|
53
|
+
"services.js",
|
|
54
|
+
"worker.js",
|
|
55
|
+
"lib/",
|
|
56
|
+
"utils/",
|
|
57
|
+
"README.md"
|
|
58
|
+
],
|
|
59
|
+
"exports": {
|
|
60
|
+
".": "./index.js",
|
|
61
|
+
"./lib/local-discovery": "./lib/local-discovery.js",
|
|
62
|
+
"./package.json": "./package.json"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/services.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const initialize = require('./utils/initialize')
|
|
5
|
+
const { buildComposeYaml } = require('./utils/compose-yaml')
|
|
6
|
+
const { ensureConfigFromExamples, gatewayPackageDir } = require('./utils/service-bootstrap')
|
|
7
|
+
|
|
8
|
+
// Every file startServices writes into the site dir carries this marker. A
|
|
9
|
+
// file without it (or the legacy header) is the user's, and is never
|
|
10
|
+
// overwritten unless config.overwrite is set.
|
|
11
|
+
const GENERATED_MARKER = '@generated by @tetherto/mdk-core startServices'
|
|
12
|
+
const LEGACY_MARKERS = ['Auto-generated by @tetherto/mdk']
|
|
13
|
+
|
|
14
|
+
function _isGenerated (content) {
|
|
15
|
+
return content.includes(GENERATED_MARKER) || LEGACY_MARKERS.some((m) => content.includes(m))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Writes `content` (which must already contain GENERATED_MARKER) to `file`,
|
|
20
|
+
* refusing to replace an existing file that was not generated by us — or
|
|
21
|
+
* whose content is not byte-identical to what we would write (`same`).
|
|
22
|
+
*/
|
|
23
|
+
function writeGenerated (file, content, { overwrite = false, same = null } = {}) {
|
|
24
|
+
if (!overwrite && fs.existsSync(file)) {
|
|
25
|
+
const existing = fs.readFileSync(file, 'utf8')
|
|
26
|
+
if (!_isGenerated(existing) && existing !== same) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`ERR_MDK_GENERATED_FILE_CONFLICT: ${file} exists and was not generated by @tetherto/mdk-core; ` +
|
|
29
|
+
'move it away or pass overwrite: true'
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
34
|
+
fs.writeFileSync(file, content)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Bind a published port to loopback unless the caller names a host.
|
|
38
|
+
function _publishPort (port, host) {
|
|
39
|
+
return `${host || '127.0.0.1'}:${port}:${port}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generates (and optionally starts) a PM2 ecosystem or Docker compose stack
|
|
44
|
+
* in the current working directory.
|
|
45
|
+
*
|
|
46
|
+
* config: { runtime: 'pm2' | 'docker', repoRoot, services, env?, image?, shouldAutoStart?,
|
|
47
|
+
* repoRoot the MDK source checkout the services run from (required;
|
|
48
|
+
* never searched for). Config is seeded from its example
|
|
49
|
+
* files and it is passed to the processes as MDK_REPO_ROOT.
|
|
50
|
+
* host? Docker: interface published gateway ports bind to
|
|
51
|
+
* (default '127.0.0.1'; per-service `svc.host` wins),
|
|
52
|
+
* overwrite? replace site-dir files that were not generated by
|
|
53
|
+
* mdk-core (default false: refuse with ERR_MDK_GENERATED_FILE_CONFLICT) }
|
|
54
|
+
*/
|
|
55
|
+
// The checkout is always named by the caller — never found by walking up
|
|
56
|
+
// from the working directory or this package.
|
|
57
|
+
function _resolveRepoRoot (config) {
|
|
58
|
+
const root = config.repoRoot
|
|
59
|
+
if (typeof root !== 'string' || !root.trim()) {
|
|
60
|
+
throw new Error('ERR_MDK_REPO_ROOT_REQUIRED: startServices needs config.repoRoot — the MDK source checkout the services run from')
|
|
61
|
+
}
|
|
62
|
+
return path.resolve(root)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const startServices = async (config) => {
|
|
66
|
+
if (config.runtime === 'pm2') {
|
|
67
|
+
return startPm2(config)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (config.runtime === 'docker') {
|
|
71
|
+
return startDocker(config)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Error(`Unsupported runtime: ${config.runtime}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _copyGenerated (src, dest, opts) {
|
|
78
|
+
const body = fs.readFileSync(src, 'utf8')
|
|
79
|
+
// The marker goes on line 1 (a comment before 'use strict' keeps the
|
|
80
|
+
// directive prologue intact). An unmarked copy identical to the source —
|
|
81
|
+
// what older versions wrote — still counts as ours.
|
|
82
|
+
writeGenerated(dest, `// ${GENERATED_MARKER} — do not edit; rewritten on every run\n${body}`, { ...opts, same: body })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ensureMdkWorker (cwd = process.cwd(), opts = {}) {
|
|
86
|
+
const mdkDir = path.join(cwd, 'mdk')
|
|
87
|
+
const utilsDir = path.join(mdkDir, 'utils')
|
|
88
|
+
|
|
89
|
+
_copyGenerated(path.join(__dirname, 'worker.js'), path.join(mdkDir, 'worker.js'), opts)
|
|
90
|
+
_copyGenerated(
|
|
91
|
+
path.join(__dirname, 'utils', 'service-bootstrap.js'),
|
|
92
|
+
path.join(utilsDir, 'service-bootstrap.js'),
|
|
93
|
+
opts
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Leftovers of the old pm2 bootstrap — removed only when they are ours.
|
|
97
|
+
for (const legacy of [
|
|
98
|
+
path.join(mdkDir, 'pm2-bootstrap.js'),
|
|
99
|
+
path.join(utilsDir, 'pm2-bootstrap.js')
|
|
100
|
+
]) {
|
|
101
|
+
if (fs.existsSync(legacy) && _isGenerated(fs.readFileSync(legacy, 'utf8'))) fs.unlinkSync(legacy)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return path.join(mdkDir, 'worker.js')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function startPm2 (config) {
|
|
108
|
+
const mdkEnv = config.env || 'development'
|
|
109
|
+
const repoRoot = _resolveRepoRoot(config)
|
|
110
|
+
const writeOpts = { overwrite: config.overwrite === true }
|
|
111
|
+
|
|
112
|
+
initialize({ sources: repoRoot })
|
|
113
|
+
ensureMdkWorker(process.cwd(), writeOpts)
|
|
114
|
+
ensureConfigFromExamples(path.join(gatewayPackageDir(), 'config'))
|
|
115
|
+
|
|
116
|
+
const workerScript = 'mdk/worker.js'
|
|
117
|
+
const sharedEnv = {
|
|
118
|
+
MDK_ENV: mdkEnv,
|
|
119
|
+
MDK_REPO_ROOT: repoRoot,
|
|
120
|
+
NODE_ENV: mdkEnv === 'production' ? 'production' : 'development'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const apps = config.services.map(svc => {
|
|
124
|
+
if (svc.kind === 'gateway') {
|
|
125
|
+
return {
|
|
126
|
+
name: `mdk-${svc.name}`,
|
|
127
|
+
script: workerScript,
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
exec_mode: 'fork',
|
|
130
|
+
instances: 1,
|
|
131
|
+
env: {
|
|
132
|
+
...sharedEnv,
|
|
133
|
+
SERVICE: 'gateway',
|
|
134
|
+
PORT: String(svc.port)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
name: `mdk-${svc.name}`,
|
|
141
|
+
script: workerScript,
|
|
142
|
+
cwd: process.cwd(),
|
|
143
|
+
exec_mode: 'fork',
|
|
144
|
+
instances: 1,
|
|
145
|
+
env: {
|
|
146
|
+
...sharedEnv,
|
|
147
|
+
SERVICE: 'worker',
|
|
148
|
+
WORKER: svc.worker,
|
|
149
|
+
TYPE: svc.type,
|
|
150
|
+
RACK: svc.rack
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// PM2 only treats filenames containing .config.js (or .json/.yaml) as ecosystem files.
|
|
156
|
+
const file = path.join(process.cwd(), 'ecosystem.config.js')
|
|
157
|
+
const content = `// ${GENERATED_MARKER} — do not edit manually
|
|
158
|
+
module.exports = ${JSON.stringify({ apps }, null, 2)}
|
|
159
|
+
`
|
|
160
|
+
|
|
161
|
+
writeGenerated(file, content, writeOpts)
|
|
162
|
+
if (config.shouldAutoStart) {
|
|
163
|
+
execFileSync('pm2', ['start', file], { stdio: 'inherit' })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function startDocker (config) {
|
|
168
|
+
if (!config.image) {
|
|
169
|
+
throw new Error('Docker runtime requires "image"')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const mdkEnv = config.env || 'development'
|
|
173
|
+
const siteDir = process.cwd()
|
|
174
|
+
const repoRoot = _resolveRepoRoot(config)
|
|
175
|
+
const repoMount = path.relative(siteDir, repoRoot).split(path.sep).join('/') || '.'
|
|
176
|
+
|
|
177
|
+
const writeOpts = { overwrite: config.overwrite === true }
|
|
178
|
+
|
|
179
|
+
initialize({ sources: repoRoot })
|
|
180
|
+
ensureMdkWorker(siteDir, writeOpts)
|
|
181
|
+
ensureConfigFromExamples(path.join(gatewayPackageDir(), 'config'))
|
|
182
|
+
|
|
183
|
+
const siteMount = path.relative(repoRoot, siteDir).split(path.sep).join('/') || '.'
|
|
184
|
+
const workingDir = `/app/repo/${siteMount}`
|
|
185
|
+
const entrypoint = path.posix.join('/app/repo', siteMount, 'docker-entrypoint.sh')
|
|
186
|
+
|
|
187
|
+
const sharedEnv = {
|
|
188
|
+
MDK_ENV: mdkEnv,
|
|
189
|
+
MDK_REPO_ROOT: '/app/repo',
|
|
190
|
+
NODE_ENV: mdkEnv === 'production' ? 'production' : 'development'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const services = {}
|
|
194
|
+
|
|
195
|
+
for (const svc of config.services) {
|
|
196
|
+
const base = {
|
|
197
|
+
image: config.image,
|
|
198
|
+
working_dir: workingDir,
|
|
199
|
+
entrypoint: ['sh', entrypoint],
|
|
200
|
+
restart: 'unless-stopped',
|
|
201
|
+
volumes: [`${repoMount}:/app/repo:cached`],
|
|
202
|
+
environment: { ...sharedEnv }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (svc.kind === 'gateway') {
|
|
206
|
+
services[svc.name] = {
|
|
207
|
+
...base,
|
|
208
|
+
environment: {
|
|
209
|
+
...base.environment,
|
|
210
|
+
SERVICE: 'gateway',
|
|
211
|
+
PORT: String(svc.port)
|
|
212
|
+
},
|
|
213
|
+
// Loopback unless an interface is explicitly configured.
|
|
214
|
+
ports: [_publishPort(svc.port, svc.host || config.host)]
|
|
215
|
+
}
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
services[svc.name] = {
|
|
220
|
+
...base,
|
|
221
|
+
environment: {
|
|
222
|
+
...base.environment,
|
|
223
|
+
SERVICE: 'worker',
|
|
224
|
+
WORKER: svc.worker,
|
|
225
|
+
TYPE: svc.type,
|
|
226
|
+
RACK: svc.rack
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const compose = {
|
|
232
|
+
version: '3.8',
|
|
233
|
+
services
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const file = path.join(siteDir, 'docker-compose.generated.yml')
|
|
237
|
+
writeGenerated(file, `# ${GENERATED_MARKER} — do not edit manually\n${buildComposeYaml(compose)}`, writeOpts)
|
|
238
|
+
|
|
239
|
+
if (config.shouldAutoStart) {
|
|
240
|
+
execFileSync('docker', ['compose', '-f', file, 'up', '-d'], { stdio: 'inherit', cwd: siteDir })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { file, repoRoot, siteDir }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { startServices, GENERATED_MARKER }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function yamlScalar (value) {
|
|
4
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
5
|
+
return String(value)
|
|
6
|
+
}
|
|
7
|
+
const str = String(value)
|
|
8
|
+
if (/^[a-zA-Z0-9._/-]+$/.test(str)) return str
|
|
9
|
+
return JSON.stringify(str)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function yamlEnv (env) {
|
|
13
|
+
const lines = [' environment:']
|
|
14
|
+
for (const [key, val] of Object.entries(env)) {
|
|
15
|
+
lines.push(` ${key}: ${yamlScalar(val)}`)
|
|
16
|
+
}
|
|
17
|
+
return lines
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildComposeYaml (compose) {
|
|
21
|
+
const lines = [`version: "${compose.version}"`, 'services:']
|
|
22
|
+
|
|
23
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
24
|
+
lines.push(` ${name}:`)
|
|
25
|
+
lines.push(` image: ${yamlScalar(svc.image)}`)
|
|
26
|
+
if (svc.working_dir) lines.push(` working_dir: ${yamlScalar(svc.working_dir)}`)
|
|
27
|
+
if (svc.entrypoint) {
|
|
28
|
+
lines.push(` entrypoint: [${svc.entrypoint.map((c) => JSON.stringify(c)).join(', ')}]`)
|
|
29
|
+
}
|
|
30
|
+
if (svc.command) {
|
|
31
|
+
const cmd = Array.isArray(svc.command) ? svc.command : [svc.command]
|
|
32
|
+
lines.push(` command: [${cmd.map((c) => JSON.stringify(c)).join(', ')}]`)
|
|
33
|
+
}
|
|
34
|
+
if (svc.restart) lines.push(` restart: ${yamlScalar(svc.restart)}`)
|
|
35
|
+
lines.push(...yamlEnv(svc.environment))
|
|
36
|
+
if (svc.ports?.length) {
|
|
37
|
+
lines.push(' ports:')
|
|
38
|
+
for (const port of svc.ports) lines.push(` - "${port}"`)
|
|
39
|
+
}
|
|
40
|
+
if (svc.volumes?.length) {
|
|
41
|
+
lines.push(' volumes:')
|
|
42
|
+
for (const vol of svc.volumes) lines.push(` - ${vol}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (compose.volumes) {
|
|
47
|
+
lines.push('volumes:')
|
|
48
|
+
for (const volName of Object.keys(compose.volumes)) {
|
|
49
|
+
lines.push(` ${volName}:`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return lines.join('\n') + '\n'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { buildComposeYaml }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const MDK_ROOT = 'tmp'
|
|
4
|
+
const MDK_STORE = 'store'
|
|
5
|
+
const KERNEL_CLUSTER = 'cluster-0'
|
|
6
|
+
const WRK_TYPES = {
|
|
7
|
+
Kernel: 'wrk-kernel',
|
|
8
|
+
GATEWAY: 'wrk-node-http'
|
|
9
|
+
}
|
|
10
|
+
const LIB_TYPES = {
|
|
11
|
+
Kernel: 'core/kernel',
|
|
12
|
+
GATEWAY: 'core/gateway',
|
|
13
|
+
AVALON: 'workers/miners/avalon',
|
|
14
|
+
BITDEER: 'workers/containers/bitdeer',
|
|
15
|
+
ANTSPACE: 'workers/containers/antspace',
|
|
16
|
+
ABB: 'workers/power-meter/abb',
|
|
17
|
+
SATEC: 'workers/power-meter/satec',
|
|
18
|
+
SCHNEIDER: 'workers/power-meter/schneider',
|
|
19
|
+
SENECA: 'workers/temperature',
|
|
20
|
+
OCEAN_POOL: 'workers/minerpools/ocean',
|
|
21
|
+
F2_POOL: 'workers/minerpools/f2pool'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
WRK_TYPES,
|
|
26
|
+
MDK_ROOT,
|
|
27
|
+
MDK_STORE,
|
|
28
|
+
KERNEL_CLUSTER,
|
|
29
|
+
LIB_TYPES
|
|
30
|
+
}
|
package/utils/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const IntervalsFacility = require('@bitfinex/bfx-facs-interval')
|
|
4
|
+
const StoreFacility = require('@tetherto/hp-svc-facs-store')
|
|
5
|
+
const ActionApproverFacility = require('@tetherto/svc-facs-action-approver')
|
|
6
|
+
|
|
7
|
+
const _startFacility = (facility) => {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
facility.start((err) => (err ? reject(err) : resolve()))
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const createFacs = async (storeDir, ctx = {}) => {
|
|
14
|
+
const facs = {}
|
|
15
|
+
facs.interval_0 = new IntervalsFacility(null, {}, ctx)
|
|
16
|
+
await _startFacility(facs.interval_0)
|
|
17
|
+
|
|
18
|
+
facs.store_s1 = new StoreFacility(null, { storeDir }, ctx)
|
|
19
|
+
await _startFacility(facs.store_s1)
|
|
20
|
+
|
|
21
|
+
facs.actionApprover_0 = new ActionApproverFacility(null, {}, ctx)
|
|
22
|
+
await _startFacility(facs.actionApprover_0)
|
|
23
|
+
|
|
24
|
+
return facs
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
createFacs
|
|
29
|
+
}
|