Skip to content

Commit ad79781

Browse files
fix(sandbox): hydrate exposed ports in Sandbox.get() so domain() works after reconnect
1 parent dfbe849 commit ad79781

4 files changed

Lines changed: 58 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bunny.net/sandbox": patch
3+
---
4+
5+
fix(sandbox): Sandbox.get() recovers exposed port mappings from CDN endpoints so domain() works after reconnect

packages/sandbox/src/provision.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ type McClient = ReturnType<typeof createMcClient>;
2020
interface AppContainer {
2121
id?: string;
2222
environmentVariables?: Array<{ name?: string; value?: string }>;
23-
endpoints?: Array<{ type?: string; publicHost?: string }>;
23+
endpoints?: Array<{
24+
type?: string;
25+
publicHost?: string;
26+
portMappings?: Array<{ containerPort?: number }>;
27+
}>;
2428
}
2529

2630
interface App {
@@ -101,6 +105,20 @@ export async function setContainerEnv(
101105
}
102106
}
103107

108+
/** Map each CDN-exposed container port to its public host. */
109+
export function extractCdnPorts(app: App): Record<number, string> {
110+
const ports: Record<number, string> = {};
111+
for (const ct of app.containerTemplates ?? []) {
112+
for (const ep of ct.endpoints ?? []) {
113+
if (ep.type !== "cdn" || !ep.publicHost) continue;
114+
for (const pm of ep.portMappings ?? []) {
115+
if (pm.containerPort != null) ports[pm.containerPort] = ep.publicHost;
116+
}
117+
}
118+
}
119+
return ports;
120+
}
121+
104122
export function firstContainerId(app: App): string | null {
105123
return app.containerTemplates?.[0]?.id ?? null;
106124
}

packages/sandbox/src/sandbox.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
22
import {
33
extractAgentToken,
44
extractAnycastHost,
5+
extractCdnPorts,
56
firstContainerId,
67
splitHost,
78
} from "./provision.ts";
@@ -82,7 +83,20 @@ describe("app extraction", () => {
8283
{
8384
id: "ct-1",
8485
environmentVariables: [{ name: "AGENT_TOKEN", value: "secret" }],
85-
endpoints: [{ type: "anycast", publicHost: "1.2.3.4:8023" }],
86+
endpoints: [
87+
{ type: "anycast", publicHost: "1.2.3.4:8023" },
88+
{
89+
type: "cdn",
90+
publicHost: "app-3000.b-cdn.net",
91+
portMappings: [{ containerPort: 3000 }],
92+
},
93+
{
94+
type: "cdn",
95+
publicHost: "app-8080.b-cdn.net",
96+
portMappings: [{ containerPort: 8080 }],
97+
},
98+
{ type: "cdn", portMappings: [{ containerPort: 9999 }] },
99+
],
86100
},
87101
],
88102
};
@@ -96,9 +110,19 @@ describe("app extraction", () => {
96110
test("returns the first container id", () => {
97111
expect(firstContainerId(app)).toBe("ct-1");
98112
});
113+
test("maps CDN-exposed ports to their public hosts", () => {
114+
expect(extractCdnPorts(app)).toEqual({
115+
3000: "app-3000.b-cdn.net",
116+
8080: "app-8080.b-cdn.net",
117+
});
118+
});
119+
test("skips CDN endpoints whose public host is still provisioning", () => {
120+
expect(extractCdnPorts(app)[9999]).toBeUndefined();
121+
});
99122
test("returns null when fields are absent", () => {
100123
expect(extractAnycastHost({})).toBeNull();
101124
expect(extractAgentToken({})).toBeNull();
102125
expect(firstContainerId({})).toBeNull();
126+
expect(extractCdnPorts({})).toEqual({});
103127
});
104128
});

packages/sandbox/src/sandbox.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
deleteApp,
99
extractAgentToken,
1010
extractAnycastHost,
11+
extractCdnPorts,
1112
extractEnv,
1213
firstContainerId,
1314
getApp,
@@ -131,7 +132,7 @@ export class Sandbox {
131132
return sandbox;
132133
}
133134

134-
/** Retrieve an existing sandbox by app ID, recovering its connection details. */
135+
/** Retrieve an existing sandbox by app ID, recovering its connection details and exposed ports. */
135136
static async get(options: GetOptions): Promise<Sandbox> {
136137
const client = mcClient(options);
137138
const app = await getApp(client, options.appId);
@@ -146,7 +147,13 @@ export class Sandbox {
146147
const name = (app as { name?: string }).name ?? options.appId;
147148

148149
return new Sandbox(
149-
{ appId: options.appId, name, agentToken, sshHost },
150+
{
151+
appId: options.appId,
152+
name,
153+
agentToken,
154+
sshHost,
155+
ports: extractCdnPorts(app),
156+
},
150157
() => client,
151158
transportFor(sshHost, agentToken),
152159
);

0 commit comments

Comments
 (0)