@mycadet/cli 0.12.10 → 0.13.0
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 +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/package.json +3 -2
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +64 -2
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +233 -36
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +8 -7
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +17 -7
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +13 -3
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +117 -47
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/hatch.ts +10 -52
- package/src/commands/pair.ts +9 -3
- package/src/commands/ps.ts +4 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/tunnel.ts +43 -4
- package/src/commands/upgrade.ts +84 -157
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +20 -23
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +5 -2
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +65 -34
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +18 -52
- package/src/lib/hatch-next-steps.ts +15 -2
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +3 -3
- package/src/lib/local.ts +10 -8
- package/src/lib/nginx-ingress.ts +254 -36
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/tunnel.ts +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +23 -1
- package/src/lib/xdg-log.ts +11 -0
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
+
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import * as tar from "tar";
|
|
8
|
+
import {
|
|
9
|
+
DockerRootStorage,
|
|
10
|
+
packRoot,
|
|
11
|
+
restoreRootBackup,
|
|
12
|
+
stageRootArchive,
|
|
13
|
+
STATE_ROOTS,
|
|
14
|
+
} from "../lib/root-backup.js";
|
|
15
|
+
import {
|
|
16
|
+
dockerResourceNames,
|
|
17
|
+
sleepContainers,
|
|
18
|
+
startContainers,
|
|
19
|
+
} from "../lib/docker.js";
|
|
20
|
+
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
21
|
+
import { mockCommands } from "./commands-mock.js";
|
|
22
|
+
|
|
23
|
+
const entry: AssistantEntry = {
|
|
24
|
+
assistantId: "docker-backup",
|
|
25
|
+
cloud: "docker",
|
|
26
|
+
runtimeUrl: "http://127.0.0.1:1",
|
|
27
|
+
};
|
|
28
|
+
const names = dockerResourceNames(entry.assistantId);
|
|
29
|
+
const containers = [
|
|
30
|
+
names.assistantContainer,
|
|
31
|
+
names.gatewayContainer,
|
|
32
|
+
names.cesContainer,
|
|
33
|
+
];
|
|
34
|
+
const rootContainers = {
|
|
35
|
+
workspace: names.assistantContainer,
|
|
36
|
+
"gateway-security": names.gatewayContainer,
|
|
37
|
+
"ces-security": names.cesContainer,
|
|
38
|
+
"ces-data": names.cesContainer,
|
|
39
|
+
};
|
|
40
|
+
let directory: string;
|
|
41
|
+
let commands: ReturnType<typeof mockCommands>;
|
|
42
|
+
const running = new Map<string, boolean>();
|
|
43
|
+
let writes = 0;
|
|
44
|
+
let failWrite = 0;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
directory = await mkdtemp(join(tmpdir(), "docker-root-mock-"));
|
|
48
|
+
commands = mockCommands();
|
|
49
|
+
writes = 0;
|
|
50
|
+
failWrite = 0;
|
|
51
|
+
for (const container of containers) running.set(container, true);
|
|
52
|
+
for (const root of STATE_ROOTS) {
|
|
53
|
+
await mkdir(join(directory, root), { mode: 0o700 });
|
|
54
|
+
await writeFile(join(directory, root, "value"), "original", {
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
await writeFile(
|
|
59
|
+
join(directory, "gateway-security", "actor-token-signing-key"),
|
|
60
|
+
Buffer.alloc(32, 1),
|
|
61
|
+
{ mode: 0o600 },
|
|
62
|
+
);
|
|
63
|
+
commands.on("docker", ["info"], {});
|
|
64
|
+
commands.on(
|
|
65
|
+
"docker",
|
|
66
|
+
(args) => args[0] === "inspect",
|
|
67
|
+
({ args }) => {
|
|
68
|
+
const container = args.at(-1)!;
|
|
69
|
+
if (args.includes("--format"))
|
|
70
|
+
return {
|
|
71
|
+
stdout:
|
|
72
|
+
args[2] === "{{.State.Running}}"
|
|
73
|
+
? String(running.get(container))
|
|
74
|
+
: running.get(container)
|
|
75
|
+
? "running"
|
|
76
|
+
: "exited",
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
stdout: JSON.stringify([
|
|
80
|
+
{
|
|
81
|
+
Image: "sha256:service-image",
|
|
82
|
+
State: {
|
|
83
|
+
Running: running.get(container),
|
|
84
|
+
Status: running.get(container) ? "running" : "exited",
|
|
85
|
+
},
|
|
86
|
+
Config: {
|
|
87
|
+
Env: ["RUNTIME_HTTP_PORT=3001", "GATEWAY_PORT=7830"],
|
|
88
|
+
User: "1001:1001",
|
|
89
|
+
},
|
|
90
|
+
Mounts: STATE_ROOTS.filter(
|
|
91
|
+
(root) => rootContainers[root] === container,
|
|
92
|
+
).map((root) => ({
|
|
93
|
+
Type: "volume",
|
|
94
|
+
Name: `remote-${root}`,
|
|
95
|
+
Source: `/remote/${root}`,
|
|
96
|
+
Destination: `/${root}`,
|
|
97
|
+
RW: true,
|
|
98
|
+
})),
|
|
99
|
+
},
|
|
100
|
+
]),
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
commands.on(
|
|
105
|
+
"docker",
|
|
106
|
+
(args) => args[0] === "kill",
|
|
107
|
+
({ args }) => {
|
|
108
|
+
expect(args[1]).toBe("--signal=SIGTERM");
|
|
109
|
+
running.set(args.at(-1)!, false);
|
|
110
|
+
return {};
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
commands.on(
|
|
114
|
+
"docker",
|
|
115
|
+
(args) => args[0] === "start",
|
|
116
|
+
({ args }) => {
|
|
117
|
+
running.set(args[1]!, true);
|
|
118
|
+
return {};
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
commands.on(
|
|
122
|
+
"docker",
|
|
123
|
+
(args) => args[0] === "cp",
|
|
124
|
+
async ({ args }) => {
|
|
125
|
+
const root = args[1]!.split(":/")[1]!.split("/")[0]!;
|
|
126
|
+
await cp(join(directory, root), args[2]!, { recursive: true });
|
|
127
|
+
return {};
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
commands.on(
|
|
131
|
+
"docker",
|
|
132
|
+
(args) => args[0] === "run" && args.includes("-i"),
|
|
133
|
+
async ({ args, input }) => {
|
|
134
|
+
writes++;
|
|
135
|
+
const mount = args[args.indexOf("--mount") + 1]!;
|
|
136
|
+
const root = mount.split("src=remote-")[1]!.split(",")[0]!;
|
|
137
|
+
expect(args).toContain("--network=none");
|
|
138
|
+
expect(args).toContain("--user=0");
|
|
139
|
+
expect(args).toContain("1001:1001");
|
|
140
|
+
expect(mount).not.toContain(directory);
|
|
141
|
+
expect(args[args.indexOf("-ec") + 1]).toContain('chown -R "$1" /restore');
|
|
142
|
+
await rm(join(directory, root), { recursive: true });
|
|
143
|
+
await mkdir(join(directory, root));
|
|
144
|
+
if (writes === failWrite)
|
|
145
|
+
return { code: 1, stderr: "injected volume replacement failure" };
|
|
146
|
+
await pipeline(
|
|
147
|
+
Readable.from(input),
|
|
148
|
+
tar.x({ cwd: join(directory, root), preserveOwner: false }),
|
|
149
|
+
);
|
|
150
|
+
return {};
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
afterEach(async () => {
|
|
155
|
+
commands.restore();
|
|
156
|
+
await rm(directory, { recursive: true, force: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
async function archive() {
|
|
160
|
+
const capture = join(directory, "capture");
|
|
161
|
+
await mkdir(capture);
|
|
162
|
+
const storage = new DockerRootStorage(entry);
|
|
163
|
+
await sleepContainers(names, { timeoutMs: 100 });
|
|
164
|
+
await storage.capture(capture);
|
|
165
|
+
const file = join(directory, "backup.tar.gz");
|
|
166
|
+
await packRoot(capture, file);
|
|
167
|
+
return { storage, file };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
test("Docker subprocess round-trip preserves every root, private files and clears stale entries", async () => {
|
|
171
|
+
const { storage, file } = await archive();
|
|
172
|
+
const stage = await stageRootArchive(file, directory);
|
|
173
|
+
for (const root of STATE_ROOTS)
|
|
174
|
+
await writeFile(join(directory, root, "stale"), "delete");
|
|
175
|
+
await storage.replace(join(stage, "state"));
|
|
176
|
+
for (const root of STATE_ROOTS) {
|
|
177
|
+
expect(await readFile(join(directory, root, "value"), "utf8")).toBe(
|
|
178
|
+
"original",
|
|
179
|
+
);
|
|
180
|
+
await expect(readFile(join(directory, root, "stale"))).rejects.toThrow();
|
|
181
|
+
}
|
|
182
|
+
expect(writes).toBe(4);
|
|
183
|
+
expect(commands.calls.filter((call) => call.args[0] === "kill")).toHaveLength(
|
|
184
|
+
3,
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("failure during Docker replacement recovers prior roots before restarting", async () => {
|
|
189
|
+
const { storage, file } = await archive();
|
|
190
|
+
for (const container of containers) running.set(container, true);
|
|
191
|
+
for (const root of STATE_ROOTS)
|
|
192
|
+
await writeFile(join(directory, root, "value"), "destination");
|
|
193
|
+
failWrite = 2;
|
|
194
|
+
const recovery = join(directory, "recovery.tar.gz");
|
|
195
|
+
await expect(
|
|
196
|
+
restoreRootBackup(
|
|
197
|
+
entry,
|
|
198
|
+
file,
|
|
199
|
+
{ recoveryPath: recovery },
|
|
200
|
+
{
|
|
201
|
+
storage,
|
|
202
|
+
sleep: () => sleepContainers(names, { timeoutMs: 100 }),
|
|
203
|
+
wake: async () => {
|
|
204
|
+
for (const container of containers) running.set(container, true);
|
|
205
|
+
},
|
|
206
|
+
save: () => {},
|
|
207
|
+
},
|
|
208
|
+
),
|
|
209
|
+
).rejects.toThrow("injected volume replacement failure");
|
|
210
|
+
expect(await storage.running()).toBe(true);
|
|
211
|
+
const previous = await stageRootArchive(recovery, directory);
|
|
212
|
+
for (const root of STATE_ROOTS) {
|
|
213
|
+
expect(await readFile(join(directory, root, "value"), "utf8")).toBe(
|
|
214
|
+
"destination",
|
|
215
|
+
);
|
|
216
|
+
expect(await readFile(join(previous, "state", root, "value"), "utf8")).toBe(
|
|
217
|
+
"destination",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("restart probes gateway and assistant on their own service ports", async () => {
|
|
223
|
+
const storage = new DockerRootStorage(entry);
|
|
224
|
+
expect(await storage.running()).toBe(true);
|
|
225
|
+
for (const container of containers) running.set(container, false);
|
|
226
|
+
commands.on(
|
|
227
|
+
"docker",
|
|
228
|
+
(args) => args[0] === "exec",
|
|
229
|
+
() => ({ stdout: "ready" }),
|
|
230
|
+
);
|
|
231
|
+
await storage.resume();
|
|
232
|
+
const probes = commands.calls.filter((call) => call.args[0] === "exec");
|
|
233
|
+
expect(
|
|
234
|
+
probes.find((call) => call.args[1] === names.gatewayContainer)?.args.at(-1),
|
|
235
|
+
).toBe("http://127.0.0.1:7830/healthz");
|
|
236
|
+
expect(
|
|
237
|
+
probes
|
|
238
|
+
.find((call) => call.args[1] === names.assistantContainer)
|
|
239
|
+
?.args.at(-1),
|
|
240
|
+
).toBe("http://127.0.0.1:3001/readyz");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("default Docker sleep aborts a graceful timeout without forcing termination", async () => {
|
|
244
|
+
commands.on(
|
|
245
|
+
"docker",
|
|
246
|
+
(args) => args[0] === "kill",
|
|
247
|
+
() => ({}),
|
|
248
|
+
);
|
|
249
|
+
await expect(sleepContainers(names, { timeoutMs: 0 })).rejects.toThrow(
|
|
250
|
+
"did not stop gracefully",
|
|
251
|
+
);
|
|
252
|
+
expect(running.get(names.assistantContainer)).toBe(true);
|
|
253
|
+
expect(commands.calls.some((call) => call.args[0] === "stop")).toBe(false);
|
|
254
|
+
expect(
|
|
255
|
+
commands.calls
|
|
256
|
+
.filter((call) => call.args[0] === "kill")
|
|
257
|
+
.every((call) => call.args.includes("--signal=SIGTERM")),
|
|
258
|
+
).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("create-only provisioning creates every target container without starting services", async () => {
|
|
262
|
+
commands.on(
|
|
263
|
+
"docker",
|
|
264
|
+
(args) => args[0] === "create",
|
|
265
|
+
({ args }) => {
|
|
266
|
+
running.set(args[args.indexOf("--name") + 1]!, false);
|
|
267
|
+
return { stdout: "created" };
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
await startContainers(
|
|
271
|
+
{
|
|
272
|
+
createOnly: true,
|
|
273
|
+
gatewayPort: 7830,
|
|
274
|
+
assistantPort: 3001,
|
|
275
|
+
imageTags: {
|
|
276
|
+
assistant: "assistant:target",
|
|
277
|
+
gateway: "gateway:target",
|
|
278
|
+
"credential-executor": "ces:target",
|
|
279
|
+
},
|
|
280
|
+
instanceName: entry.assistantId,
|
|
281
|
+
res: names,
|
|
282
|
+
},
|
|
283
|
+
() => {},
|
|
284
|
+
);
|
|
285
|
+
const creates = commands.calls.filter((call) => call.args[0] === "create");
|
|
286
|
+
expect(creates).toHaveLength(3);
|
|
287
|
+
expect(creates.every((call) => !call.args.includes("-d"))).toBe(true);
|
|
288
|
+
expect(
|
|
289
|
+
commands.calls.some(
|
|
290
|
+
(call) => call.args[0] === "start" || call.args[0] === "run",
|
|
291
|
+
),
|
|
292
|
+
).toBe(false);
|
|
293
|
+
expect([...running.values()].every((value) => !value)).toBe(true);
|
|
294
|
+
});
|