Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/sandbox/local-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,21 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox
return { running: running === "true", imageId };
}

async function networksIntact(name: string): Promise<boolean> {
const r = await dexec(["inspect", "-f", "{{json .NetworkSettings.Networks}}", name]);
if (r.code !== 0) return false;
let endpoints: Record<string, { NetworkID?: string }>;
try {
endpoints = JSON.parse(r.stdout.trim() || "{}");
} catch {
return false;
}
for (const endpoint of Object.values(endpoints ?? {})) {
if (endpoint.NetworkID && (await dexec(["network", "inspect", endpoint.NetworkID])).code !== 0) return false;
}
return true;
}

async function resolvePort(name: string): Promise<number> {
const cached = portByName.get(name);
if (cached) return cached;
Expand Down Expand Up @@ -293,7 +308,7 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox
const name = localContainerName(scope);
scopeByContainer.set(name, scope);
const state = await containerState(name);
if (state && state.imageId === imageId) {
if (state && state.imageId === imageId && (state.running || (await networksIntact(name)))) {
if (!state.running) await startContainer(name);
else await connectCore(await ensureNetwork(name));
activeByContainer.set(name, (activeByContainer.get(name) ?? 0) + 1);
Expand All @@ -318,12 +333,13 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox
const name = localScratchName(key);
scratchByKey.set(key, name);
const state = await containerState(name);
if (state) {
if (state && (state.running || (await networksIntact(name)))) {
if (!state.running) await startContainer(name);
else await connectCore(await ensureNetwork(name));
activeByContainer.set(name, (activeByContainer.get(name) ?? 0) + 1);
return { name, coldStart: false };
}
if (state) await dexec(["rm", "-f", name]);
await runContainer(name, undefined, false);
activeByContainer.set(name, (activeByContainer.get(name) ?? 0) + 1);
return { name, coldStart: true };
Expand Down
35 changes: 35 additions & 0 deletions test/local-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,38 @@ test("containerized core joins each sandbox network and reaches the daemon by co
await sb.teardown(h, { destroy: true });
assert.equal(fake.connections.has(`${localNetworkName(h.id)}|qm-test-core`), false);
});

test("a parked container whose network was pruned is recreated with its home volume", async () => {
const fake = installFakeDocker(daemonPort);
const sb = makeSandbox(fake);
const layers = rw(scopeId("personal", "U41"));
const h1 = await sb.provision(layers);
await sb.teardown(h1);
assert.equal(fake.containers.get(h1.id)!.running, false);
const volume = fake.containers.get(h1.id)!.volume!;
fake.networks.delete(localNetworkName(h1.id));

const h2 = await makeSandbox(fake).provision(layers);
assert.equal(h2.id, h1.id);
assert.equal(fake.runCount, 2, "container recreated after its network vanished");
assert.equal(fake.containers.get(h2.id)!.running, true);
assert.equal(fake.volumes.has(volume), true, "volume survived the recreate");
assert.equal(fake.containers.get(h2.id)!.volume, volume, "recreate remounted the same volume");
assert.equal(fake.networks.has(localNetworkName(h1.id)), true, "network recreated");
assert.equal(h2.coldStart, false, "existing volume means a warm home");
});

test("a parked scratch box whose network was pruned is recreated", async () => {
const fake = installFakeDocker(daemonPort);
const sb = makeSandbox(fake);
const layers = rw(scopeId("personal", "U42"));
const h1 = await sb.provision(layers, { scratch: { key: "pruned" } });
fake.containers.get(h1.id)!.running = false;
fake.networks.delete(localNetworkName(h1.id));

const h2 = await sb.provision(layers, { scratch: { key: "pruned" } });
assert.equal(h2.id, h1.id);
assert.equal(fake.runCount, 2, "scratch container recreated");
assert.equal(fake.containers.get(h2.id)!.running, true);
assert.equal(fake.networks.has(localNetworkName(h1.id)), true, "network recreated");
});
6 changes: 6 additions & 0 deletions test/support/fake-docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface FakeContainer {
running: boolean;
labels: Record<string, string>;
volume?: string;
network?: string;
args: string[];
}

Expand Down Expand Up @@ -54,6 +55,7 @@ export function installFakeDocker(daemonPort: number): FakeDocker {
const [k = "", v = ""] = args[++i]!.split("=");
c.labels[k] = v;
} else if (a === "-v") c.volume = args[++i]!.split(":")[0]!;
else if (a === "--network") c.network = args[++i]!;
else if (a === "-p" || a === "--cpus" || a === "--memory") i++;
}
return c;
Expand All @@ -75,6 +77,8 @@ export function installFakeDocker(daemonPort: number): FakeDocker {
const name = rest[rest.length - 1]!;
const c = containers.get(name);
if (!c) return fail(`Error: No such object: ${name}`);
if (rest.includes("{{json .NetworkSettings.Networks}}"))
return ok(JSON.stringify(c.network ? { [c.network]: { NetworkID: c.network } } : {}));
return ok(`${c.running} ${c.imageId}`);
}
case "network": {
Expand Down Expand Up @@ -122,6 +126,8 @@ export function installFakeDocker(daemonPort: number): FakeDocker {
case "start": {
const c = containers.get(rest[0]!);
if (!c) return fail("Error: No such container");
if (c.network && !networks.has(c.network))
return fail(`Error response from daemon: failed to set up container networking: network ${c.network} not found`);
c.running = true;
return ok(rest[0]!);
}
Expand Down