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.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +819 -0
  3. package/bin/redkite.mjs +12 -0
  4. package/dist/build.d.ts +27 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/build.js +132 -0
  7. package/dist/build.js.map +1 -0
  8. package/dist/checks.d.ts +4 -0
  9. package/dist/checks.d.ts.map +1 -0
  10. package/dist/checks.js +49 -0
  11. package/dist/checks.js.map +1 -0
  12. package/dist/cli/agent.d.ts +2 -0
  13. package/dist/cli/agent.d.ts.map +1 -0
  14. package/dist/cli/agent.js +24 -0
  15. package/dist/cli/agent.js.map +1 -0
  16. package/dist/cli/config.d.ts +5 -0
  17. package/dist/cli/config.d.ts.map +1 -0
  18. package/dist/cli/config.js +217 -0
  19. package/dist/cli/config.js.map +1 -0
  20. package/dist/cli/index.d.ts +11 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +438 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cli/log.d.ts +9 -0
  25. package/dist/cli/log.d.ts.map +1 -0
  26. package/dist/cli/log.js +128 -0
  27. package/dist/cli/log.js.map +1 -0
  28. package/dist/cli/screen.d.ts +34 -0
  29. package/dist/cli/screen.d.ts.map +1 -0
  30. package/dist/cli/screen.js +298 -0
  31. package/dist/cli/screen.js.map +1 -0
  32. package/dist/cli/viewer.d.ts +9 -0
  33. package/dist/cli/viewer.d.ts.map +1 -0
  34. package/dist/cli/viewer.js +139 -0
  35. package/dist/cli/viewer.js.map +1 -0
  36. package/dist/config.d.ts +7 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +79 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/deploy.d.ts +19 -0
  41. package/dist/deploy.d.ts.map +1 -0
  42. package/dist/deploy.js +275 -0
  43. package/dist/deploy.js.map +1 -0
  44. package/dist/docker.d.ts +96 -0
  45. package/dist/docker.d.ts.map +1 -0
  46. package/dist/docker.js +325 -0
  47. package/dist/docker.js.map +1 -0
  48. package/dist/dockerfile.d.ts +14 -0
  49. package/dist/dockerfile.d.ts.map +1 -0
  50. package/dist/dockerfile.js +160 -0
  51. package/dist/dockerfile.js.map +1 -0
  52. package/dist/health.d.ts +13 -0
  53. package/dist/health.d.ts.map +1 -0
  54. package/dist/health.js +54 -0
  55. package/dist/health.js.map +1 -0
  56. package/dist/host.d.ts +21 -0
  57. package/dist/host.d.ts.map +1 -0
  58. package/dist/host.js +27 -0
  59. package/dist/host.js.map +1 -0
  60. package/dist/index.d.ts +26 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +26 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/layout.d.ts +5 -0
  65. package/dist/layout.d.ts.map +1 -0
  66. package/dist/layout.js +45 -0
  67. package/dist/layout.js.map +1 -0
  68. package/dist/localHost.d.ts +7 -0
  69. package/dist/localHost.d.ts.map +1 -0
  70. package/dist/localHost.js +32 -0
  71. package/dist/localHost.js.map +1 -0
  72. package/dist/log.d.ts +14 -0
  73. package/dist/log.d.ts.map +1 -0
  74. package/dist/log.js +15 -0
  75. package/dist/log.js.map +1 -0
  76. package/dist/nginx.d.ts +4 -0
  77. package/dist/nginx.d.ts.map +1 -0
  78. package/dist/nginx.js +53 -0
  79. package/dist/nginx.js.map +1 -0
  80. package/dist/pipeline.d.ts +103 -0
  81. package/dist/pipeline.d.ts.map +1 -0
  82. package/dist/pipeline.js +142 -0
  83. package/dist/pipeline.js.map +1 -0
  84. package/dist/presets/index.d.ts +3 -0
  85. package/dist/presets/index.d.ts.map +1 -0
  86. package/dist/presets/index.js +3 -0
  87. package/dist/presets/index.js.map +1 -0
  88. package/dist/presets/nextApp.d.ts +15 -0
  89. package/dist/presets/nextApp.d.ts.map +1 -0
  90. package/dist/presets/nextApp.js +53 -0
  91. package/dist/presets/nextApp.js.map +1 -0
  92. package/dist/presets/nodeApp.d.ts +19 -0
  93. package/dist/presets/nodeApp.d.ts.map +1 -0
  94. package/dist/presets/nodeApp.js +29 -0
  95. package/dist/presets/nodeApp.js.map +1 -0
  96. package/dist/secrets/bitwarden.d.ts +3 -0
  97. package/dist/secrets/bitwarden.d.ts.map +1 -0
  98. package/dist/secrets/bitwarden.js +6 -0
  99. package/dist/secrets/bitwarden.js.map +1 -0
  100. package/dist/secrets/refs.d.ts +9 -0
  101. package/dist/secrets/refs.d.ts.map +1 -0
  102. package/dist/secrets/refs.js +20 -0
  103. package/dist/secrets/refs.js.map +1 -0
  104. package/dist/secrets/store.d.ts +9 -0
  105. package/dist/secrets/store.d.ts.map +1 -0
  106. package/dist/secrets/store.js +104 -0
  107. package/dist/secrets/store.js.map +1 -0
  108. package/dist/services/ensure.d.ts +18 -0
  109. package/dist/services/ensure.d.ts.map +1 -0
  110. package/dist/services/ensure.js +93 -0
  111. package/dist/services/ensure.js.map +1 -0
  112. package/dist/services/index.d.ts +19 -0
  113. package/dist/services/index.d.ts.map +1 -0
  114. package/dist/services/index.js +27 -0
  115. package/dist/services/index.js.map +1 -0
  116. package/dist/services/planned.d.ts +20 -0
  117. package/dist/services/planned.d.ts.map +1 -0
  118. package/dist/services/planned.js +77 -0
  119. package/dist/services/planned.js.map +1 -0
  120. package/dist/shell.d.ts +11 -0
  121. package/dist/shell.d.ts.map +1 -0
  122. package/dist/shell.js +65 -0
  123. package/dist/shell.js.map +1 -0
  124. package/dist/source.d.ts +16 -0
  125. package/dist/source.d.ts.map +1 -0
  126. package/dist/source.js +108 -0
  127. package/dist/source.js.map +1 -0
  128. package/dist/sshHost.d.ts +15 -0
  129. package/dist/sshHost.d.ts.map +1 -0
  130. package/dist/sshHost.js +91 -0
  131. package/dist/sshHost.js.map +1 -0
  132. package/dist/steps.d.ts +27 -0
  133. package/dist/steps.d.ts.map +1 -0
  134. package/dist/steps.js +69 -0
  135. package/dist/steps.js.map +1 -0
  136. package/dist/topology.d.ts +40 -0
  137. package/dist/topology.d.ts.map +1 -0
  138. package/dist/topology.js +113 -0
  139. package/dist/topology.js.map +1 -0
  140. package/dist/types.d.ts +100 -0
  141. package/dist/types.d.ts.map +1 -0
  142. package/dist/types.js +2 -0
  143. package/dist/types.js.map +1 -0
  144. package/package.json +64 -0
  145. package/src/build.ts +236 -0
  146. package/src/checks.ts +65 -0
  147. package/src/cli/agent.ts +28 -0
  148. package/src/cli/config.ts +275 -0
  149. package/src/cli/index.ts +559 -0
  150. package/src/cli/log.ts +168 -0
  151. package/src/cli/screen.ts +409 -0
  152. package/src/cli/viewer.ts +192 -0
  153. package/src/config.ts +90 -0
  154. package/src/deploy.ts +409 -0
  155. package/src/docker.ts +433 -0
  156. package/src/dockerfile.ts +228 -0
  157. package/src/health.ts +83 -0
  158. package/src/host.ts +54 -0
  159. package/src/index.ts +25 -0
  160. package/src/layout.ts +41 -0
  161. package/src/localHost.ts +50 -0
  162. package/src/log.ts +38 -0
  163. package/src/nginx.ts +63 -0
  164. package/src/pipeline.ts +335 -0
  165. package/src/presets/index.ts +2 -0
  166. package/src/presets/nextApp.ts +70 -0
  167. package/src/presets/nodeApp.ts +46 -0
  168. package/src/secrets/bitwarden.ts +7 -0
  169. package/src/secrets/refs.ts +32 -0
  170. package/src/secrets/store.ts +147 -0
  171. package/src/services/ensure.ts +160 -0
  172. package/src/services/index.ts +48 -0
  173. package/src/services/planned.ts +119 -0
  174. package/src/shell.ts +95 -0
  175. package/src/source.ts +171 -0
  176. package/src/sshHost.ts +145 -0
  177. package/src/steps.ts +101 -0
  178. package/src/topology.ts +199 -0
  179. package/src/types.ts +243 -0
package/dist/shell.js ADDED
@@ -0,0 +1,65 @@
1
+ import { spawn } from "node:child_process";
2
+ import { lineReader, tail } from "./host.js";
3
+ // Every child still running. A detached child outlives this process, so a stop
4
+ // that does not go through here leaves a build running
5
+ const live = new Set();
6
+ // Signals every group and answers with how many are still there. The count is
7
+ // the whole point: nothing may exit while it is above zero
8
+ export function signalEverything(name) {
9
+ for (const child of live) {
10
+ try {
11
+ if (child.pid)
12
+ process.kill(-child.pid, name);
13
+ }
14
+ catch {
15
+ // Already gone, which is the outcome being asked for
16
+ }
17
+ }
18
+ return live.size;
19
+ }
20
+ export function stillRunning() {
21
+ return live.size;
22
+ }
23
+ export function spawnCollect(command, args, options = {}) {
24
+ return new Promise((resolve, reject) => {
25
+ if (options.signal?.aborted)
26
+ return reject(new Error("Stopped"));
27
+ // Its own process group, so one signal reaches everything it started. A
28
+ // shell that ran docker dies on its own otherwise, and docker keeps going
29
+ const child = spawn(command, args, {
30
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
31
+ detached: true,
32
+ });
33
+ live.add(child);
34
+ const out = [];
35
+ const err = [];
36
+ const collect = (into) => lineReader((line) => {
37
+ into.push(line);
38
+ options.onLine?.(line);
39
+ });
40
+ const stdout = collect(out);
41
+ const stderr = collect(err);
42
+ // Only a streamed command is truncated. The host snapshot is one command
43
+ // listing every container and image, and a busy host overruns any tail
44
+ const keep = (lines) => (options.onLine ? tail(lines) : lines.join("\n"));
45
+ child.stdout?.on("data", (chunk) => stdout.push(chunk.toString()));
46
+ child.stderr?.on("data", (chunk) => stderr.push(chunk.toString()));
47
+ child.on("error", reject);
48
+ // The only thing that settles this. A signalled command answers here once
49
+ // it has actually gone, which is what lets a caller wait for exactly that
50
+ child.on("close", (code) => {
51
+ live.delete(child);
52
+ stdout.flush();
53
+ stderr.flush();
54
+ resolve({ code: code ?? 1, stdout: keep(out), stderr: keep(err) });
55
+ });
56
+ if (options.stdin !== undefined)
57
+ child.stdin?.end(options.stdin);
58
+ });
59
+ }
60
+ // Single quotes are what a shell strips, so a value containing double quotes
61
+ // survives being handed to one
62
+ export function quote(value) {
63
+ return `'${value.replaceAll("'", `'\\''`)}'`;
64
+ }
65
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { Host } from "./host.js";
2
+ export type Source = {
3
+ release: string;
4
+ tree: string;
5
+ };
6
+ export type SourceRequest = {
7
+ name: string;
8
+ repo?: string;
9
+ path?: string;
10
+ include?: string[];
11
+ branch: string;
12
+ submodules: boolean;
13
+ detail?: (message: string) => void;
14
+ };
15
+ export declare function prepareSource(host: Host, request: SourceRequest): Promise<Source>;
16
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAOtC,MAAM,MAAM,MAAM,GAAG;IAGnB,OAAO,EAAE,MAAM,CAAC;IAEhB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAG1B,IAAI,EAAE,MAAM,CAAC;IAEb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IAGd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,CAAC;AAKF,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,CAKjB"}
package/dist/source.js ADDED
@@ -0,0 +1,108 @@
1
+ // accept-new rather than no: a host key that changes is still worth refusing
2
+ const GIT_SSH = "ssh -o StrictHostKeyChecking=accept-new";
3
+ export async function prepareSource(host, request) {
4
+ if (request.path)
5
+ return await localSource(host, request.path, request);
6
+ if (request.repo)
7
+ return await clonedSource(host, request.repo, request);
8
+ throw new Error(`${request.name} names neither a repo nor a path to build from`);
9
+ }
10
+ // Built where it sits. Nothing is cloned, checked out or cleaned: what the
11
+ // build reads is the tree as the person running this left it
12
+ async function localSource(host, path, request) {
13
+ const detail = request.detail ?? (() => { });
14
+ detail(`reading ${path}`);
15
+ await assertKnowable(host, path, request.include);
16
+ const release = await treeOf(host, path, request.include);
17
+ detail(`built from ${release.slice(0, 7)}`);
18
+ return { release, tree: path };
19
+ }
20
+ // Something has to say what belongs in the build. A work tree's .gitignore
21
+ // does; anywhere else the deployment has to say it, or the release would cover
22
+ // whatever happened to be lying in the directory
23
+ async function assertKnowable(host, path, include) {
24
+ if (include)
25
+ return;
26
+ const inside = await host.sh(`git -C '${path}' rev-parse --is-inside-work-tree`);
27
+ if (inside.code === 0 && inside.stdout.trim() === "true")
28
+ return;
29
+ throw new Error(`${path} is not a git work tree, so nothing there says what belongs in the ` +
30
+ 'build. Say it: include: ["src", "package.json"] on the app names what ' +
31
+ "ships, and git init there would let .gitignore name it instead");
32
+ }
33
+ // git's own content addressing, over a scratch repository so nothing about the
34
+ // source changes and no object lands in it. The tree covers what is committed,
35
+ // what is modified and what is untracked, so an edit that is never committed is
36
+ // a new release. Without an include it honours .gitignore, which is the same
37
+ // set the build reads
38
+ async function treeOf(host, path, include) {
39
+ // Everything the work tree does not ignore, or exactly what was named. The
40
+ // -- is only for the second: it would make -A a path rather than a flag
41
+ const added = include ? `-- ${include.map((item) => `'${item}'`).join(" ")}` : "-A";
42
+ const written = await host.sh([
43
+ "scratch=$(mktemp -d)",
44
+ 'git init -q --bare "$scratch/git"',
45
+ `export GIT_DIR="$scratch/git" GIT_WORK_TREE='${path}' GIT_INDEX_FILE="$scratch/index"`,
46
+ `git add ${added}`,
47
+ "git write-tree",
48
+ 'rm -rf "$scratch"',
49
+ ].join("\n"));
50
+ const release = written.stdout.trim().split("\n").at(-1) ?? "";
51
+ if (/^[0-9a-f]{40}$/.test(release))
52
+ return release;
53
+ throw new Error(`Could not read the state of ${path}: ${written.stderr || written.stdout}`);
54
+ }
55
+ async function clonedSource(host, repo, request) {
56
+ const detail = request.detail ?? (() => { });
57
+ const mirror = `${host.cache}/mirrors/${request.name}.git`;
58
+ const path = `${host.cache}/source/${request.name}`;
59
+ detail(`fetching ${request.branch}`);
60
+ await run(host, `updating the mirror of ${repo}`, [
61
+ `if [ -d '${mirror}' ]; then`,
62
+ ` git -C '${mirror}' remote set-url origin '${repo}'`,
63
+ ` git -C '${mirror}' remote update --prune`,
64
+ "else",
65
+ ` mkdir -p '${host.cache}/mirrors'`,
66
+ ` git clone --mirror '${repo}' '${mirror}'`,
67
+ "fi",
68
+ ]);
69
+ const resolved = await run(host, `resolving ${request.branch}`, [
70
+ `git -C '${mirror}' rev-parse 'refs/heads/${request.branch}'`,
71
+ ]);
72
+ const release = resolved.stdout.trim();
73
+ if (!/^[0-9a-f]{40}$/.test(release)) {
74
+ throw new Error(`${repo} has no branch ${request.branch}`);
75
+ }
76
+ detail(`checking out ${release.slice(0, 7)}`);
77
+ await run(host, `checking out ${release.slice(0, 7)}`, [
78
+ // The checkout shares the mirror's object store, so it costs the working
79
+ // tree and nothing else. Reused between deploys, hence the reset and clean
80
+ `if [ ! -d '${path}/.git' ]; then`,
81
+ ` rm -rf '${path}'`,
82
+ ` mkdir -p '${host.cache}/source'`,
83
+ ` git clone --shared --no-checkout '${mirror}' '${path}'`,
84
+ "fi",
85
+ `git -C '${path}' fetch --prune origin`,
86
+ `git -C '${path}' checkout --detach --force '${release}'`,
87
+ // Leaves the submodules alone: they are tracked, and clean only removes
88
+ // what is not
89
+ `git -C '${path}' clean -ffdx`,
90
+ ]);
91
+ if (request.submodules) {
92
+ // --remote follows the branch named in .gitmodules rather than the commit
93
+ // the parent recorded, which is what the pipeline has always done
94
+ detail("updating submodules");
95
+ await run(host, "updating submodules", [
96
+ `git -C '${path}' submodule sync --recursive`,
97
+ `git -C '${path}' submodule update --init --remote --recursive`,
98
+ ]);
99
+ }
100
+ return { release, tree: path };
101
+ }
102
+ async function run(host, what, script) {
103
+ const result = await host.sh([`export GIT_SSH_COMMAND='${GIT_SSH}'`, "set -e", ...script].join("\n"));
104
+ if (result.code === 0)
105
+ return result;
106
+ throw new Error(`Failed ${what}: ${result.stderr || result.stdout}`);
107
+ }
108
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AA8BA,6EAA6E;AAC7E,MAAM,OAAO,GAAG,yCAAyC,CAAC;AAE1D,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAU,EACV,OAAsB;IAEtB,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,MAAM,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACxE,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,MAAM,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAEzE,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,gDAAgD,CAAC,CAAC;AACnF,CAAC;AAED,2EAA2E;AAC3E,6DAA6D;AAC7D,KAAK,UAAU,WAAW,CACxB,IAAU,EACV,IAAY,EACZ,OAAsB;IAEtB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5C,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAE1B,MAAM,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAElD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,MAAM,CAAC,cAAc,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IAE5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED,2EAA2E;AAC3E,+EAA+E;AAC/E,iDAAiD;AACjD,KAAK,UAAU,cAAc,CAAC,IAAU,EAAE,IAAY,EAAE,OAAkB;IACxE,IAAI,OAAO;QAAE,OAAO;IAEpB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,WAAW,IAAI,mCAAmC,CAAC,CAAC;IACjF,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM;QAAE,OAAO;IAEjE,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,qEAAqE;QAC1E,wEAAwE;QACxE,gEAAgE,CACnE,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,sBAAsB;AACtB,KAAK,UAAU,MAAM,CAAC,IAAU,EAAE,IAAY,EAAE,OAAkB;IAChE,2EAA2E;IAC3E,wEAAwE;IACxE,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAEpF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,EAAE,CAC3B;QACE,sBAAsB;QACtB,mCAAmC;QACnC,gDAAgD,IAAI,mCAAmC;QACvF,WAAW,KAAK,EAAE;QAClB,gBAAgB;QAChB,mBAAmB;KACpB,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IAEF,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/D,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAEnD,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,KAAK,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAC9F,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,IAAU,EACV,IAAY,EACZ,OAAsB;IAEtB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,YAAY,OAAO,CAAC,IAAI,MAAM,CAAC;IAC3D,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,WAAW,OAAO,CAAC,IAAI,EAAE,CAAC;IAEpD,MAAM,CAAC,YAAY,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,MAAM,GAAG,CAAC,IAAI,EAAE,0BAA0B,IAAI,EAAE,EAAE;QAChD,YAAY,MAAM,WAAW;QAC7B,aAAa,MAAM,4BAA4B,IAAI,GAAG;QACtD,aAAa,MAAM,yBAAyB;QAC5C,MAAM;QACN,eAAe,IAAI,CAAC,KAAK,WAAW;QACpC,yBAAyB,IAAI,MAAM,MAAM,GAAG;QAC5C,IAAI;KACL,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,aAAa,OAAO,CAAC,MAAM,EAAE,EAAE;QAC9D,WAAW,MAAM,2BAA2B,OAAO,CAAC,MAAM,GAAG;KAC9D,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,kBAAkB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,CAAC,gBAAgB,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IAC9C,MAAM,GAAG,CAAC,IAAI,EAAE,gBAAgB,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE;QACrD,yEAAyE;QACzE,2EAA2E;QAC3E,cAAc,IAAI,gBAAgB;QAClC,aAAa,IAAI,GAAG;QACpB,eAAe,IAAI,CAAC,KAAK,UAAU;QACnC,uCAAuC,MAAM,MAAM,IAAI,GAAG;QAC1D,IAAI;QACJ,WAAW,IAAI,wBAAwB;QACvC,WAAW,IAAI,gCAAgC,OAAO,GAAG;QACzD,wEAAwE;QACxE,cAAc;QACd,WAAW,IAAI,eAAe;KAC/B,CAAC,CAAC;IAEH,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,0EAA0E;QAC1E,kEAAkE;QAClE,MAAM,CAAC,qBAAqB,CAAC,CAAC;QAC9B,MAAM,GAAG,CAAC,IAAI,EAAE,qBAAqB,EAAE;YACrC,WAAW,IAAI,8BAA8B;YAC7C,WAAW,IAAI,gDAAgD;SAChE,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,GAAG,CAAC,IAAU,EAAE,IAAY,EAAE,MAAgB;IAC3D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAC1B,CAAC,2BAA2B,OAAO,GAAG,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CACxE,CAAC;IAEF,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAErC,MAAM,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AACvE,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Host, OnLine, Result } from "./host.js";
2
+ export type Ssh = (args: string[], options?: {
3
+ stdin?: string;
4
+ onLine?: OnLine;
5
+ }) => Promise<Result>;
6
+ export type Shell = (command: string) => Promise<Result>;
7
+ export type SshOptions = {
8
+ run?: Ssh;
9
+ shell?: Shell;
10
+ directory?: string;
11
+ cache?: string;
12
+ signal?: AbortSignal;
13
+ };
14
+ export declare function sshHost(bastion: string, options?: SshOptions): Promise<Host>;
15
+ //# sourceMappingURL=sshHost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sshHost.d.ts","sourceRoot":"","sources":["../src/sshHost.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAQtD,MAAM,MAAM,GAAG,GAAG,CAChB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1C,OAAO,CAAC,MAAM,CAAC,CAAC;AAIrB,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEzD,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAeF,wBAAsB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkFtF"}
@@ -0,0 +1,91 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { dirname } from "node:path";
3
+ import { quote, spawnCollect } from "./shell.js";
4
+ // One TCP connection and one authentication for the whole deploy. Without it
5
+ // every command pays a handshake, which is most of what a command costs
6
+ const MULTIPLEX = [
7
+ "-o",
8
+ "ControlMaster=auto",
9
+ "-o",
10
+ "ControlPersist=60s",
11
+ "-o",
12
+ "StrictHostKeyChecking=no",
13
+ // The host clones from GitHub on our behalf rather than us shipping a tree
14
+ "-A",
15
+ ];
16
+ export async function sshHost(bastion, options = {}) {
17
+ const control = `/tmp/redkite-${randomUUID().slice(0, 8)}.control`;
18
+ const directory = options.directory ?? `/tmp/redkite-${randomUUID().slice(0, 8)}`;
19
+ const signal = options.signal;
20
+ const run = options.run ?? ((args, extra) => spawnCollect("ssh", args, { ...extra, signal }));
21
+ // Cleanup has to survive the abort that made it necessary, so it is the one
22
+ // thing here that is not killed along with everything else
23
+ const final = options.run ?? ((args, extra) => spawnCollect("ssh", args, extra));
24
+ const shell = options.shell ??
25
+ ((command) => spawnCollect("sh", ["-c", command], { signal }));
26
+ const argv = (command) => [
27
+ ...MULTIPLEX,
28
+ "-o",
29
+ `ControlPath=${control}`,
30
+ bastion,
31
+ command,
32
+ ];
33
+ const ssh = (command, extra) => run(argv(command), extra);
34
+ // Each command records the process group job control gave it, because killing
35
+ // the ssh client here leaves what it was carrying running there
36
+ let issued = 0;
37
+ // One round trip for all of it, including the home directory the cache hangs
38
+ // off. A literal ~ would only survive as long as every caller used a shell
39
+ const opened = await ssh(`mkdir -p -m 700 '${directory}' && mkdir -p "$HOME/.cache/redkite" && printf %s "$HOME"`);
40
+ if (opened.code !== 0) {
41
+ throw new Error(`Could not reach ${bastion}: ${opened.stderr || opened.stdout}`);
42
+ }
43
+ const cache = options.cache ?? `${opened.stdout.trim()}/.cache/redkite`;
44
+ return {
45
+ directory,
46
+ cache,
47
+ sh: async (command, onLine) => {
48
+ issued += 1;
49
+ return await ssh(supervised(command, `${directory}/run.${issued}.pid`), { onLine });
50
+ },
51
+ write: async (name, contents) => {
52
+ const path = `${directory}/${name}`;
53
+ const result = await ssh(`mkdir -p '${dirname(path)}' && cat > '${path}'`, {
54
+ stdin: contents,
55
+ });
56
+ if (result.code !== 0) {
57
+ throw new Error(`Could not write ${name}: ${result.stderr}`);
58
+ }
59
+ return path;
60
+ },
61
+ // Through the connection that is already open, so an image is streamed
62
+ // rather than written to a disk at each end and copied between them
63
+ pipe: async (local, remote) => await shell(`${local} | ssh ${argv(remote).map(quote).join(" ")}`),
64
+ // Signalled where they are running, not here. Killing the client would
65
+ // leave the build going with nothing left able to reach it
66
+ stop: async (name) => {
67
+ const result = await final(argv(sweep(directory, name)));
68
+ return Number(result.stdout.trim()) || 0;
69
+ },
70
+ close: async () => {
71
+ await final(argv(`rm -rf '${directory}'`));
72
+ await final(["-o", `ControlPath=${control}`, "-O", "exit", bastion]);
73
+ },
74
+ };
75
+ }
76
+ // Job control puts a background job in a process group of its own, which is the
77
+ // only handle a second connection has on what the first one started. Nothing is
78
+ // written to stdout by this: an announcement would corrupt the host snapshot
79
+ function supervised(command, pidfile) {
80
+ return `set -m; { ${command}; } & __rk=$!; printf %s "$__rk" > '${pidfile}'; wait "$__rk"`;
81
+ }
82
+ // Signals every group this deploy started there, then counts the ones still
83
+ // answering. The count is what the caller waits on: a build that ignores the
84
+ // first signal keeps it above zero until a harder one is sent
85
+ function sweep(directory, name) {
86
+ return (`left=0; for f in '${directory}'/*.pid; do [ -f "$f" ] || continue; ` +
87
+ `p=$(cat "$f" 2>/dev/null) || continue; ` +
88
+ `kill -${name} -"$p" 2>/dev/null; ` +
89
+ `kill -0 -"$p" 2>/dev/null && left=$((left+1)); done; printf %s "$left"`);
90
+ }
91
+ //# sourceMappingURL=sshHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sshHost.js","sourceRoot":"","sources":["../src/sshHost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { Built, Step } from "./pipeline.js";
2
+ import type { Topology } from "./topology.js";
3
+ import type { AppSpec, StepNetwork } from "./types.js";
4
+ export declare function attachment(network: StepNetwork, topology: Topology): string[];
5
+ type MigrateOptions = {
6
+ app: string;
7
+ command: string;
8
+ network?: StepNetwork;
9
+ tunnel?: {
10
+ bastion: string;
11
+ from: string;
12
+ alias?: string;
13
+ port?: number;
14
+ };
15
+ };
16
+ export declare function migrate(options: MigrateOptions): Step<`swap:before:${string}`>;
17
+ export declare function builderOf(input: Built, name: string): string;
18
+ type SentryOptions = {
19
+ stripFromImage: string;
20
+ };
21
+ export declare function sentry(options: SentryOptions): {
22
+ provider: "sentry";
23
+ stripFromImage: string;
24
+ };
25
+ export declare function routeOf(app: AppSpec): string;
26
+ export {};
27
+ //# sourceMappingURL=steps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAQ,IAAI,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAKvD,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,EAAE,CAY7E;AAED,KAAK,cAAc,GAAG;IAGpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAIhB,OAAO,CAAC,EAAE,WAAW,CAAC;IAItB,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3E,CAAC;AAIF,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC,eAAe,MAAM,EAAE,CAAC,CAyB9E;AAqBD,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,UAKnD;AAED,KAAK,aAAa,GAAG;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhD,wBAAgB,MAAM,CAAC,OAAO,EAAE,aAAa;;;EAE5C;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,UAEnC"}
package/dist/steps.js ADDED
@@ -0,0 +1,69 @@
1
+ import { environmentOf } from "./config.js";
2
+ // The flags that attach one. The deployment network carries the aliases too,
3
+ // because a service answers to its alias rather than its container name, and a
4
+ // step reaching postgres:5432 is the whole reason to be on that network
5
+ export function attachment(network, topology) {
6
+ if (network === "host")
7
+ return ["--network host"];
8
+ if (network === "none")
9
+ return ["--network none"];
10
+ if (network !== "deployment")
11
+ return [`--network ${network.named}`];
12
+ return [
13
+ // No address is asked for. Docker allocates from the bottom of the subnet
14
+ // and every derived address starts at .20, so an ephemeral container never
15
+ // takes one an app is about to be created on
16
+ `--network ${topology.network}`,
17
+ ...Object.entries(topology.extraHosts).map(([name, ip]) => `--add-host ${name}:${ip}`),
18
+ ];
19
+ }
20
+ // An ordinary step at an ordinary point. Hung before the swap, it runs while
21
+ // the old containers still serve, and it throws, so nothing retires
22
+ export function migrate(options) {
23
+ const command = options.command.split(" ");
24
+ return {
25
+ point: `swap:before:migrate-${options.app}`,
26
+ check: (plan) => assertReachable(plan, options),
27
+ run: async (input, context) => {
28
+ const image = builderOf(input, options.app);
29
+ context.task.detail(`${options.app}: ${options.command}`);
30
+ await context.docker.runOrThrow([
31
+ "run --rm",
32
+ ...attachment(options.network ?? "host", context.topology),
33
+ "--workdir /app",
34
+ image,
35
+ ...command,
36
+ ].join(" "), `${options.app} failed to migrate`);
37
+ return input;
38
+ },
39
+ };
40
+ }
41
+ // Checked before the run starts, so a config naming an app that never kept a
42
+ // builder fails without having touched the host
43
+ function assertReachable(plan, options) {
44
+ const app = plan.config.apps.find((item) => item.name === options.app);
45
+ if (!app)
46
+ throw new Error(`${options.app} names no app in this deployment`);
47
+ const bastion = environmentOf(plan.config, plan.environment)?.host?.bastion;
48
+ const tunnel = options.tunnel;
49
+ if (!tunnel || tunnel.bastion === bastion)
50
+ return;
51
+ throw new Error(`${options.app} tunnels its migration through ${tunnel.bastion}, ` +
52
+ `but ${plan.environment} deploys to ${bastion ?? "this machine"}. ` +
53
+ "The step runs on the deploy host, so they have to be the same");
54
+ }
55
+ // A deployment may replace the build step, and a command hung on the pipeline
56
+ // has to run in an image something actually produced
57
+ export function builderOf(input, name) {
58
+ const app = input.apps.find((item) => item.name === name);
59
+ if (app?.builderTag)
60
+ return app.builderTag;
61
+ throw new Error(`${name} has no builder image, so nothing can be run in it`);
62
+ }
63
+ export function sentry(options) {
64
+ return { provider: "sentry", stripFromImage: options.stripFromImage };
65
+ }
66
+ export function routeOf(app) {
67
+ return app.route;
68
+ }
69
+ //# sourceMappingURL=steps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.js","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { Deployment } from "./types.js";
2
+ export type AppTopology = {
3
+ name: string;
4
+ container: string;
5
+ retired: string;
6
+ failed: string;
7
+ currentAddress: string;
8
+ retiredAddress: string;
9
+ port: number;
10
+ route: string;
11
+ volumes: {
12
+ volume: string;
13
+ mountPath: string;
14
+ }[];
15
+ caches: Record<string, string>;
16
+ };
17
+ export type ServiceTopology = {
18
+ name: string;
19
+ container: string;
20
+ address: string;
21
+ alias?: string;
22
+ volumes: {
23
+ volume: string;
24
+ mountPath: string;
25
+ }[];
26
+ };
27
+ export type Topology = {
28
+ environment: string;
29
+ branch: string;
30
+ network: string;
31
+ subnet: string;
32
+ cidr: string;
33
+ publicPort?: number;
34
+ router: ServiceTopology;
35
+ apps: AppTopology[];
36
+ services: ServiceTopology[];
37
+ extraHosts: Record<string, string>;
38
+ };
39
+ export declare function topologyFor(config: Deployment, environment: string): Topology;
40
+ //# sourceMappingURL=topology.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,UAAU,EAAe,MAAM,YAAY,CAAC;AAcnE,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IAEb,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAE5B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,CAAC;AAEF,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,QAAQ,CAgD7E"}
@@ -0,0 +1,113 @@
1
+ import { environmentOf } from "./config.js";
2
+ import { mountFor } from "./layout.js";
3
+ // Every name and address the deployment uses, derived from the app list. This
4
+ // is the file that replaces the constants block: nothing is chosen by hand, so
5
+ // adding an app cannot collide with an address somebody already picked.
6
+ const NGINX_OCTET = 20;
7
+ const APP_BLOCK_START = 21;
8
+ // Services sit in their own block, so adding one never moves an app
9
+ const SERVICE_BLOCK_START = 200;
10
+ export function topologyFor(config, environment) {
11
+ const env = environmentOf(config, environment);
12
+ if (!env) {
13
+ const known = Object.keys(config.environments ?? {});
14
+ // An environment is a file, so having none is a different mistake from
15
+ // asking for one that is not there, and reads as one
16
+ if (known.length === 0) {
17
+ throw new Error(`No environments. Each one is a redkite.<name>.config.ts beside the ` +
18
+ `deployment, so ${environment} wants a redkite.${environment}.config.ts`);
19
+ }
20
+ throw new Error(`Unknown environment ${environment}, expected one of ${known.join(", ")}`);
21
+ }
22
+ const prefix = `${config.project}-${environment}`;
23
+ const address = (octet) => `${env.subnet}.${octet}`;
24
+ const apps = config.apps.map((app, index) => appTopology(app, prefix, environment, address, index));
25
+ const services = config.services.map((service, index) => serviceTopology(service, prefix, address, index));
26
+ return {
27
+ environment,
28
+ branch: env.branch,
29
+ network: `${prefix}-network`,
30
+ subnet: env.subnet,
31
+ cidr: `${env.subnet}.0/16`,
32
+ publicPort: env.publicPort,
33
+ router: {
34
+ name: "nginx",
35
+ container: `${prefix}-nginx`,
36
+ address: address(NGINX_OCTET),
37
+ volumes: [],
38
+ },
39
+ apps,
40
+ services,
41
+ extraHosts: extraHosts(apps, services, env.extraHosts),
42
+ };
43
+ }
44
+ function appTopology(app, prefix, environment, address, index) {
45
+ const container = `${prefix}-${app.name}`;
46
+ const base = APP_BLOCK_START + index * 2;
47
+ const volumes = Object.entries(app.volumes ?? {}).map(([name, mountPath]) => ({
48
+ volume: `${container}-${name}`,
49
+ mountPath,
50
+ }));
51
+ // Deduplicated by where each one mounts: an app that is the whole repository
52
+ // resolves the root and its own node_modules to one path, and two ids for one
53
+ // target would be a second cache nothing ever writes to
54
+ const targets = new Set();
55
+ const caches = {};
56
+ for (const cache of app.build.caches) {
57
+ const target = mountFor(cache, app.dir);
58
+ if (targets.has(target))
59
+ continue;
60
+ targets.add(target);
61
+ caches[cache] = `${app.name}-${environment}-${cache}-cache`;
62
+ }
63
+ return {
64
+ name: app.name,
65
+ container,
66
+ retired: `retired-${container}`,
67
+ failed: `failed-${container}`,
68
+ // Retired takes the lower slot, so a rollback never renumbers the live one
69
+ retiredAddress: address(base),
70
+ currentAddress: address(base + 1),
71
+ port: app.port,
72
+ route: app.route,
73
+ volumes,
74
+ caches,
75
+ };
76
+ }
77
+ function serviceTopology(service, prefix, address, index) {
78
+ const container = `${prefix}-${service.name}`;
79
+ return {
80
+ name: service.name,
81
+ container,
82
+ // Pinned when a service already exists at a known address
83
+ address: address(service.address ?? SERVICE_BLOCK_START + index),
84
+ alias: service.alias,
85
+ volumes: Object.entries(service.volumes ?? {}).map(([name, mountPath]) => ({
86
+ volume: `${container}-${name}`,
87
+ mountPath,
88
+ })),
89
+ };
90
+ }
91
+ // Nginx resolves upstreams by container name, apps reach services by alias
92
+ function extraHosts(apps, services, declared = {}) {
93
+ const hosts = {};
94
+ for (const app of apps) {
95
+ hosts[app.container] = app.currentAddress;
96
+ hosts[app.retired] = app.retiredAddress;
97
+ }
98
+ for (const service of services) {
99
+ if (service.alias)
100
+ hosts[service.alias] = service.address;
101
+ }
102
+ // Declared last, but never over a derived one: a name that already resolves
103
+ // to a container in this deployment would send its traffic somewhere else,
104
+ // and the deploy would look like it worked
105
+ for (const [name, address] of Object.entries(declared)) {
106
+ if (name in hosts) {
107
+ throw new Error(`extraHosts names ${name}, which this deployment already resolves to ${hosts[name]}`);
108
+ }
109
+ hosts[name] = address;
110
+ }
111
+ return hosts;
112
+ }
113
+ //# sourceMappingURL=topology.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topology.js","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"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"}