redkite 0.1.6
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 +21 -0
- package/README.md +819 -0
- package/bin/redkite.mjs +12 -0
- package/dist/build.d.ts +27 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +132 -0
- package/dist/build.js.map +1 -0
- package/dist/checks.d.ts +4 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +49 -0
- package/dist/checks.js.map +1 -0
- package/dist/cli/agent.d.ts +2 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +24 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +217 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +438 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/log.d.ts +9 -0
- package/dist/cli/log.d.ts.map +1 -0
- package/dist/cli/log.js +128 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/screen.d.ts +34 -0
- package/dist/cli/screen.d.ts.map +1 -0
- package/dist/cli/screen.js +298 -0
- package/dist/cli/screen.js.map +1 -0
- package/dist/cli/viewer.d.ts +9 -0
- package/dist/cli/viewer.d.ts.map +1 -0
- package/dist/cli/viewer.js +139 -0
- package/dist/cli/viewer.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -0
- package/dist/deploy.d.ts +19 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +275 -0
- package/dist/deploy.js.map +1 -0
- package/dist/docker.d.ts +96 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +325 -0
- package/dist/docker.js.map +1 -0
- package/dist/dockerfile.d.ts +14 -0
- package/dist/dockerfile.d.ts.map +1 -0
- package/dist/dockerfile.js +160 -0
- package/dist/dockerfile.js.map +1 -0
- package/dist/health.d.ts +13 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +54 -0
- package/dist/health.js.map +1 -0
- package/dist/host.d.ts +21 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +27 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +5 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +45 -0
- package/dist/layout.js.map +1 -0
- package/dist/localHost.d.ts +7 -0
- package/dist/localHost.d.ts.map +1 -0
- package/dist/localHost.js +32 -0
- package/dist/localHost.js.map +1 -0
- package/dist/log.d.ts +14 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +15 -0
- package/dist/log.js.map +1 -0
- package/dist/nginx.d.ts +4 -0
- package/dist/nginx.d.ts.map +1 -0
- package/dist/nginx.js +53 -0
- package/dist/nginx.js.map +1 -0
- package/dist/pipeline.d.ts +103 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +142 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/presets/index.d.ts +3 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +3 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/presets/nextApp.d.ts +15 -0
- package/dist/presets/nextApp.d.ts.map +1 -0
- package/dist/presets/nextApp.js +53 -0
- package/dist/presets/nextApp.js.map +1 -0
- package/dist/presets/nodeApp.d.ts +19 -0
- package/dist/presets/nodeApp.d.ts.map +1 -0
- package/dist/presets/nodeApp.js +29 -0
- package/dist/presets/nodeApp.js.map +1 -0
- package/dist/secrets/bitwarden.d.ts +3 -0
- package/dist/secrets/bitwarden.d.ts.map +1 -0
- package/dist/secrets/bitwarden.js +6 -0
- package/dist/secrets/bitwarden.js.map +1 -0
- package/dist/secrets/refs.d.ts +9 -0
- package/dist/secrets/refs.d.ts.map +1 -0
- package/dist/secrets/refs.js +20 -0
- package/dist/secrets/refs.js.map +1 -0
- package/dist/secrets/store.d.ts +9 -0
- package/dist/secrets/store.d.ts.map +1 -0
- package/dist/secrets/store.js +104 -0
- package/dist/secrets/store.js.map +1 -0
- package/dist/services/ensure.d.ts +18 -0
- package/dist/services/ensure.d.ts.map +1 -0
- package/dist/services/ensure.js +93 -0
- package/dist/services/ensure.js.map +1 -0
- package/dist/services/index.d.ts +19 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +27 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/planned.d.ts +20 -0
- package/dist/services/planned.d.ts.map +1 -0
- package/dist/services/planned.js +77 -0
- package/dist/services/planned.js.map +1 -0
- package/dist/shell.d.ts +11 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +65 -0
- package/dist/shell.js.map +1 -0
- package/dist/source.d.ts +16 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +108 -0
- package/dist/source.js.map +1 -0
- package/dist/sshHost.d.ts +15 -0
- package/dist/sshHost.d.ts.map +1 -0
- package/dist/sshHost.js +91 -0
- package/dist/sshHost.js.map +1 -0
- package/dist/steps.d.ts +27 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +69 -0
- package/dist/steps.js.map +1 -0
- package/dist/topology.d.ts +40 -0
- package/dist/topology.d.ts.map +1 -0
- package/dist/topology.js +113 -0
- package/dist/topology.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
- package/src/build.ts +236 -0
- package/src/checks.ts +65 -0
- package/src/cli/agent.ts +28 -0
- package/src/cli/config.ts +275 -0
- package/src/cli/index.ts +559 -0
- package/src/cli/log.ts +168 -0
- package/src/cli/screen.ts +409 -0
- package/src/cli/viewer.ts +192 -0
- package/src/config.ts +90 -0
- package/src/deploy.ts +409 -0
- package/src/docker.ts +433 -0
- package/src/dockerfile.ts +228 -0
- package/src/health.ts +83 -0
- package/src/host.ts +54 -0
- package/src/index.ts +25 -0
- package/src/layout.ts +41 -0
- package/src/localHost.ts +50 -0
- package/src/log.ts +38 -0
- package/src/nginx.ts +63 -0
- package/src/pipeline.ts +335 -0
- package/src/presets/index.ts +2 -0
- package/src/presets/nextApp.ts +70 -0
- package/src/presets/nodeApp.ts +46 -0
- package/src/secrets/bitwarden.ts +7 -0
- package/src/secrets/refs.ts +32 -0
- package/src/secrets/store.ts +147 -0
- package/src/services/ensure.ts +160 -0
- package/src/services/index.ts +48 -0
- package/src/services/planned.ts +119 -0
- package/src/shell.ts +95 -0
- package/src/source.ts +171 -0
- package/src/sshHost.ts +145 -0
- package/src/steps.ts +101 -0
- package/src/topology.ts +199 -0
- package/src/types.ts +243 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { AnyStep } from "./pipeline.js";
|
|
2
|
+
export type Environment = {
|
|
3
|
+
buildOn?: "host" | "local";
|
|
4
|
+
branch: string;
|
|
5
|
+
subnet: string;
|
|
6
|
+
publicPort?: number;
|
|
7
|
+
host?: DeployHost;
|
|
8
|
+
extraHosts?: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
export type StepNetwork = "host" | "deployment" | "none" | {
|
|
11
|
+
named: string;
|
|
12
|
+
};
|
|
13
|
+
export type DeployHost = {
|
|
14
|
+
bastion: string;
|
|
15
|
+
socket?: string;
|
|
16
|
+
};
|
|
17
|
+
export type SecretRef = {
|
|
18
|
+
provider: string;
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
export type SecretRefs = SecretRef | SecretRef[];
|
|
22
|
+
export type ServiceSpec = {
|
|
23
|
+
name: string;
|
|
24
|
+
image: string;
|
|
25
|
+
alias?: string;
|
|
26
|
+
restart?: "always" | "unless-stopped";
|
|
27
|
+
files?: Record<string, string>;
|
|
28
|
+
environment?: Record<string, string>;
|
|
29
|
+
secrets?: SecretRefs;
|
|
30
|
+
address?: number;
|
|
31
|
+
volumes?: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
export type CarryPath = string | {
|
|
34
|
+
path: string;
|
|
35
|
+
optional: true;
|
|
36
|
+
};
|
|
37
|
+
export type BuildSpec = {
|
|
38
|
+
preset: string;
|
|
39
|
+
builderImage: string;
|
|
40
|
+
runtimeImage: string;
|
|
41
|
+
dependencies?: {
|
|
42
|
+
files: string[];
|
|
43
|
+
step: string;
|
|
44
|
+
stripScripts?: string[];
|
|
45
|
+
};
|
|
46
|
+
steps: string[];
|
|
47
|
+
output: string;
|
|
48
|
+
carry: CarryPath[];
|
|
49
|
+
keepsLayout?: boolean;
|
|
50
|
+
entrypoint: string[];
|
|
51
|
+
caches: string[];
|
|
52
|
+
submodules: boolean;
|
|
53
|
+
aptPackages: string[];
|
|
54
|
+
runtimePackages: string[];
|
|
55
|
+
runtimeSteps: string[];
|
|
56
|
+
sourcemaps?: SourcemapSpec;
|
|
57
|
+
};
|
|
58
|
+
export type SourcemapSpec = {
|
|
59
|
+
provider: "sentry";
|
|
60
|
+
stripFromImage: string;
|
|
61
|
+
};
|
|
62
|
+
export type VerifySpec = {
|
|
63
|
+
steps: string[];
|
|
64
|
+
network?: StepNetwork;
|
|
65
|
+
environment?: Record<string, string>;
|
|
66
|
+
};
|
|
67
|
+
export type HealthSpec = {
|
|
68
|
+
path: string;
|
|
69
|
+
expect: (body: Record<string, unknown>) => boolean;
|
|
70
|
+
retries?: number;
|
|
71
|
+
intervalMs?: number;
|
|
72
|
+
delayMs?: number;
|
|
73
|
+
};
|
|
74
|
+
export type AppSpec = {
|
|
75
|
+
name: string;
|
|
76
|
+
repo?: string;
|
|
77
|
+
path?: string;
|
|
78
|
+
include?: string[];
|
|
79
|
+
route: string;
|
|
80
|
+
port: number;
|
|
81
|
+
secrets?: SecretRefs;
|
|
82
|
+
dir?: string;
|
|
83
|
+
build: BuildSpec;
|
|
84
|
+
health: HealthSpec;
|
|
85
|
+
verify?: VerifySpec;
|
|
86
|
+
volumes?: Record<string, string>;
|
|
87
|
+
environment?: Record<string, string>;
|
|
88
|
+
files?: Record<string, SecretRef>;
|
|
89
|
+
};
|
|
90
|
+
export type Deployment = {
|
|
91
|
+
project: string;
|
|
92
|
+
environment?: Environment;
|
|
93
|
+
environments?: Record<string, Environment>;
|
|
94
|
+
maxBodySize?: string;
|
|
95
|
+
proxyImage?: string;
|
|
96
|
+
services: ServiceSpec[];
|
|
97
|
+
apps: AppSpec[];
|
|
98
|
+
steps?: AnyStep[];
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redkite",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Blue-green Docker deployment from one config file, over ssh. No engine, no agent, no dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deploy",
|
|
7
|
+
"deployment",
|
|
8
|
+
"blue-green",
|
|
9
|
+
"zero-downtime",
|
|
10
|
+
"docker",
|
|
11
|
+
"buildkit",
|
|
12
|
+
"nginx",
|
|
13
|
+
"ssh",
|
|
14
|
+
"ci",
|
|
15
|
+
"cd"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Joao Kdouk <contato@kdouk.com>",
|
|
19
|
+
"homepage": "https://github.com/JVKdouk/RedKite#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"url": "git+https://github.com/JVKdouk/RedKite",
|
|
22
|
+
"type": "git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/JVKdouk/RedKite/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"main": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"bin": {
|
|
38
|
+
"redkite": "./bin/redkite.mjs"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"bin",
|
|
43
|
+
"src",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "rm -rf dist && tsc --project tsconfig.build.json",
|
|
49
|
+
"check": "tsc --noEmit",
|
|
50
|
+
"test": "tsx --test test/*.test.ts",
|
|
51
|
+
"bench": "tsx bench/index.ts",
|
|
52
|
+
"plan": "node bin/redkite.mjs plan --config examples/acme/redkite.config.ts",
|
|
53
|
+
"prepare": "npm run build",
|
|
54
|
+
"prepublishOnly": "npm run check && npm test && node scripts/verifyRelease.mjs"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=22.18.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^22.10.2",
|
|
61
|
+
"tsx": "^4.19.2",
|
|
62
|
+
"typescript": "5.9.3"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { Docker } from "./docker.js";
|
|
5
|
+
import { BUILDER_STAGE, renderDockerfile, renderDockerignore } from "./dockerfile.js";
|
|
6
|
+
import type { Host } from "./host.js";
|
|
7
|
+
import { prepareSource } from "./source.js";
|
|
8
|
+
import type { AppTopology } from "./topology.js";
|
|
9
|
+
import type { AppSpec } from "./types.js";
|
|
10
|
+
|
|
11
|
+
// One build. The host clones the repository, renders the pipeline as a
|
|
12
|
+
// Dockerfile, and hands it to the BuildKit inside the daemon that will run the
|
|
13
|
+
// container, so the image is finished where it is needed and never moves.
|
|
14
|
+
|
|
15
|
+
export type BuildContext = {
|
|
16
|
+
host: Host;
|
|
17
|
+
docker: Docker;
|
|
18
|
+
// Set when the image is built somewhere other than where it will run. The
|
|
19
|
+
// build goes to the daemon above, and what it produced is streamed to this one
|
|
20
|
+
deliver?: { host: Host; docker: Docker };
|
|
21
|
+
// Contents of the .env the image ships with
|
|
22
|
+
env: string;
|
|
23
|
+
// Container path to the contents that land there
|
|
24
|
+
files: Record<string, string>;
|
|
25
|
+
branch: string;
|
|
26
|
+
environment: string;
|
|
27
|
+
detail?: (message: string) => void;
|
|
28
|
+
// Prints what the build itself wrote, line by line as it runs
|
|
29
|
+
output?: (line: string) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Bumped when the pipeline changes shape without the config changing. The tag
|
|
33
|
+
// below is what tells a host it already holds an image, and a host that trusts
|
|
34
|
+
// the commit alone serves the last pipeline's output forever
|
|
35
|
+
const PIPELINE = "4";
|
|
36
|
+
|
|
37
|
+
export type BuildResult = {
|
|
38
|
+
release: string;
|
|
39
|
+
// Everything that shaped the image, not just the commit it was built from
|
|
40
|
+
fingerprint: string;
|
|
41
|
+
// Versioned name the image carries, which is what the next deploy recognises
|
|
42
|
+
tag: string;
|
|
43
|
+
// The builder stage, kept as an image of its own. The runtime image holds
|
|
44
|
+
// only what the app compiled to, so this is the one place a step can run the
|
|
45
|
+
// app's own toolchain
|
|
46
|
+
builderTag: string;
|
|
47
|
+
// True when the host already held this exact image and nothing was rebuilt
|
|
48
|
+
cached: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export async function build(
|
|
52
|
+
app: AppSpec,
|
|
53
|
+
topology: AppTopology,
|
|
54
|
+
context: BuildContext,
|
|
55
|
+
): Promise<BuildResult> {
|
|
56
|
+
const { host, docker } = context;
|
|
57
|
+
const spec = app.build;
|
|
58
|
+
const detail = context.detail ?? (() => {});
|
|
59
|
+
|
|
60
|
+
const source = await prepareSource(host, {
|
|
61
|
+
name: topology.container,
|
|
62
|
+
repo: app.repo,
|
|
63
|
+
// Already absolute when the CLI loaded the config, because a path is read
|
|
64
|
+
// against the deployment file rather than wherever this was run
|
|
65
|
+
path: app.path && resolve(app.path),
|
|
66
|
+
include: app.include,
|
|
67
|
+
branch: context.branch,
|
|
68
|
+
submodules: spec.submodules,
|
|
69
|
+
detail,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const release = source.release;
|
|
73
|
+
const fingerprint = fingerprintOf(app, context, release);
|
|
74
|
+
const tag = `${topology.container}:${release}-${fingerprint}`;
|
|
75
|
+
// Always, rather than where something remembered to ask for it. It costs the
|
|
76
|
+
// export of layers the runtime build produced anyway, and a flag that has to
|
|
77
|
+
// be set before a step can run is a flag that will be missing
|
|
78
|
+
const builderTag = `${topology.container}-builder:${release}-${fingerprint}`;
|
|
79
|
+
|
|
80
|
+
// Asked of the daemon that will run the container, not the one that builds.
|
|
81
|
+
// An image this machine holds and the host does not is one still to be sent
|
|
82
|
+
const runner = context.deliver?.docker ?? docker;
|
|
83
|
+
|
|
84
|
+
// The whole build is skipped, not just a transfer. Nothing about this commit,
|
|
85
|
+
// this pipeline or these secrets differs from the image already sitting there
|
|
86
|
+
if (await held(runner, tag, builderTag)) {
|
|
87
|
+
detail(`already built at ${release.slice(0, 7)}`);
|
|
88
|
+
await runner.image.retag(tag, topology.container);
|
|
89
|
+
|
|
90
|
+
return { release, fingerprint, tag, builderTag, cached: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const secrets = await writeSecrets(app, context, fingerprint);
|
|
94
|
+
|
|
95
|
+
const dockerfile = await host.write(
|
|
96
|
+
`${app.name}.Dockerfile`,
|
|
97
|
+
renderDockerfile(spec, {
|
|
98
|
+
caches: topology.caches,
|
|
99
|
+
port: topology.port,
|
|
100
|
+
release,
|
|
101
|
+
environment: context.environment,
|
|
102
|
+
envSecret: secrets.env.id,
|
|
103
|
+
fileSecrets: Object.fromEntries(
|
|
104
|
+
secrets.files.map((file) => [file.path, file.id]),
|
|
105
|
+
),
|
|
106
|
+
dir: app.dir,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// BuildKit reads this beside the Dockerfile rather than inside the context,
|
|
111
|
+
// which is what lets the checkout stay a checkout: the repository keeps its
|
|
112
|
+
// own .dockerignore, and .git never enters the build
|
|
113
|
+
await host.write(
|
|
114
|
+
`${app.name}.Dockerfile.dockerignore`,
|
|
115
|
+
renderDockerignore(app.include),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const invocation = {
|
|
119
|
+
context: source.tree,
|
|
120
|
+
dockerfile,
|
|
121
|
+
secrets: Object.fromEntries(
|
|
122
|
+
[secrets.env, ...secrets.files].map((secret) => [secret.id, secret.src]),
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
detail(`building ${release.slice(0, 7)}`);
|
|
127
|
+
await docker.image.build(
|
|
128
|
+
{ ...invocation, tags: [tag, topology.container] },
|
|
129
|
+
watch(detail, context.output),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Every layer of this was just built, so it costs the export alone
|
|
133
|
+
detail("keeping the builder");
|
|
134
|
+
await docker.image.build(
|
|
135
|
+
{ ...invocation, tags: [builderTag], target: BUILDER_STAGE },
|
|
136
|
+
watch(detail, context.output),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await ship(context, [tag, topology.container, builderTag], detail);
|
|
140
|
+
|
|
141
|
+
return { release, fingerprint, tag, builderTag, cached: false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// One stream, rather than a tarball written here, copied, and read there. The
|
|
145
|
+
// tags travel inside the archive, so nothing has to be named again on arrival
|
|
146
|
+
async function ship(
|
|
147
|
+
context: BuildContext,
|
|
148
|
+
tags: (string | undefined)[],
|
|
149
|
+
detail: (message: string) => void,
|
|
150
|
+
) {
|
|
151
|
+
const deliver = context.deliver;
|
|
152
|
+
if (!deliver) return;
|
|
153
|
+
|
|
154
|
+
const named = tags.filter((tag) => tag !== undefined);
|
|
155
|
+
detail(`shipping ${named.length} images`);
|
|
156
|
+
|
|
157
|
+
const result = await deliver.host.pipe(
|
|
158
|
+
`docker save ${named.join(" ")}`,
|
|
159
|
+
"docker load",
|
|
160
|
+
context.output,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (result.code !== 0) {
|
|
164
|
+
throw new Error(`Could not ship the image: ${result.stderr || result.stdout}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await deliver.docker.track((state) => {
|
|
168
|
+
for (const tag of named) state.images.add(tag);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function held(docker: Docker, tag: string, builderTag: string) {
|
|
173
|
+
if (!(await docker.image.exists(tag))) return false;
|
|
174
|
+
|
|
175
|
+
// A runtime image without the builder that produced it would run the
|
|
176
|
+
// migration from whatever release happened to be tagged last
|
|
177
|
+
return await docker.image.exists(builderTag);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Named by the id BuildKit mounts it under, holding where it sits on the host
|
|
181
|
+
type Secret = { id: string; src: string };
|
|
182
|
+
|
|
183
|
+
// And, for a credential, where it has to land in the image
|
|
184
|
+
type FileSecret = Secret & { path: string };
|
|
185
|
+
|
|
186
|
+
// The fingerprint is part of every id, because a secret mount is not part of a
|
|
187
|
+
// layer's cache key. Without it a changed environment file is answered with the
|
|
188
|
+
// image that was built from the old one
|
|
189
|
+
async function writeSecrets(
|
|
190
|
+
app: AppSpec,
|
|
191
|
+
context: BuildContext,
|
|
192
|
+
fingerprint: string,
|
|
193
|
+
): Promise<{ env: Secret; files: FileSecret[] }> {
|
|
194
|
+
const env = {
|
|
195
|
+
id: `${app.name}-env-${fingerprint}`,
|
|
196
|
+
src: await context.host.write(`${app.name}.env`, context.env),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const files = await Promise.all(
|
|
200
|
+
Object.entries(context.files).map(async ([path, contents], index) => ({
|
|
201
|
+
path,
|
|
202
|
+
id: `${app.name}-file-${index}-${fingerprint}`,
|
|
203
|
+
// Named by index, so a container path with slashes in it stays one file
|
|
204
|
+
src: await context.host.write(`${app.name}.file.${index}`, contents),
|
|
205
|
+
})),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return { env, files };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// BuildKit names the step it is on in its own progress output, which is a
|
|
212
|
+
// better line to show than anything this file could invent
|
|
213
|
+
const STEP = /^#\d+ \[[^\]]*\] (.+)$/;
|
|
214
|
+
|
|
215
|
+
function watch(detail: (message: string) => void, output?: (line: string) => void) {
|
|
216
|
+
return (line: string) => {
|
|
217
|
+
output?.(line);
|
|
218
|
+
|
|
219
|
+
const step = STEP.exec(line)?.[1];
|
|
220
|
+
if (step) detail(step);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The commit is one input of several. The pipeline, the environment file and
|
|
225
|
+
// any credential baked in as a file all change the image without touching it
|
|
226
|
+
function fingerprintOf(app: AppSpec, context: BuildContext, release: string) {
|
|
227
|
+
return createHash("sha256")
|
|
228
|
+
.update(PIPELINE)
|
|
229
|
+
.update(release)
|
|
230
|
+
.update(app.dir ?? "")
|
|
231
|
+
.update(JSON.stringify(app.build))
|
|
232
|
+
.update(context.env)
|
|
233
|
+
.update(JSON.stringify(Object.entries(context.files).sort()))
|
|
234
|
+
.digest("hex")
|
|
235
|
+
.slice(0, 12);
|
|
236
|
+
}
|
package/src/checks.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { appRoot } from "./layout.js";
|
|
2
|
+
import type { Built, Context, Plan, Released } from "./pipeline.js";
|
|
3
|
+
import { quote } from "./shell.js";
|
|
4
|
+
import { attachment, builderOf } from "./steps.js";
|
|
5
|
+
import type { AppSpec, VerifySpec } from "./types.js";
|
|
6
|
+
|
|
7
|
+
// What redkite's verify step runs. An app declares the commands that decide
|
|
8
|
+
// whether its build works, and this is the whole of putting them somewhere they
|
|
9
|
+
// can reach the services the deployment brought up.
|
|
10
|
+
//
|
|
11
|
+
// The image is the builder rather than the runtime one. The runtime image holds
|
|
12
|
+
// the compiled output and nothing that compiled it, so the test runner is not
|
|
13
|
+
// in there, and installing one at check time would test a different tree.
|
|
14
|
+
|
|
15
|
+
export async function runChecks(input: Built, context: Context): Promise<Released> {
|
|
16
|
+
const checked: string[] = [];
|
|
17
|
+
|
|
18
|
+
for (const app of context.config.apps) {
|
|
19
|
+
if (!app.verify) continue;
|
|
20
|
+
|
|
21
|
+
await checkApp(app, app.verify, input, context);
|
|
22
|
+
checked.push(app.name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Anything that got here passed: a failing command throws. The lists a swap
|
|
26
|
+
// fills stay empty, because a verify run moves no addresses
|
|
27
|
+
return { ...input, ok: true, released: [], reverted: [], checked };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function checkApp(app: AppSpec, spec: VerifySpec, input: Built, context: Context) {
|
|
31
|
+
const flags = [
|
|
32
|
+
"run --rm",
|
|
33
|
+
...attachment(spec.network ?? "deployment", context.topology),
|
|
34
|
+
`--workdir ${appRoot(app.dir)}`,
|
|
35
|
+
...settings({ ...app.environment, ...spec.environment }),
|
|
36
|
+
builderOf(input, app.name),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// In order and one at a time. The first is usually what brings the database
|
|
40
|
+
// to the schema the rest expect, so running them together would race
|
|
41
|
+
for (const command of spec.steps) {
|
|
42
|
+
context.task.detail(`${app.name}: ${command}`);
|
|
43
|
+
|
|
44
|
+
await context.docker.runOrThrow(
|
|
45
|
+
[...flags, "sh -c", quote(command)].join(" "),
|
|
46
|
+
`${app.name} failed ${command}`,
|
|
47
|
+
context.task.line,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function settings(environment: Record<string, string>) {
|
|
53
|
+
return Object.entries(environment).map(([name, value]) => `-e ${name}=${quote(value)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Checked before the run starts, so asking for a verify that would check
|
|
57
|
+
// nothing fails without having created a network or built an image
|
|
58
|
+
export function assertCheckable(plan: Plan) {
|
|
59
|
+
if (plan.config.apps.some((app) => app.verify)) return;
|
|
60
|
+
|
|
61
|
+
throw new Error(
|
|
62
|
+
"No app declares verify, so this run would check nothing. " +
|
|
63
|
+
"An app is verified by giving it verify: { steps: [...] }",
|
|
64
|
+
);
|
|
65
|
+
}
|
package/src/cli/agent.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Cloning private repositories and opening the tunnel both go through the
|
|
4
|
+
// agent, so a deploy without one fails halfway rather than at the start
|
|
5
|
+
|
|
6
|
+
// A plain writer rather than a Log, because this runs before the view opens:
|
|
7
|
+
// ssh-add may ask for a passphrase, and it cannot ask through a screen
|
|
8
|
+
// something else is drawing
|
|
9
|
+
export function requireAgent(warn: (message: string) => void) {
|
|
10
|
+
const existing = process.env["SSH_AUTH_SOCK"];
|
|
11
|
+
if (existing) return existing;
|
|
12
|
+
|
|
13
|
+
warn("No SSH agent, starting one");
|
|
14
|
+
|
|
15
|
+
const output = execFileSync("ssh-agent", ["-s"], { encoding: "utf8" });
|
|
16
|
+
const socket = output.match(/SSH_AUTH_SOCK=([^;]+);/)?.[1];
|
|
17
|
+
const pid = output.match(/SSH_AGENT_PID=([^;]+);/)?.[1];
|
|
18
|
+
|
|
19
|
+
if (!socket) throw new Error("ssh-agent did not report a socket");
|
|
20
|
+
|
|
21
|
+
process.env["SSH_AUTH_SOCK"] = socket;
|
|
22
|
+
if (pid) process.env["SSH_AGENT_PID"] = pid;
|
|
23
|
+
|
|
24
|
+
// Inherits stdio so a passphrase prompt reaches the person running this
|
|
25
|
+
execFileSync("ssh-add", [], { stdio: "inherit" });
|
|
26
|
+
|
|
27
|
+
return socket;
|
|
28
|
+
}
|