redkite 0.1.10 → 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 +147 -31
- package/dist/build.d.ts +3 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +14 -7
- package/dist/build.js.map +1 -1
- package/dist/cli/crash.d.ts +46 -0
- package/dist/cli/crash.d.ts.map +1 -0
- package/dist/cli/crash.js +202 -0
- package/dist/cli/crash.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +38 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/log.d.ts +1 -0
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +1 -1
- package/dist/cli/log.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -0
- package/dist/config.js.map +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +73 -9
- package/dist/deploy.js.map +1 -1
- package/dist/docker.d.ts +1 -0
- package/dist/docker.d.ts.map +1 -1
- package/dist/docker.js +5 -0
- package/dist/docker.js.map +1 -1
- package/dist/steps.d.ts +0 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +4 -12
- package/dist/steps.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +23 -11
- package/src/cli/crash.ts +286 -0
- package/src/cli/index.ts +43 -11
- package/src/cli/log.ts +1 -1
- package/src/config.ts +69 -0
- package/src/deploy.ts +89 -9
- package/src/docker.ts +6 -0
- package/src/steps.ts +4 -19
- package/src/types.ts +19 -0
package/src/cli/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { environmentOf } from "../config.js";
|
|
1
|
+
import { environmentOf, withEnvironment } from "../config.js";
|
|
2
2
|
import { deploy, verify } from "../deploy.js";
|
|
3
3
|
import { Docker } from "../docker.js";
|
|
4
4
|
import type { Host } from "../host.js";
|
|
@@ -17,6 +17,7 @@ import type { Deployment, DeployHost } from "../types.js";
|
|
|
17
17
|
import { needsAgent, requireAgent } from "./agent.js";
|
|
18
18
|
import { loadConfig, projectRoot } from "./config.js";
|
|
19
19
|
import { loadDotenv } from "./dotenv.js";
|
|
20
|
+
import { dumpCrash, recording } from "./crash.js";
|
|
20
21
|
import { createLog, describeFailure } from "./log.js";
|
|
21
22
|
|
|
22
23
|
// One command that reads the config and does everything under it: the agent,
|
|
@@ -42,7 +43,7 @@ of the project, and everything below it is derived.
|
|
|
42
43
|
|
|
43
44
|
// Wraps a host rather than living inside one, so both implementations are
|
|
44
45
|
// measured the same way and neither knows it is being timed
|
|
45
|
-
function measured(host: Host,
|
|
46
|
+
function measured(host: Host, say?: (line: string) => void) {
|
|
46
47
|
const totals = { commands: 0, commandMs: 0, files: 0 };
|
|
47
48
|
|
|
48
49
|
const wrapped: Host = {
|
|
@@ -61,7 +62,7 @@ function measured(host: Host, log?: Log) {
|
|
|
61
62
|
|
|
62
63
|
// The exit code matters: several of these are allowed to fail, and a
|
|
63
64
|
// deploy reading verbose output is one where somebody wants to know which
|
|
64
|
-
|
|
65
|
+
say?.(` $ ${command} ${Date.now() - started}ms exit ${result.code}`);
|
|
65
66
|
return result;
|
|
66
67
|
},
|
|
67
68
|
|
|
@@ -211,7 +212,7 @@ function asLog(say: (message: string) => void): Log {
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
async function plan(environment: string, configPath?: string, read: string[] = []) {
|
|
214
|
-
const config = await loadConfig(configPath);
|
|
215
|
+
const config = withEnvironment(await loadConfig(configPath), environment);
|
|
215
216
|
const topology = topologyFor(config, environment);
|
|
216
217
|
|
|
217
218
|
const say = (message = "") => process.stdout.write(`${message}\n`);
|
|
@@ -549,7 +550,9 @@ async function run(
|
|
|
549
550
|
configPath: string | undefined,
|
|
550
551
|
options: RunOptions,
|
|
551
552
|
) {
|
|
552
|
-
|
|
553
|
+
// Folded in before anything opens, so an environment naming an app that is
|
|
554
|
+
// not there is refused before a connection or a vault
|
|
555
|
+
const config = withEnvironment(await loadConfig(configPath), environment);
|
|
553
556
|
|
|
554
557
|
// Fail on a missing environment before opening anything for it
|
|
555
558
|
const topology = topologyFor(config, environment);
|
|
@@ -566,10 +569,37 @@ async function run(
|
|
|
566
569
|
// failure path, and the finally below removes the scratch directory
|
|
567
570
|
const stopping = new AbortController();
|
|
568
571
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
572
|
+
// Everything the run says still reaches the view. The recording is what a
|
|
573
|
+
// crash log is written from, since the view keeps only each step's tail
|
|
574
|
+
const recorder = recording(
|
|
575
|
+
createLog({
|
|
576
|
+
...options,
|
|
577
|
+
onQuit: () => stop(),
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const log = recorder.log;
|
|
582
|
+
|
|
583
|
+
// Before the view closes, so where the log went is among what it writes out.
|
|
584
|
+
// A log that cannot be written must not replace the failure it was recording
|
|
585
|
+
const dumped = async (outcome: string, error?: unknown) => {
|
|
586
|
+
try {
|
|
587
|
+
const path = await dumpCrash(config, recorder.transcript, {
|
|
588
|
+
project: config.project,
|
|
589
|
+
environment,
|
|
590
|
+
command: kind,
|
|
591
|
+
version: await version(),
|
|
592
|
+
argv: process.argv.slice(2),
|
|
593
|
+
outcome,
|
|
594
|
+
at: Date.now(),
|
|
595
|
+
error,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (path) log.warn(`Crash logs written to ${path}`);
|
|
599
|
+
} catch (failure) {
|
|
600
|
+
log.warn(`Could not write the crash log: ${describeFailure(failure).split("\n")[0]}`);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
573
603
|
|
|
574
604
|
// Read by the wait below, so a press during it hardens what that is sending
|
|
575
605
|
let hardest: "TERM" | "KILL" = "TERM";
|
|
@@ -600,7 +630,7 @@ async function run(
|
|
|
600
630
|
try {
|
|
601
631
|
const meter = measured(
|
|
602
632
|
await hostFor(deployHost, log, stopping.signal),
|
|
603
|
-
options.verbose ? log :
|
|
633
|
+
options.verbose ? log : recorder.command,
|
|
604
634
|
);
|
|
605
635
|
|
|
606
636
|
host = meter.host;
|
|
@@ -631,15 +661,17 @@ async function run(
|
|
|
631
661
|
|
|
632
662
|
log.fail(`Reverted ${result.reverted.join(", ")}`);
|
|
633
663
|
process.exitCode = 1;
|
|
664
|
+
await dumped("reverted");
|
|
634
665
|
} catch (error) {
|
|
635
666
|
// Whatever the command in flight said about being killed is noise: what
|
|
636
|
-
// happened is that somebody asked for it to stop
|
|
667
|
+
// happened is that somebody asked for it to stop, which is not a crash
|
|
637
668
|
if (stopping.signal.aborted) {
|
|
638
669
|
log.fail("Stopped");
|
|
639
670
|
process.exitCode = 130;
|
|
640
671
|
} else {
|
|
641
672
|
log.fail(describeFailure(error));
|
|
642
673
|
process.exitCode = 1;
|
|
674
|
+
await dumped("failed", error);
|
|
643
675
|
}
|
|
644
676
|
} finally {
|
|
645
677
|
// Nothing leaves while the build is still running. The signal a press
|
package/src/cli/log.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PROXY } from "./services/proxy.js";
|
|
2
2
|
import { assertSteps } from "./pipeline.js";
|
|
3
3
|
import { pluginSteps } from "./plugin.js";
|
|
4
|
+
import { listRefs } from "./secrets/refs.js";
|
|
5
|
+
import type { AnyStep } from "./pipeline.js";
|
|
4
6
|
import type { Deployment, Environment } from "./types.js";
|
|
5
7
|
|
|
6
8
|
// The one selected, from the one place worth looking. A deployment that carries
|
|
@@ -10,6 +12,72 @@ export function environmentOf(config: Deployment, name: string) {
|
|
|
10
12
|
return config.environment ?? config.environments?.[name];
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
// The selected environment's items folded into the apps they name, after each
|
|
16
|
+
// app's own. Refs merge in order and the later key wins, so the deployment holds
|
|
17
|
+
// what every environment shares and the environment holds what differs
|
|
18
|
+
export function withEnvironment(config: Deployment, name: string): Deployment {
|
|
19
|
+
const environment = environmentOf(config, name);
|
|
20
|
+
if (!environment?.secrets && !environment?.files && !environment?.steps) return config;
|
|
21
|
+
|
|
22
|
+
const { secrets = {}, files = {}, steps, ...rest } = environment;
|
|
23
|
+
assertNamesApps(config, name, [...Object.keys(secrets), ...Object.keys(files)]);
|
|
24
|
+
|
|
25
|
+
const apps = config.apps.map((app) => {
|
|
26
|
+
const refs = secrets[app.name];
|
|
27
|
+
const paths = files[app.name];
|
|
28
|
+
if (!refs && !paths) return app;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...app,
|
|
32
|
+
secrets: refs ? [...listRefs(app.secrets), ...listRefs(refs)] : app.secrets,
|
|
33
|
+
files: paths ? { ...app.files, ...paths } : app.files,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const folded = { ...config, apps, steps: steps ? stepsWith(config, steps) : config.steps };
|
|
38
|
+
|
|
39
|
+
// What was folded in leaves the environment, so folding it again adds nothing
|
|
40
|
+
// and a caller that already did it can hand the result on to one that will
|
|
41
|
+
if (config.environment) return { ...folded, environment: rest };
|
|
42
|
+
return { ...folded, environments: { ...config.environments, [name]: rest } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A step at a point the deployment already fills replaces it for this
|
|
46
|
+
// environment, where it stood. One only the environment has leads, the way a
|
|
47
|
+
// plugin's does, so a snapshot added here still runs above the shared migration
|
|
48
|
+
function stepsWith(config: Deployment, given: AnyStep[]) {
|
|
49
|
+
// Before the replacement map, which would otherwise keep the last of two and
|
|
50
|
+
// drop the other without a word
|
|
51
|
+
assertSteps(given);
|
|
52
|
+
|
|
53
|
+
const shared = config.steps ?? [];
|
|
54
|
+
const claimed = new Set(shared.map((step) => step.point));
|
|
55
|
+
const replacing = new Map(given.map((step) => [step.point, step]));
|
|
56
|
+
|
|
57
|
+
const steps = [
|
|
58
|
+
...given.filter((step) => !claimed.has(step.point)),
|
|
59
|
+
...shared.map((step) => replacing.get(step.point) ?? step),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// A plugin fills points too, and one the environment lands on is the same
|
|
63
|
+
// collision defineDeployment refuses
|
|
64
|
+
assertSteps([...pluginSteps(config.plugins), ...steps]);
|
|
65
|
+
return steps;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// An environment file does not import the deployment, so an app name there
|
|
69
|
+
// cannot be checked when it compiles. A typo would otherwise read nothing
|
|
70
|
+
function assertNamesApps(config: Deployment, environment: string, names: string[]) {
|
|
71
|
+
const apps = new Set(config.apps.map((app) => app.name));
|
|
72
|
+
const unknown = names.filter((name) => !apps.has(name));
|
|
73
|
+
if (unknown.length === 0) return;
|
|
74
|
+
|
|
75
|
+
throw new Error(
|
|
76
|
+
`${environment} gives secrets to ${unknown.join(", ")}, which this deployment ` +
|
|
77
|
+
`has no app by that name for. Its apps are ${[...apps].join(", ")}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
13
81
|
// Identity, but it pins the type so a missing health predicate fails to
|
|
14
82
|
// compile. Environments are excluded rather than merely unused: each lives in
|
|
15
83
|
// a redkite.<name>.config.ts of its own, and a second place to put one is a
|
|
@@ -31,6 +99,7 @@ export function defineDeployment<const T extends Deployment & { environments?: n
|
|
|
31
99
|
// Identity, for the environment a redkite.<name>.config.ts holds. It pins the
|
|
32
100
|
// type the same way defineDeployment does, so a missing subnet fails to compile
|
|
33
101
|
export function defineEnvironment<const T extends Environment>(environment: T): T {
|
|
102
|
+
if (environment.steps) assertSteps(environment.steps);
|
|
34
103
|
return environment;
|
|
35
104
|
}
|
|
36
105
|
|
package/src/deploy.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { build, type BuildContext, type BuildResult } from "./build.js";
|
|
1
|
+
import { build, refOf, sourceOf, type BuildContext, type BuildResult } from "./build.js";
|
|
2
2
|
import { assertCheckable, runChecks } from "./checks.js";
|
|
3
|
-
import { environmentOf } from "./config.js";
|
|
3
|
+
import { environmentOf, withEnvironment } from "./config.js";
|
|
4
4
|
import { Docker } from "./docker.js";
|
|
5
5
|
import { envFileFor } from "./environment.js";
|
|
6
6
|
import { healthcheck, type HealthDeps } from "./health.js";
|
|
@@ -26,6 +26,7 @@ import { pluginSteps } from "./plugin.js";
|
|
|
26
26
|
import { readEnv, readRef, type SecretStores } from "./secrets/refs.js";
|
|
27
27
|
import { ensureService } from "./services/ensure.js";
|
|
28
28
|
import { plannedServices } from "./services/planned.js";
|
|
29
|
+
import type { Source } from "./source.js";
|
|
29
30
|
import { topologyFor, type AppTopology, type Topology } from "./topology.js";
|
|
30
31
|
import type { AppSpec, Deployment } from "./types.js";
|
|
31
32
|
|
|
@@ -68,11 +69,12 @@ export async function verify(options: DeployOptions): Promise<Finished> {
|
|
|
68
69
|
|
|
69
70
|
async function start(run: Run, options: DeployOptions): Promise<Finished> {
|
|
70
71
|
const log = options.log ?? silent;
|
|
72
|
+
const config = withEnvironment(options.config, options.environment);
|
|
71
73
|
|
|
72
74
|
const setting: Omit<Context, "task"> = {
|
|
73
|
-
config
|
|
75
|
+
config,
|
|
74
76
|
environment: options.environment,
|
|
75
|
-
topology: topologyFor(
|
|
77
|
+
topology: topologyFor(config, options.environment),
|
|
76
78
|
host: options.host,
|
|
77
79
|
docker: new Docker(options.host),
|
|
78
80
|
secrets: options.secrets,
|
|
@@ -82,7 +84,7 @@ async function start(run: Run, options: DeployOptions): Promise<Finished> {
|
|
|
82
84
|
|
|
83
85
|
// A plugin's steps lead, so a snapshot listed as a plugin runs above the
|
|
84
86
|
// migration the deployment writes after it
|
|
85
|
-
const added = [...pluginSteps(
|
|
87
|
+
const added = [...pluginSteps(config.plugins), ...(config.steps ?? [])];
|
|
86
88
|
|
|
87
89
|
return await runPipeline(run, merge(supplied(options), added), setting, options.signal);
|
|
88
90
|
}
|
|
@@ -214,8 +216,14 @@ async function release(
|
|
|
214
216
|
// Inside, because the swap is not over until this says so. A stop lands
|
|
215
217
|
// here more often than anywhere else: it is the longest part, and by now
|
|
216
218
|
// every address has already moved
|
|
217
|
-
|
|
219
|
+
const unhealthy = await checkAll(context, health);
|
|
220
|
+
|
|
221
|
+
if (unhealthy.size > 0) {
|
|
218
222
|
context.log.fail("Health checks failed, reverting");
|
|
223
|
+
|
|
224
|
+
// Before the revert, which gives the live name back to the container it
|
|
225
|
+
// retired. Asked after it, the logs would be the previous release's
|
|
226
|
+
await dumpLogs(context, apps, unhealthy);
|
|
219
227
|
await Promise.all(apps.map((app) => revert(docker, topology, app)));
|
|
220
228
|
|
|
221
229
|
return {
|
|
@@ -306,11 +314,46 @@ async function checkAll(context: Context, health: Omit<HealthDeps, "probe">) {
|
|
|
306
314
|
log,
|
|
307
315
|
};
|
|
308
316
|
|
|
309
|
-
|
|
317
|
+
const healthy = await healthcheck(target.container, target.port, app.health, deps);
|
|
318
|
+
return healthy ? undefined : target.container;
|
|
310
319
|
}),
|
|
311
320
|
);
|
|
312
321
|
|
|
313
|
-
|
|
322
|
+
// The containers that failed, since what gets written out for each depends on
|
|
323
|
+
// whether it was one of them
|
|
324
|
+
return new Set(results.filter((container): container is string => container !== undefined));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Enough to hold a stack trace and what led up to it, not a day of access logs
|
|
328
|
+
const LOG_TAIL = 200;
|
|
329
|
+
|
|
330
|
+
// Every new container's, not only the ones that failed: a backend that never came
|
|
331
|
+
// up is often explained by what the frontend says it could not reach. Each is a
|
|
332
|
+
// step of its own, so the crash log gives it a file, and the one that failed its
|
|
333
|
+
// check is marked failed, which is what puts its tail on the screen at the end
|
|
334
|
+
async function dumpLogs(context: Context, apps: AppTopology[], unhealthy: Set<string>) {
|
|
335
|
+
await Promise.all(
|
|
336
|
+
apps.map(async (app) => {
|
|
337
|
+
const task = context.log.step(`Logs of ${app.name}`);
|
|
338
|
+
task.detail(`the last ${LOG_TAIL} lines of ${app.container}`);
|
|
339
|
+
|
|
340
|
+
const result = await context.docker.container.logs(app.container, LOG_TAIL);
|
|
341
|
+
|
|
342
|
+
// Said, and left there. The revert still has to run, and logs that could
|
|
343
|
+
// not be read are no reason to leave a failed release serving
|
|
344
|
+
if (result.code !== 0) {
|
|
345
|
+
task.fail(`could not read the logs of ${app.container}: ${result.stdout || result.stderr}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const lines = result.stdout.split("\n").filter((line) => line.length > 0);
|
|
350
|
+
for (const line of lines) task.line(line);
|
|
351
|
+
|
|
352
|
+
const said = `${lines.length} lines from ${app.container}`;
|
|
353
|
+
if (unhealthy.has(app.container)) task.fail(`${said}, which failed its health check`);
|
|
354
|
+
else task.done(said);
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
314
357
|
}
|
|
315
358
|
|
|
316
359
|
// The proxy is one of these, derived rather than listed. What each should be
|
|
@@ -346,6 +389,8 @@ async function buildAll(
|
|
|
346
389
|
|
|
347
390
|
return await Promise.all(
|
|
348
391
|
config.apps.map(async (app) => {
|
|
392
|
+
const placed = appOf(topology, app.name);
|
|
393
|
+
const source = await clone(app, placed, here ?? context.host, environment.branch, log);
|
|
349
394
|
const task = log.step(`Building ${app.name}`);
|
|
350
395
|
|
|
351
396
|
try {
|
|
@@ -373,7 +418,7 @@ async function buildAll(
|
|
|
373
418
|
output: task.line,
|
|
374
419
|
};
|
|
375
420
|
|
|
376
|
-
const result = await build(app,
|
|
421
|
+
const result = await build(app, placed, buildContext, source);
|
|
377
422
|
task.done(`${result.release.slice(0, 7)}${result.cached ? " (held)" : ""}`);
|
|
378
423
|
|
|
379
424
|
return { app, result };
|
|
@@ -385,6 +430,41 @@ async function buildAll(
|
|
|
385
430
|
);
|
|
386
431
|
}
|
|
387
432
|
|
|
433
|
+
// A step of its own, ahead of the build that reads it. A wrong branch or an
|
|
434
|
+
// unreachable repository shows here, and folded into the build it was a detail
|
|
435
|
+
// that scrolled past without ever saying what it fetched
|
|
436
|
+
async function clone(
|
|
437
|
+
app: AppSpec,
|
|
438
|
+
placed: AppTopology,
|
|
439
|
+
host: Host,
|
|
440
|
+
branch: string,
|
|
441
|
+
log: Log,
|
|
442
|
+
): Promise<Source | undefined> {
|
|
443
|
+
// A directory is read where it is, not cloned, and the build still does that
|
|
444
|
+
if (!app.repo) return undefined;
|
|
445
|
+
|
|
446
|
+
const ref = refOf(app, branch);
|
|
447
|
+
const what = `${app.repo}, ${ref.kind} ${ref.name}`;
|
|
448
|
+
const task = log.step(`Cloning ${app.name}`);
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
task.detail(what);
|
|
452
|
+
const source = await sourceOf(app, placed, {
|
|
453
|
+
host,
|
|
454
|
+
branch,
|
|
455
|
+
detail: task.detail,
|
|
456
|
+
output: task.line,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Said again at the end, where it outlasts the details that replaced it
|
|
460
|
+
task.done(`${what} at ${source.release.slice(0, 7)}`);
|
|
461
|
+
return source;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
task.fail(`${app.name} could not clone ${what}`);
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
388
468
|
async function resolveFiles(app: AppSpec, stores: SecretStores) {
|
|
389
469
|
const entries = await Promise.all(
|
|
390
470
|
Object.entries(app.files ?? {}).map(
|
package/src/docker.ts
CHANGED
|
@@ -233,6 +233,12 @@ class DockerContainer {
|
|
|
233
233
|
return RUNNING.has(await this.status(name));
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
// Both streams, since a process that dies as it starts says why on stderr, and
|
|
237
|
+
// stamped so a line can be set against when the health check gave up
|
|
238
|
+
async logs(name: string, tail: number) {
|
|
239
|
+
return await this.docker.run(`container logs --tail ${tail} --timestamps ${name} 2>&1`);
|
|
240
|
+
}
|
|
241
|
+
|
|
236
242
|
async start(name: string) {
|
|
237
243
|
if (!(await this.exists(name))) {
|
|
238
244
|
throw new Error(`Cannot start ${name}, it does not exist`);
|
package/src/steps.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { environmentOf } from "./config.js";
|
|
2
1
|
import { envFlags } from "./environment.js";
|
|
3
2
|
import type { Built, Plan, Step } from "./pipeline.js";
|
|
4
3
|
import type { Topology } from "./topology.js";
|
|
@@ -30,10 +29,6 @@ type MigrateOptions = {
|
|
|
30
29
|
// already reach whatever the app's environment file points at. A database
|
|
31
30
|
// this deployment runs as a service is on "deployment" instead
|
|
32
31
|
network?: StepNetwork;
|
|
33
|
-
// The machine this migration reaches its database through. The step runs on
|
|
34
|
-
// the deploy host, so a deployment that moved to another one is refused
|
|
35
|
-
// rather than pointed at a database it may not be able to see
|
|
36
|
-
through?: string;
|
|
37
32
|
};
|
|
38
33
|
|
|
39
34
|
// An ordinary step at an ordinary point. Hung before the swap, it runs while
|
|
@@ -43,7 +38,7 @@ export function migrate(options: MigrateOptions): Step<`swap:before:${string}`>
|
|
|
43
38
|
|
|
44
39
|
return {
|
|
45
40
|
point: `swap:before:migrate-${options.app}`,
|
|
46
|
-
check: (plan) =>
|
|
41
|
+
check: (plan) => assertApp(plan, options),
|
|
47
42
|
|
|
48
43
|
run: async (input, context) => {
|
|
49
44
|
const image = builderOf(input, options.app);
|
|
@@ -73,19 +68,9 @@ export function migrate(options: MigrateOptions): Step<`swap:before:${string}`>
|
|
|
73
68
|
|
|
74
69
|
// Checked before the run starts, so a config naming an app that never kept a
|
|
75
70
|
// builder fails without having touched the host
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const bastion = environmentOf(plan.config, plan.environment)?.host?.bastion;
|
|
81
|
-
const expected = options.through;
|
|
82
|
-
if (!expected || expected === bastion) return;
|
|
83
|
-
|
|
84
|
-
throw new Error(
|
|
85
|
-
`${options.app} migrates through ${expected}, ` +
|
|
86
|
-
`but ${plan.environment} deploys to ${bastion ?? "this machine"}. ` +
|
|
87
|
-
"The step runs on the deploy host, so they have to be the same",
|
|
88
|
-
);
|
|
71
|
+
function assertApp(plan: Plan, options: MigrateOptions) {
|
|
72
|
+
if (plan.config.apps.some((item) => item.name === options.app)) return;
|
|
73
|
+
throw new Error(`${options.app} names no app in this deployment`);
|
|
89
74
|
}
|
|
90
75
|
|
|
91
76
|
// A deployment may replace the build step, and a command hung on the pipeline
|
package/src/types.ts
CHANGED
|
@@ -26,6 +26,16 @@ export type Environment = {
|
|
|
26
26
|
// derives. For a database or a legacy service that has no DNS the apps can
|
|
27
27
|
// use, and whose address is what differs between environments
|
|
28
28
|
extraHosts?: Record<string, string>;
|
|
29
|
+
// App name to the items this environment reads, after the app's own. Which
|
|
30
|
+
// item is usually what differs between staging and production, and the rest
|
|
31
|
+
// of what differs already lives here
|
|
32
|
+
secrets?: Record<string, SecretRefs>;
|
|
33
|
+
// App name to container path to item, laid over the app's own files
|
|
34
|
+
files?: Record<string, Record<string, SecretRef>>;
|
|
35
|
+
// Steps for this environment alone. One at a point the deployment already
|
|
36
|
+
// fills replaces it here, where it stood. One only this environment has runs
|
|
37
|
+
// ahead of the deployment's, so a snapshot lands above the migration
|
|
38
|
+
steps?: AnyStep[];
|
|
29
39
|
};
|
|
30
40
|
|
|
31
41
|
// Where a step's container is attached. "host" is the deploy host's own stack,
|
|
@@ -272,4 +282,13 @@ export type Deployment = {
|
|
|
272
282
|
// refs, and whatever else brings steps of its own. Nothing a plugin carries
|
|
273
283
|
// happens until it is listed here, redkite's own vault included
|
|
274
284
|
plugins?: Plugin[];
|
|
285
|
+
// How redkite itself behaves, rather than anything it deploys
|
|
286
|
+
options?: DeploymentOptions;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export type DeploymentOptions = {
|
|
290
|
+
// A run that fails writes everything it said to
|
|
291
|
+
// /tmp/<project>/<environment>/crash-<time>/: the run's own log, and one file
|
|
292
|
+
// per step with its output in full. On unless this says false
|
|
293
|
+
crashLog?: boolean;
|
|
275
294
|
};
|