rollbridge 0.1.11 → 0.1.12
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 +6 -2
- package/docs/cli.md +8 -0
- package/docs/config.md +22 -0
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/config.js +10 -1
- package/src/daemon.js +56 -20
- package/src/release-group.js +31 -1
- package/test/config-path.test.js +16 -1
- package/test/rollbridge.test.js +92 -3
package/README.md
CHANGED
|
@@ -543,8 +543,12 @@ so its output goes to the journal (`journalctl -u rollbridge`). Key directives:
|
|
|
543
543
|
The daemon is long-lived and survives deploys. **Deploy with
|
|
544
544
|
`rollbridge deploy` (or `rollbridge deploy --ensure-daemon`), not
|
|
545
545
|
`systemctl restart`** — pointing `--config` at a stable, daemon-wide file while
|
|
546
|
-
release paths are passed per deploy.
|
|
547
|
-
|
|
546
|
+
release paths are passed per deploy. The daemon reloads compatible process and
|
|
547
|
+
lifecycle config before each deploy, so updated graceful-stop deadlines govern
|
|
548
|
+
the release being retired without interrupting the stable proxy. Listener and
|
|
549
|
+
process-topology changes still require a daemon restart; see
|
|
550
|
+
[`docs/config.md`](docs/config.md#config-reloads). Use `command -v rollbridge`
|
|
551
|
+
to find the absolute CLI path for `ExecStart`.
|
|
548
552
|
|
|
549
553
|
See [`docs/logging.md`](docs/logging.md) for where the daemon's JSON logs go
|
|
550
554
|
(stdout / journald / the `--daemon-log-path` file) and how to rotate them — the
|
package/docs/cli.md
CHANGED
|
@@ -72,6 +72,14 @@ traffic to it, then drains and stops the previous release. Prints
|
|
|
72
72
|
If the new release fails to start or health-check, the previous release stays
|
|
73
73
|
active and the command errors.
|
|
74
74
|
|
|
75
|
+
Before each deploy, the daemon reloads the config path it was started with.
|
|
76
|
+
Compatible process and lifecycle changes apply to the new release and govern
|
|
77
|
+
how the previous release retires, including updated `nonBlockingDrain`,
|
|
78
|
+
`stopSignal`, `lifecycle`, and `gracefulStopMs` settings. The daemon adopts the
|
|
79
|
+
new config only after the replacement release starts successfully. Changes to
|
|
80
|
+
daemon-owned listeners or process topology fail the deploy with a restart
|
|
81
|
+
instruction; see [Config reloads](config.md#config-reloads).
|
|
82
|
+
|
|
75
83
|
- `--release-path <path>` (**required**) — path to the prepared release
|
|
76
84
|
directory; available to process templates as `{{releasePath}}`.
|
|
77
85
|
- `--release-id <id>` — identifier for the release. Defaults to `--revision`,
|
package/docs/config.md
CHANGED
|
@@ -17,6 +17,28 @@ export default {
|
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
## Config reloads
|
|
21
|
+
|
|
22
|
+
The daemon reloads the config file it was started with before every deploy and
|
|
23
|
+
rollback. Compatible changes apply to the replacement release, daemon-wide
|
|
24
|
+
service restart definitions, and the retirement of the previous release. This
|
|
25
|
+
means changes to process commands, environment, health checks, lifecycle hooks,
|
|
26
|
+
`nonBlockingDrain`, stop signals, graceful-stop timeouts, restart policies, and
|
|
27
|
+
memory supervision do not require restarting the stable proxy daemon. Updated
|
|
28
|
+
retirement settings also apply to processes that were started by the previous
|
|
29
|
+
release.
|
|
30
|
+
|
|
31
|
+
The replacement config is adopted only after the new release starts and passes
|
|
32
|
+
its health check. Invalid config, an incompatible change, or a failed release
|
|
33
|
+
leaves the previous config and release active.
|
|
34
|
+
|
|
35
|
+
Settings that own daemon listeners or change the managed-process topology still
|
|
36
|
+
require a daemon restart: `application`, `control`, `statePath`, `proxy.host`,
|
|
37
|
+
`proxy.port`, `proxy.upstreamHost`, and changes to process ids, count, `policy`,
|
|
38
|
+
`deployStrategy`, `replicas`, or `port`. A deploy with one of these changes
|
|
39
|
+
fails before starting the replacement and names the settings that require a
|
|
40
|
+
restart.
|
|
41
|
+
|
|
20
42
|
## Top-level fields
|
|
21
43
|
|
|
22
44
|
| Field | Type | Default | Description |
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ export async function runCli(argv) {
|
|
|
33
33
|
.action(async (options) => {
|
|
34
34
|
const configPath = await resolveConfigPath(options.config)
|
|
35
35
|
const config = await loadConfig(configPath)
|
|
36
|
-
const daemon = new RollbridgeDaemon({config})
|
|
36
|
+
const daemon = new RollbridgeDaemon({config, configPath})
|
|
37
37
|
|
|
38
38
|
await daemon.start()
|
|
39
39
|
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs/promises"
|
|
4
|
+
import {createHash} from "node:crypto"
|
|
5
|
+
import {createRequire} from "node:module"
|
|
4
6
|
import os from "node:os"
|
|
5
7
|
import path from "node:path"
|
|
6
8
|
import {pathToFileURL} from "node:url"
|
|
@@ -27,6 +29,7 @@ import {pathToFileURL} from "node:url"
|
|
|
27
29
|
|
|
28
30
|
const PROCESS_POLICIES = new Set(["proxied", "companion", "singleton", "service"])
|
|
29
31
|
const DEFAULT_CONFIG_FILENAMES = ["rollbridge.js"]
|
|
32
|
+
const commonJsRequire = createRequire(import.meta.url)
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* Imports a JavaScript config module without validating it.
|
|
@@ -38,7 +41,13 @@ const DEFAULT_CONFIG_FILENAMES = ["rollbridge.js"]
|
|
|
38
41
|
*/
|
|
39
42
|
export async function parseConfigFile(configPath) {
|
|
40
43
|
const absolutePath = path.resolve(configPath)
|
|
41
|
-
const
|
|
44
|
+
const configUrl = pathToFileURL(absolutePath)
|
|
45
|
+
const configSource = await fs.readFile(absolutePath)
|
|
46
|
+
|
|
47
|
+
configUrl.searchParams.set("rollbridgeConfig", createHash("sha256").update(configSource).digest("hex"))
|
|
48
|
+
delete commonJsRequire.cache[commonJsRequire.resolve(absolutePath)]
|
|
49
|
+
|
|
50
|
+
const moduleNamespace = await import(configUrl.href)
|
|
42
51
|
const exported = moduleNamespace.default
|
|
43
52
|
|
|
44
53
|
if (exported === undefined) {
|
package/src/daemon.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import fs from "node:fs/promises"
|
|
4
4
|
import http from "node:http"
|
|
5
5
|
import net from "node:net"
|
|
6
|
+
import {isDeepStrictEqual} from "node:util"
|
|
6
7
|
import httpProxy from "http-proxy"
|
|
8
|
+
import {loadConfig} from "./config.js"
|
|
7
9
|
import EventLog from "./event-log.js"
|
|
8
10
|
import ReleaseGroup from "./release-group.js"
|
|
9
11
|
import {clearState, isProcessAlive, liveProcesses, readState, writeState} from "./state-store.js"
|
|
@@ -23,10 +25,12 @@ export default class RollbridgeDaemon {
|
|
|
23
25
|
/**
|
|
24
26
|
* @param {object} args - Options.
|
|
25
27
|
* @param {import("./config.js").RollbridgeConfig} args.config - Rollbridge config.
|
|
28
|
+
* @param {string} [args.configPath] - Config file path to reload before deploys.
|
|
26
29
|
* @param {(message: string, data?: Record<string, JsonValue>) => void} [args.logger] - Logger.
|
|
27
30
|
*/
|
|
28
|
-
constructor({config, logger}) {
|
|
31
|
+
constructor({config, configPath, logger}) {
|
|
29
32
|
this.config = config
|
|
33
|
+
this.configPath = configPath
|
|
30
34
|
this.eventLog = new EventLog(EVENT_HISTORY_LIMIT)
|
|
31
35
|
|
|
32
36
|
const baseLogger = logger || ((message, data = {}) => console.log(JSON.stringify({at: new Date().toISOString(), data, message})))
|
|
@@ -319,9 +323,13 @@ export default class RollbridgeDaemon {
|
|
|
319
323
|
async deploy({releaseId, releasePath, revision}) {
|
|
320
324
|
if (this.stopping) throw new Error("Rollbridge is shutting down")
|
|
321
325
|
|
|
326
|
+
const nextConfig = this.configPath ? await loadConfig(this.configPath) : this.config
|
|
327
|
+
|
|
328
|
+
this.assertReloadCompatible(nextConfig)
|
|
329
|
+
|
|
322
330
|
const newReleaseId = releaseId || revision || new Date().toISOString().replace(/[^0-9]/g, "")
|
|
323
331
|
const release = new ReleaseGroup({
|
|
324
|
-
config:
|
|
332
|
+
config: nextConfig,
|
|
325
333
|
logger: this.logger,
|
|
326
334
|
releaseId: newReleaseId,
|
|
327
335
|
releasePath,
|
|
@@ -343,6 +351,7 @@ export default class RollbridgeDaemon {
|
|
|
343
351
|
|
|
344
352
|
const previousRelease = this.activeRelease
|
|
345
353
|
|
|
354
|
+
this.config = nextConfig
|
|
346
355
|
this.releases.set(release.releaseId, release)
|
|
347
356
|
release.activate()
|
|
348
357
|
this.activeRelease = release
|
|
@@ -352,7 +361,7 @@ export default class RollbridgeDaemon {
|
|
|
352
361
|
await this.replaceSingletons(release)
|
|
353
362
|
|
|
354
363
|
if (previousRelease) {
|
|
355
|
-
void this.drainAndPrune(previousRelease)
|
|
364
|
+
void this.drainAndPrune(previousRelease, nextConfig)
|
|
356
365
|
}
|
|
357
366
|
|
|
358
367
|
this.persistState()
|
|
@@ -363,6 +372,45 @@ export default class RollbridgeDaemon {
|
|
|
363
372
|
}
|
|
364
373
|
}
|
|
365
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Rejects config changes that require rebinding daemon-owned resources or changing process topology.
|
|
377
|
+
* @param {import("./config.js").RollbridgeConfig} nextConfig - Freshly loaded config.
|
|
378
|
+
* @returns {void}
|
|
379
|
+
*/
|
|
380
|
+
assertReloadCompatible(nextConfig) {
|
|
381
|
+
/** @type {string[]} */
|
|
382
|
+
const restartRequired = []
|
|
383
|
+
|
|
384
|
+
if (nextConfig.application !== this.config.application) restartRequired.push("application")
|
|
385
|
+
if (!isDeepStrictEqual(nextConfig.control, this.config.control)) restartRequired.push("control")
|
|
386
|
+
if (nextConfig.statePath !== this.config.statePath) restartRequired.push("statePath")
|
|
387
|
+
|
|
388
|
+
if (nextConfig.proxy.host !== this.config.proxy.host) restartRequired.push("proxy.host")
|
|
389
|
+
if (nextConfig.proxy.port !== this.config.proxy.port) restartRequired.push("proxy.port")
|
|
390
|
+
if (nextConfig.proxy.upstreamHost !== this.config.proxy.upstreamHost) restartRequired.push("proxy.upstreamHost")
|
|
391
|
+
|
|
392
|
+
if (nextConfig.processes.length !== this.config.processes.length) {
|
|
393
|
+
restartRequired.push("processes")
|
|
394
|
+
} else {
|
|
395
|
+
for (const processConfig of this.config.processes) {
|
|
396
|
+
const nextProcessConfig = nextConfig.processes.find((candidate) => candidate.id === processConfig.id)
|
|
397
|
+
|
|
398
|
+
if (!nextProcessConfig ||
|
|
399
|
+
nextProcessConfig.policy !== processConfig.policy ||
|
|
400
|
+
nextProcessConfig.deployStrategy !== processConfig.deployStrategy ||
|
|
401
|
+
nextProcessConfig.replicas !== processConfig.replicas ||
|
|
402
|
+
!isDeepStrictEqual(nextProcessConfig.port, processConfig.port)) {
|
|
403
|
+
restartRequired.push("processes")
|
|
404
|
+
break
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (restartRequired.length > 0) {
|
|
410
|
+
throw new Error(`Config changes to ${restartRequired.join(", ")} cannot be applied live; restart the Rollbridge daemon before deploying.`)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
366
414
|
/**
|
|
367
415
|
* Rolls back to a previously-active release by re-running the deploy flow on its
|
|
368
416
|
* retained metadata: it re-starts the target release, health-checks it, switches
|
|
@@ -418,7 +466,7 @@ export default class RollbridgeDaemon {
|
|
|
418
466
|
async ensureServices(release, startedServices) {
|
|
419
467
|
await release.allocatePorts()
|
|
420
468
|
|
|
421
|
-
for (const processConfig of
|
|
469
|
+
for (const processConfig of release.config.processes) {
|
|
422
470
|
if (processConfig.policy !== "service" || processConfig.deployStrategy === "handoff") continue
|
|
423
471
|
if (this.services.has(processConfig.id)) continue
|
|
424
472
|
|
|
@@ -473,20 +521,7 @@ export default class RollbridgeDaemon {
|
|
|
473
521
|
|
|
474
522
|
const nextDefinition = release.buildProcess(processConfig, {shouldRestart: () => !this.stopping})
|
|
475
523
|
|
|
476
|
-
service.updateDefinition(
|
|
477
|
-
command: nextDefinition.command,
|
|
478
|
-
cwd: nextDefinition.cwd,
|
|
479
|
-
env: nextDefinition.env,
|
|
480
|
-
lifecycle: nextDefinition.lifecycle,
|
|
481
|
-
logger: nextDefinition.logger,
|
|
482
|
-
memory: nextDefinition.memory,
|
|
483
|
-
outputLines: nextDefinition.outputLines,
|
|
484
|
-
restart: nextDefinition.restart,
|
|
485
|
-
restartDelayMs: nextDefinition.restartDelayMs,
|
|
486
|
-
shouldRestart: nextDefinition.shouldRestart,
|
|
487
|
-
stopSignal: nextDefinition.stopSignal,
|
|
488
|
-
stopTimeoutMs: nextDefinition.stopTimeoutMs
|
|
489
|
-
})
|
|
524
|
+
service.updateDefinition(nextDefinition)
|
|
490
525
|
}
|
|
491
526
|
}
|
|
492
527
|
|
|
@@ -611,11 +646,12 @@ export default class RollbridgeDaemon {
|
|
|
611
646
|
/**
|
|
612
647
|
* Drains and stops a retired release in the background, then prunes stopped releases.
|
|
613
648
|
* @param {ReleaseGroup} release - Release to drain and stop.
|
|
649
|
+
* @param {import("./config.js").RollbridgeConfig} [config] - Refreshed config governing retirement.
|
|
614
650
|
* @returns {Promise<void>} Resolves once drained, stopped, and pruned.
|
|
615
651
|
*/
|
|
616
|
-
async drainAndPrune(release) {
|
|
652
|
+
async drainAndPrune(release, config = this.config) {
|
|
617
653
|
try {
|
|
618
|
-
await release.drainAndStop(
|
|
654
|
+
await release.drainAndStop(config.proxy.drainTimeoutMs, config)
|
|
619
655
|
this.logger("release drained", {releaseId: release.releaseId})
|
|
620
656
|
} catch (error) {
|
|
621
657
|
this.logger("release drain failed", {error: error instanceof Error ? error.message : String(error), releaseId: release.releaseId})
|
package/src/release-group.js
CHANGED
|
@@ -232,6 +232,34 @@ export default class ReleaseGroup extends EventEmitter {
|
|
|
232
232
|
})
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Applies refreshed process definitions before retiring this release.
|
|
237
|
+
* @param {import("./config.js").RollbridgeConfig} config - Current deployment config.
|
|
238
|
+
* @returns {void}
|
|
239
|
+
*/
|
|
240
|
+
refreshProcessDefinitions(config) {
|
|
241
|
+
this.config = config
|
|
242
|
+
this.handoffServiceIds.clear()
|
|
243
|
+
this.nonBlockingDrainIds.clear()
|
|
244
|
+
|
|
245
|
+
for (const processConfig of config.processes) {
|
|
246
|
+
const instances = this.getProcesses(processConfig.id)
|
|
247
|
+
|
|
248
|
+
for (let index = 0; index < instances.length; index += 1) {
|
|
249
|
+
const instance = instances[index]
|
|
250
|
+
const nextDefinition = this.buildProcess(processConfig, {
|
|
251
|
+
count: processConfig.replicas,
|
|
252
|
+
index,
|
|
253
|
+
instanceId: instance.id
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
instance.process.updateDefinition(nextDefinition)
|
|
257
|
+
if (processConfig.policy === "service" && processConfig.deployStrategy === "handoff") this.handoffServiceIds.add(instance.id)
|
|
258
|
+
if (processConfig.nonBlockingDrain) this.nonBlockingDrainIds.add(instance.id)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
235
263
|
/**
|
|
236
264
|
* @param {import("./config.js").ProcessConfig} processConfig - Process config.
|
|
237
265
|
* @param {{count: number, index: number}} replica - Replica index and total count.
|
|
@@ -325,13 +353,15 @@ export default class ReleaseGroup extends EventEmitter {
|
|
|
325
353
|
/**
|
|
326
354
|
* Starts draining and stops once existing connections close or timeout.
|
|
327
355
|
* @param {number} timeoutMs - Drain timeout.
|
|
356
|
+
* @param {import("./config.js").RollbridgeConfig} [config] - Refreshed config governing retirement.
|
|
328
357
|
* @returns {Promise<void>} Resolves when stopped.
|
|
329
358
|
*/
|
|
330
|
-
async drainAndStop(timeoutMs) {
|
|
359
|
+
async drainAndStop(timeoutMs, config = this.config) {
|
|
331
360
|
if (this.state === "stopped") return
|
|
332
361
|
|
|
333
362
|
this.state = "draining"
|
|
334
363
|
this.drainStartedAt = new Date().toISOString()
|
|
364
|
+
this.refreshProcessDefinitions(config)
|
|
335
365
|
|
|
336
366
|
// Stop nonBlockingDrain processes (e.g. job workers) immediately and in the background, so
|
|
337
367
|
// their lifecycle drain runs as soon as the release is retired — in parallel with the
|
package/test/config-path.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs/promises"
|
|
|
5
5
|
import os from "node:os"
|
|
6
6
|
import path from "node:path"
|
|
7
7
|
import test from "node:test"
|
|
8
|
-
import {resolveConfigPath} from "../src/config.js"
|
|
8
|
+
import {loadConfig, resolveConfigPath} from "../src/config.js"
|
|
9
9
|
import {runCli} from "../src/cli.js"
|
|
10
10
|
|
|
11
11
|
const validConfig = {
|
|
@@ -65,6 +65,21 @@ test("resolveConfigPath throws an actionable error when no default config exists
|
|
|
65
65
|
}
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
+
test("loadConfig reloads a changed CommonJS module", async () => {
|
|
69
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-cfgpath-"))
|
|
70
|
+
const configPath = await writeConfigModule(dir)
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
assert.equal((await loadConfig(configPath)).application, "demo")
|
|
74
|
+
|
|
75
|
+
await fs.writeFile(configPath, `module.exports = ${JSON.stringify({...validConfig, application: "updated"}, null, 2)}\n`)
|
|
76
|
+
|
|
77
|
+
assert.equal((await loadConfig(configPath)).application, "updated")
|
|
78
|
+
} finally {
|
|
79
|
+
await fs.rm(dir, {force: true, recursive: true})
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
68
83
|
test("validate CLI command resolves the default config when --config is omitted", async () => {
|
|
69
84
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "rollbridge-cfgpath-"))
|
|
70
85
|
const originalCwd = process.cwd()
|
package/test/rollbridge.test.js
CHANGED
|
@@ -102,6 +102,96 @@ test("failed health check leaves the previous release active", async () => {
|
|
|
102
102
|
}
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
+
test("deploy reloads process config and retires the previous worker with the refreshed timeout", async () => {
|
|
106
|
+
const fixture = await createFixture({nonBlockingDrainWorker: true, workerStopDelayMs: 10000})
|
|
107
|
+
const initialConfig = normalizeConfig({
|
|
108
|
+
...fixture.config,
|
|
109
|
+
processes: fixture.config.processes.map((processConfig) => processConfig.id === "worker"
|
|
110
|
+
? {...processConfig, gracefulStopMs: "indefinite"}
|
|
111
|
+
: processConfig)
|
|
112
|
+
})
|
|
113
|
+
const configPath = await writeConfigFile(initialConfig, fixture.root)
|
|
114
|
+
const daemon = new RollbridgeDaemon({config: initialConfig, configPath, logger: () => {}})
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await daemon.start()
|
|
118
|
+
await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
|
|
119
|
+
|
|
120
|
+
const refreshedConfig = normalizeConfig({
|
|
121
|
+
...initialConfig,
|
|
122
|
+
processes: initialConfig.processes.map((processConfig) => processConfig.id === "worker"
|
|
123
|
+
? {...processConfig, gracefulStopMs: 50}
|
|
124
|
+
: processConfig)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
await writeConfigFile(refreshedConfig, fixture.root)
|
|
128
|
+
await daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"})
|
|
129
|
+
await waitFor(() => statusRelease(daemon, "v1").processes.find((processStatus) => processStatus.id === "worker")?.state === "stopped", 1000)
|
|
130
|
+
|
|
131
|
+
assert.equal(daemon.config.processes.find((processConfig) => processConfig.id === "worker")?.gracefulStopMs, 50)
|
|
132
|
+
assert.equal(await fetchText(daemon, "/release"), "v2")
|
|
133
|
+
} finally {
|
|
134
|
+
await daemon.shutdown()
|
|
135
|
+
await fs.rm(fixture.root, {force: true, recursive: true})
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test("deploy rejects a reloaded config that changes the running proxy", async () => {
|
|
140
|
+
const fixture = await createFixture()
|
|
141
|
+
const configPath = await writeConfigFile(fixture.config, fixture.root)
|
|
142
|
+
const daemon = new RollbridgeDaemon({config: fixture.config, configPath, logger: () => {}})
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await daemon.start()
|
|
146
|
+
await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
|
|
147
|
+
await writeConfigFile(normalizeConfig({
|
|
148
|
+
...fixture.config,
|
|
149
|
+
proxy: {...fixture.config.proxy, host: "0.0.0.0"}
|
|
150
|
+
}), fixture.root)
|
|
151
|
+
|
|
152
|
+
await assert.rejects(
|
|
153
|
+
() => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
|
|
154
|
+
/proxy\.host.*restart the Rollbridge daemon/
|
|
155
|
+
)
|
|
156
|
+
assert.equal(daemon.status().activeReleaseId, "v1")
|
|
157
|
+
assert.equal(await fetchText(daemon, "/release"), "v1")
|
|
158
|
+
} finally {
|
|
159
|
+
await daemon.shutdown()
|
|
160
|
+
await fs.rm(fixture.root, {force: true, recursive: true})
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test("a failed deploy does not adopt reloaded process config", async () => {
|
|
165
|
+
const fixture = await createFixture()
|
|
166
|
+
const configPath = await writeConfigFile(fixture.config, fixture.root)
|
|
167
|
+
const daemon = new RollbridgeDaemon({config: fixture.config, configPath, logger: () => {}})
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await daemon.start()
|
|
171
|
+
await daemon.deploy({releaseId: "v1", releasePath: fixture.root, revision: "v1"})
|
|
172
|
+
|
|
173
|
+
const failingConfig = normalizeConfig({
|
|
174
|
+
...fixture.config,
|
|
175
|
+
processes: fixture.config.processes.map((processConfig) => processConfig.id === "web"
|
|
176
|
+
? {...processConfig, health: {...processConfig.health, path: "/not-ready", timeoutMs: 100}}
|
|
177
|
+
: processConfig)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
await writeConfigFile(failingConfig, fixture.root)
|
|
181
|
+
await assert.rejects(
|
|
182
|
+
() => daemon.deploy({releaseId: "v2", releasePath: fixture.root, revision: "v2"}),
|
|
183
|
+
/Health check failed/
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
assert.equal(daemon.config.processes.find((processConfig) => processConfig.id === "web")?.health?.path, "/ping")
|
|
187
|
+
assert.equal(daemon.status().activeReleaseId, "v1")
|
|
188
|
+
assert.equal(await fetchText(daemon, "/release"), "v1")
|
|
189
|
+
} finally {
|
|
190
|
+
await daemon.shutdown()
|
|
191
|
+
await fs.rm(fixture.root, {force: true, recursive: true})
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
105
195
|
test("wildcard proxy bind host targets release processes through loopback", async () => {
|
|
106
196
|
const fixture = await createFixture({proxyHost: "0.0.0.0"})
|
|
107
197
|
const daemon = await startDaemon(fixture.config)
|
|
@@ -1276,10 +1366,9 @@ async function processEvents(logPath) {
|
|
|
1276
1366
|
* @returns {Promise<string>} Written config path.
|
|
1277
1367
|
*/
|
|
1278
1368
|
async function writeConfigFile(config, root) {
|
|
1279
|
-
const configPath = path.join(root, "rollbridge.
|
|
1369
|
+
const configPath = path.join(root, "rollbridge.mjs")
|
|
1280
1370
|
|
|
1281
|
-
|
|
1282
|
-
await fs.writeFile(configPath, `module.exports = ${JSON.stringify(config, null, 2)}\n`)
|
|
1371
|
+
await fs.writeFile(configPath, `export default ${JSON.stringify(config, null, 2)}\n`)
|
|
1283
1372
|
|
|
1284
1373
|
return configPath
|
|
1285
1374
|
}
|