rollbridge 0.1.5 → 0.1.7
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/README.md +125 -4
- package/TODO.md +45 -43
- package/docs/cli.md +166 -6
- package/docs/config.md +172 -2
- package/docs/logging.md +77 -0
- package/docs/releasing.md +53 -0
- package/docs/tensorbuzz-runbook.md +129 -0
- package/docs/velocious.md +49 -11
- package/docs/workers.md +115 -0
- package/package.json +1 -1
- package/src/cli.js +327 -1
- package/src/config.js +268 -6
- package/src/daemon.js +216 -13
- package/src/doctor.js +177 -0
- package/src/event-log.js +47 -0
- package/src/managed-process.js +225 -16
- package/src/predeploy-cleanup.js +340 -0
- package/src/process-memory.js +110 -0
- package/src/recover.js +134 -0
- package/src/release-group.js +71 -21
- package/src/state-store.js +103 -0
- package/src/system-ids.js +71 -0
- package/src/template.js +32 -0
- package/test/completion.test.js +64 -0
- package/test/config-validation.test.js +268 -0
- package/test/doctor.test.js +205 -3
- package/test/event-log.test.js +46 -0
- package/test/fixtures/memory-hog.js +19 -0
- package/test/managed-process.test.js +290 -0
- package/test/predeploy-cleanup.test.js +131 -0
- package/test/process-memory.test.js +40 -0
- package/test/recover.test.js +162 -0
- package/test/release-group.test.js +22 -0
- package/test/rollbridge.test.js +523 -6
- package/test/state-store.test.js +69 -0
- package/test/system-ids.test.js +24 -0
package/src/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs/promises"
|
|
4
|
+
import os from "node:os"
|
|
4
5
|
import path from "node:path"
|
|
5
6
|
import {pathToFileURL} from "node:url"
|
|
6
7
|
|
|
@@ -10,11 +11,15 @@ import {pathToFileURL} from "node:url"
|
|
|
10
11
|
* @typedef {{path: string, startDelayMs: number, timeoutMs: number, intervalMs: number}} HealthConfig
|
|
11
12
|
* @typedef {"proxied" | "companion" | "singleton" | "service"} ProcessPolicy
|
|
12
13
|
* @typedef {{backoffFactor: number, maxDelayMs: number, maxRestarts: number | undefined, windowMs: number}} RestartConfig
|
|
13
|
-
* @typedef {{
|
|
14
|
-
* @typedef {{
|
|
14
|
+
* @typedef {{checkIntervalMs: number, limitBytes: number, warnBytes: number}} MemoryConfig
|
|
15
|
+
* @typedef {{drainCommand?: string, drainTimeoutMs: number, quietCommand?: string, stopCommand?: string}} LifecycleConfig
|
|
16
|
+
* @typedef {{cwd?: string, env: Record<string, string>, gracefulStopMs: number, health?: HealthConfig, id: string, lifecycle: LifecycleConfig, memory?: MemoryConfig, nonBlockingDrain: boolean, outputLines: number, policy: ProcessPolicy, port?: PortRange, replicas: number, restart: RestartConfig, restartDelayMs: number, stopSignal: string, command: string}} ProcessConfig
|
|
17
|
+
* @typedef {{group?: number | string, mode?: number, owner?: number | string, path: string}} ControlConfig
|
|
15
18
|
* @typedef {{drainTimeoutMs: number, forceStopTimeoutMs: number, healthPath: string, healthTimeoutMs: number, host: string, port: number, upstreamHost: string}} ProxyConfig
|
|
19
|
+
* @typedef {{includes: string[], name: string}} LegacyTakeoverProcessConfig
|
|
20
|
+
* @typedef {{forceStopTimeoutMs: number, processes: LegacyTakeoverProcessConfig[], screens: string[]}} LegacyTakeoverConfig
|
|
16
21
|
* @typedef {{keep: number, maxAgeMs: number}} ReleaseRetentionConfig
|
|
17
|
-
* @typedef {{application: string, control: ControlConfig, processes: ProcessConfig[], proxy: ProxyConfig, releaseRetention: ReleaseRetentionConfig}} RollbridgeConfig
|
|
22
|
+
* @typedef {{application: string, control: ControlConfig, legacyTakeover?: LegacyTakeoverConfig, processes: ProcessConfig[], proxy: ProxyConfig, releaseRetention: ReleaseRetentionConfig, statePath?: string}} RollbridgeConfig
|
|
18
23
|
* @typedef {{fix: string, message: string}} ConfigIssue
|
|
19
24
|
*/
|
|
20
25
|
|
|
@@ -125,15 +130,19 @@ export function validateConfig(rawConfig, configPath = process.cwd()) {
|
|
|
125
130
|
const processesSource = arrayAt(source.processes, "processes", issues)
|
|
126
131
|
const proxy = normalizeProxy(proxySource, issues)
|
|
127
132
|
const control = {
|
|
133
|
+
group: normalizeControlPrincipal(controlSource.group, "control.group", issues),
|
|
128
134
|
mode: normalizeSocketMode(controlSource.mode, "control.mode", issues),
|
|
135
|
+
owner: normalizeControlPrincipal(controlSource.owner, "control.owner", issues),
|
|
129
136
|
path: normalizeString(controlSource.path, "control.path", issues, {default: `/tmp/rollbridge-${application}.sock`})
|
|
130
137
|
}
|
|
131
138
|
const processes = processesSource.map((processSource, index) => normalizeProcess(processSource, index, proxy, issues))
|
|
139
|
+
const legacyTakeover = normalizeLegacyTakeover(source.legacyTakeover, proxy, issues)
|
|
132
140
|
const releaseRetention = normalizeReleaseRetention(objectAt(source.releaseRetention, "releaseRetention", issues, {}), issues)
|
|
141
|
+
const statePath = source.statePath === undefined || source.statePath === null ? undefined : normalizeString(source.statePath, "statePath", issues)
|
|
133
142
|
|
|
134
143
|
validateProcessSet(processes, issues)
|
|
135
144
|
|
|
136
|
-
return {config: {application, control, processes, proxy, releaseRetention}, issues}
|
|
145
|
+
return {config: {application, control, legacyTakeover, processes, proxy, releaseRetention, statePath}, issues}
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
/**
|
|
@@ -176,7 +185,7 @@ function normalizeProcess(value, index, proxy, issues) {
|
|
|
176
185
|
if (!isPlainObject(value)) {
|
|
177
186
|
issues.push({fix: `Define processes[${index}] as a mapping with id, policy, and command.`, message: `processes[${index}] must be an object`})
|
|
178
187
|
|
|
179
|
-
return {command: "", cwd: undefined, env: {}, gracefulStopMs: proxy.forceStopTimeoutMs, health: undefined, id: "", outputLines: 50, policy: "companion", port: undefined, restart: defaultRestartConfig(), restartDelayMs: 1000}
|
|
188
|
+
return {command: "", cwd: undefined, env: {}, gracefulStopMs: proxy.forceStopTimeoutMs, health: undefined, id: "", lifecycle: {drainTimeoutMs: 0}, memory: undefined, nonBlockingDrain: false, outputLines: 50, policy: "companion", port: undefined, replicas: 1, restart: defaultRestartConfig(), restartDelayMs: 1000, stopSignal: "SIGTERM"}
|
|
180
189
|
}
|
|
181
190
|
|
|
182
191
|
const source = value
|
|
@@ -188,11 +197,16 @@ function normalizeProcess(value, index, proxy, issues) {
|
|
|
188
197
|
gracefulStopMs: normalizeNumber(source.gracefulStopMs, `processes[${index}].gracefulStopMs`, issues, {default: proxy.forceStopTimeoutMs}),
|
|
189
198
|
health: normalizeHealth(source.health, `processes[${index}].health`, proxy, issues),
|
|
190
199
|
id: normalizeString(source.id, `processes[${index}].id`, issues),
|
|
200
|
+
lifecycle: normalizeLifecycle(source.lifecycle, `processes[${index}].lifecycle`, issues),
|
|
201
|
+
memory: normalizeMemory(source.memory, `processes[${index}].memory`, issues),
|
|
202
|
+
nonBlockingDrain: normalizeBoolean(source.nonBlockingDrain, `processes[${index}].nonBlockingDrain`, issues, false),
|
|
191
203
|
outputLines: normalizeOutputLines(source.outputLines, `processes[${index}].outputLines`, issues),
|
|
192
204
|
policy: normalizePolicy(source.policy, `processes[${index}].policy`, issues),
|
|
193
205
|
port: normalizePortRange(source.port, `processes[${index}].port`, issues),
|
|
206
|
+
replicas: normalizeReplicas(source.replicas, `processes[${index}].replicas`, issues),
|
|
194
207
|
restart: normalizeRestart(source.restart, `processes[${index}].restart`, issues),
|
|
195
|
-
restartDelayMs: normalizeNumber(source.restartDelayMs, `processes[${index}].restartDelayMs`, issues, {default: 1000})
|
|
208
|
+
restartDelayMs: normalizeNumber(source.restartDelayMs, `processes[${index}].restartDelayMs`, issues, {default: 1000}),
|
|
209
|
+
stopSignal: normalizeStopSignal(source.stopSignal, `processes[${index}].stopSignal`, issues)
|
|
196
210
|
}
|
|
197
211
|
}
|
|
198
212
|
|
|
@@ -265,6 +279,184 @@ function normalizeBackoffFactor(value, key, issues) {
|
|
|
265
279
|
return factor
|
|
266
280
|
}
|
|
267
281
|
|
|
282
|
+
/**
|
|
283
|
+
* @param {JsonValue} value - Raw memory supervision config.
|
|
284
|
+
* @param {string} key - Config key.
|
|
285
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
286
|
+
* @returns {MemoryConfig | undefined} Normalized memory config, or undefined when monitoring is off.
|
|
287
|
+
*/
|
|
288
|
+
function normalizeMemory(value, key, issues) {
|
|
289
|
+
if (value === undefined || value === null) return undefined
|
|
290
|
+
|
|
291
|
+
if (!isPlainObject(value)) {
|
|
292
|
+
issues.push({fix: `Set ${key} to a mapping with limitBytes (and optionally warnBytes, checkIntervalMs), or omit it to disable memory supervision.`, message: `${key} must be an object`})
|
|
293
|
+
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const limitBytes = normalizeNumber(value.limitBytes, `${key}.limitBytes`, issues, {default: 0})
|
|
298
|
+
const warnBytes = normalizeNumber(value.warnBytes, `${key}.warnBytes`, issues, {default: 0})
|
|
299
|
+
const checkIntervalMs = normalizeNumber(value.checkIntervalMs, `${key}.checkIntervalMs`, issues, {default: 5000})
|
|
300
|
+
|
|
301
|
+
if (!Number.isInteger(limitBytes) || limitBytes <= 0) {
|
|
302
|
+
issues.push({fix: `Set ${key}.limitBytes to a positive integer number of bytes, e.g. 536870912 (512 MiB).`, message: `${key}.limitBytes must be a positive integer`})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!Number.isInteger(warnBytes) || warnBytes < 0) {
|
|
306
|
+
issues.push({fix: `Set ${key}.warnBytes to a non-negative integer number of bytes below limitBytes, e.g. 402653184 (384 MiB).`, message: `${key}.warnBytes must be a non-negative integer`})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (checkIntervalMs <= 0) {
|
|
310
|
+
issues.push({fix: `Set ${key}.checkIntervalMs to a positive number of milliseconds, e.g. 5000.`, message: `${key}.checkIntervalMs must be a positive number`})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {checkIntervalMs, limitBytes, warnBytes}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {JsonValue} value - Raw lifecycle config.
|
|
318
|
+
* @param {string} key - Config key.
|
|
319
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
320
|
+
* @returns {LifecycleConfig} Normalized lifecycle hooks (no commands and a 0 drain by default).
|
|
321
|
+
*/
|
|
322
|
+
function normalizeLifecycle(value, key, issues) {
|
|
323
|
+
if (value === undefined || value === null) return {drainTimeoutMs: 0}
|
|
324
|
+
|
|
325
|
+
if (!isPlainObject(value)) {
|
|
326
|
+
issues.push({fix: `Set ${key} to a mapping with optional quietCommand, drainCommand, stopCommand, and drainTimeoutMs.`, message: `${key} must be an object`})
|
|
327
|
+
|
|
328
|
+
return {drainTimeoutMs: 0}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const drainTimeoutMs = normalizeNumber(value.drainTimeoutMs, `${key}.drainTimeoutMs`, issues, {default: 0})
|
|
332
|
+
/** @type {LifecycleConfig} */
|
|
333
|
+
const lifecycle = {drainTimeoutMs: nonNegativeOrDefault(drainTimeoutMs, `${key}.drainTimeoutMs`, issues, 0, false)}
|
|
334
|
+
|
|
335
|
+
if (value.quietCommand !== undefined) lifecycle.quietCommand = normalizeString(value.quietCommand, `${key}.quietCommand`, issues)
|
|
336
|
+
if (value.drainCommand !== undefined) lifecycle.drainCommand = normalizeString(value.drainCommand, `${key}.drainCommand`, issues)
|
|
337
|
+
if (value.stopCommand !== undefined) lifecycle.stopCommand = normalizeString(value.stopCommand, `${key}.stopCommand`, issues)
|
|
338
|
+
|
|
339
|
+
if (lifecycle.drainCommand !== undefined && lifecycle.drainTimeoutMs <= 0) {
|
|
340
|
+
issues.push({fix: `Set ${key}.drainTimeoutMs to a positive number to bound ${key}.drainCommand; with 0 the drain step is skipped and the command never runs.`, message: `${key}.drainCommand requires a positive ${key}.drainTimeoutMs`})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return lifecycle
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @param {JsonValue} value - Raw legacy takeover config.
|
|
348
|
+
* @param {ProxyConfig} proxy - Proxy config defaults.
|
|
349
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
350
|
+
* @returns {LegacyTakeoverConfig | undefined} Normalized legacy takeover config, or undefined when omitted.
|
|
351
|
+
*/
|
|
352
|
+
function normalizeLegacyTakeover(value, proxy, issues) {
|
|
353
|
+
if (value === undefined || value === null) return undefined
|
|
354
|
+
|
|
355
|
+
if (!isPlainObject(value)) {
|
|
356
|
+
issues.push({fix: "Set legacyTakeover to a mapping with screens and/or processes.", message: "legacyTakeover must be an object"})
|
|
357
|
+
|
|
358
|
+
return {forceStopTimeoutMs: proxy.forceStopTimeoutMs, processes: [], screens: []}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const forceStopTimeoutMs = normalizeNumber(value.forceStopTimeoutMs, "legacyTakeover.forceStopTimeoutMs", issues, {default: proxy.forceStopTimeoutMs})
|
|
362
|
+
const screens = normalizeStringList(value.screens, "legacyTakeover.screens", issues)
|
|
363
|
+
const processes = normalizeLegacyTakeoverProcesses(value.processes, issues)
|
|
364
|
+
|
|
365
|
+
if (screens.length === 0 && processes.length === 0) {
|
|
366
|
+
issues.push({fix: "Set legacyTakeover.screens or legacyTakeover.processes so predeploy-cleanup knows what legacy processes it may stop.", message: "legacyTakeover must define at least one screen or process matcher"})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
forceStopTimeoutMs: nonNegativeOrDefault(forceStopTimeoutMs, "legacyTakeover.forceStopTimeoutMs", issues, proxy.forceStopTimeoutMs, false),
|
|
371
|
+
processes,
|
|
372
|
+
screens
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param {JsonValue} value - Raw list value.
|
|
378
|
+
* @param {string} key - Config key.
|
|
379
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
380
|
+
* @returns {string[]} Normalized strings.
|
|
381
|
+
*/
|
|
382
|
+
function normalizeStringList(value, key, issues) {
|
|
383
|
+
if (value === undefined || value === null) return []
|
|
384
|
+
|
|
385
|
+
if (!Array.isArray(value)) {
|
|
386
|
+
issues.push({fix: `Set ${key} to a list of strings.`, message: `${key} must be an array`})
|
|
387
|
+
|
|
388
|
+
return []
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return value.flatMap((entry, index) => {
|
|
392
|
+
if (typeof entry === "string" && entry.length > 0) return [entry]
|
|
393
|
+
|
|
394
|
+
issues.push({fix: `Set ${key}[${index}] to a non-empty string.`, message: `${key}[${index}] must be a non-empty string`})
|
|
395
|
+
return []
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @param {JsonValue} value - Raw legacy process matchers.
|
|
401
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
402
|
+
* @returns {LegacyTakeoverProcessConfig[]} Normalized process matchers.
|
|
403
|
+
*/
|
|
404
|
+
function normalizeLegacyTakeoverProcesses(value, issues) {
|
|
405
|
+
if (value === undefined || value === null) return []
|
|
406
|
+
|
|
407
|
+
if (!Array.isArray(value)) {
|
|
408
|
+
issues.push({fix: "Set legacyTakeover.processes to a list of mappings with includes strings.", message: "legacyTakeover.processes must be an array"})
|
|
409
|
+
|
|
410
|
+
return []
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return value.flatMap((entry, index) => normalizeLegacyTakeoverProcess(entry, index, issues))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {JsonValue} value - Raw legacy process matcher.
|
|
418
|
+
* @param {number} index - Matcher index.
|
|
419
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
420
|
+
* @returns {LegacyTakeoverProcessConfig[]} Normalized matcher, or an empty list when invalid.
|
|
421
|
+
*/
|
|
422
|
+
function normalizeLegacyTakeoverProcess(value, index, issues) {
|
|
423
|
+
const key = `legacyTakeover.processes[${index}]`
|
|
424
|
+
|
|
425
|
+
if (!isPlainObject(value)) {
|
|
426
|
+
issues.push({fix: `Set ${key} to a mapping with includes strings.`, message: `${key} must be an object`})
|
|
427
|
+
|
|
428
|
+
return []
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const includes = normalizeStringList(value.includes, `${key}.includes`, issues)
|
|
432
|
+
if (includes.length === 0) {
|
|
433
|
+
issues.push({fix: `Set ${key}.includes to one or more command-line substrings that identify the legacy process.`, message: `${key}.includes must contain at least one matcher`})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return [{
|
|
437
|
+
includes,
|
|
438
|
+
name: normalizeString(value.name, `${key}.name`, issues, {default: `legacy process ${index + 1}`})
|
|
439
|
+
}]
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @param {JsonValue} value - Raw stop signal.
|
|
444
|
+
* @param {string} key - Config key.
|
|
445
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
446
|
+
* @returns {string} The graceful-stop signal name (default "SIGTERM").
|
|
447
|
+
*/
|
|
448
|
+
function normalizeStopSignal(value, key, issues) {
|
|
449
|
+
if (value === undefined || value === null) return "SIGTERM"
|
|
450
|
+
|
|
451
|
+
if (typeof value === "string" && Object.prototype.hasOwnProperty.call(os.constants.signals, value)) {
|
|
452
|
+
return value
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
issues.push({fix: `Set ${key} to a signal name such as "SIGTERM", "SIGINT", or "SIGQUIT".`, message: `${key} must be a valid signal name`})
|
|
456
|
+
|
|
457
|
+
return "SIGTERM"
|
|
458
|
+
}
|
|
459
|
+
|
|
268
460
|
/**
|
|
269
461
|
* @param {JsonValue} value - Raw output retention value.
|
|
270
462
|
* @param {string} key - Config key.
|
|
@@ -283,6 +475,40 @@ function normalizeOutputLines(value, key, issues) {
|
|
|
283
475
|
return outputLines
|
|
284
476
|
}
|
|
285
477
|
|
|
478
|
+
/**
|
|
479
|
+
* @param {JsonValue} value - Raw replica count.
|
|
480
|
+
* @param {string} key - Config key.
|
|
481
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
482
|
+
* @returns {number} Number of replicas to run (default 1).
|
|
483
|
+
*/
|
|
484
|
+
function normalizeReplicas(value, key, issues) {
|
|
485
|
+
const replicas = normalizeNumber(value, key, issues, {default: 1})
|
|
486
|
+
|
|
487
|
+
if (!Number.isInteger(replicas) || replicas < 1) {
|
|
488
|
+
issues.push({fix: `Set ${key} to a positive integer number of replicas, e.g. 1 or 4.`, message: `${key} must be a positive integer`})
|
|
489
|
+
|
|
490
|
+
return 1
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return replicas
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* @param {JsonValue} value - Raw boolean value.
|
|
498
|
+
* @param {string} key - Config key.
|
|
499
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
500
|
+
* @param {boolean} fallback - Default when unset.
|
|
501
|
+
* @returns {boolean} The boolean value, or the fallback when unset/invalid.
|
|
502
|
+
*/
|
|
503
|
+
function normalizeBoolean(value, key, issues, fallback) {
|
|
504
|
+
if (value === undefined || value === null) return fallback
|
|
505
|
+
if (typeof value === "boolean") return value
|
|
506
|
+
|
|
507
|
+
issues.push({fix: `Set ${key} to true or false.`, message: `${key} must be a boolean`})
|
|
508
|
+
|
|
509
|
+
return fallback
|
|
510
|
+
}
|
|
511
|
+
|
|
286
512
|
/**
|
|
287
513
|
* @param {Record<string, JsonValue>} source - Raw release retention config.
|
|
288
514
|
* @param {ConfigIssue[]} issues - Issue collector.
|
|
@@ -339,6 +565,26 @@ function normalizeSocketMode(value, key, issues) {
|
|
|
339
565
|
return undefined
|
|
340
566
|
}
|
|
341
567
|
|
|
568
|
+
/**
|
|
569
|
+
* @param {JsonValue} value - Raw owner/group value.
|
|
570
|
+
* @param {string} key - Config key.
|
|
571
|
+
* @param {ConfigIssue[]} issues - Issue collector.
|
|
572
|
+
* @returns {number | string | undefined} A numeric id, a name (resolved at bind time), or undefined when unset.
|
|
573
|
+
*/
|
|
574
|
+
function normalizeControlPrincipal(value, key, issues) {
|
|
575
|
+
if (value === undefined || value === null) return undefined
|
|
576
|
+
|
|
577
|
+
if (typeof value === "number") {
|
|
578
|
+
if (Number.isInteger(value) && value >= 0) return value
|
|
579
|
+
} else if (typeof value === "string" && value.length > 0) {
|
|
580
|
+
return value
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
issues.push({fix: `Set ${key} to a non-negative integer id or a user/group name, e.g. "deploy".`, message: `${key} must be a non-negative integer id or a name`})
|
|
584
|
+
|
|
585
|
+
return undefined
|
|
586
|
+
}
|
|
587
|
+
|
|
342
588
|
/**
|
|
343
589
|
* Validates cross-process rules: unique ids, exactly one proxied process, and proxied ports.
|
|
344
590
|
* @param {ProcessConfig[]} processes - Normalized processes.
|
|
@@ -356,6 +602,22 @@ function validateProcessSet(processes, issues) {
|
|
|
356
602
|
}
|
|
357
603
|
|
|
358
604
|
seenIds.add(processConfig.id)
|
|
605
|
+
|
|
606
|
+
if (processConfig.id.includes("#")) {
|
|
607
|
+
issues.push({fix: `Remove "#" from the process id "${processConfig.id}"; it is reserved for replica instance ids (e.g. "${processConfig.id.replace(/#/g, "")}#0").`, message: `Process id "${processConfig.id}" must not contain "#"`})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (processConfig.replicas > 1 && (processConfig.policy !== "companion" || processConfig.port)) {
|
|
611
|
+
issues.push({fix: `Run replicas (${processConfig.replicas}) only on a companion process without a port; "${processConfig.id}" is ${processConfig.policy}${processConfig.port ? " with a port" : ""}.`, message: `Process "${processConfig.id}" can only set replicas > 1 on a companion process without a port`})
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (processConfig.nonBlockingDrain && processConfig.policy !== "companion") {
|
|
615
|
+
issues.push({fix: `Set nonBlockingDrain only on a companion process; "${processConfig.id}" is ${processConfig.policy}.`, message: `Process "${processConfig.id}" can only set nonBlockingDrain on a companion process`})
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (processConfig.lifecycle.stopCommand && processConfig.stopSignal !== "SIGTERM") {
|
|
619
|
+
issues.push({fix: `Drop the custom stopSignal "${processConfig.stopSignal}" or lifecycle.stopCommand from "${processConfig.id}": with a stopCommand set, Rollbridge runs it to stop the process instead of sending stopSignal, so the signal is never used.`, message: `Process "${processConfig.id}" sets both lifecycle.stopCommand and a custom stopSignal, but stopCommand replaces stopSignal`})
|
|
620
|
+
}
|
|
359
621
|
}
|
|
360
622
|
|
|
361
623
|
const proxiedProcesses = processes.filter((processConfig) => processConfig.policy === "proxied")
|