Skip to content

Commit 8319c84

Browse files
authored
Merge pull request #223 from codesandbox/add-new-architecture-template-builds
chore: add support for csb build for new architecture
2 parents 6e1173b + dc44085 commit 8319c84

File tree

6 files changed

+512
-9
lines changed

6 files changed

+512
-9
lines changed

src/PintClient/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import {
2121
listPorts,
2222
PortInfo,
23+
PortsListResponse,
2324
streamPortsList,
2425
} from "../api-clients/pint";
2526

@@ -28,16 +29,17 @@ class PintPortsClient implements IAgentClientPorts {
2829
const abortController = new AbortController();
2930

3031
streamPortsList({
32+
client: this.apiClient,
3133
signal: abortController.signal,
3234
headers: {
3335
headers: { Accept: "text/event-stream" },
3436
},
3537
}).then(async ({ stream }) => {
3638
for await (const evt of stream) {
37-
const data = parseStreamEvent<PortInfo[]>(evt);
39+
const data = parseStreamEvent<PortsListResponse>(evt);
3840

3941
fire(
40-
data.map((pintPort) => ({
42+
data.ports.map((pintPort) => ({
4143
port: pintPort.port,
4244
url: pintPort.address,
4345
}))

src/SandboxClient/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Barrier } from "../utils/barrier";
1616
import { AgentClient } from "../AgentClient";
1717
import { SandboxSession } from "../types";
1818
import { Tracer, SpanStatusCode } from "@opentelemetry/api";
19+
import { PintClient } from "../PintClient";
1920

2021
export * from "./filesystem";
2122
export * from "./ports";
@@ -41,6 +42,15 @@ export class SandboxClient {
4142
initStatusCb?: (event: system.InitStatus) => void,
4243
tracer?: Tracer
4344
) {
45+
if (session.isPint) {
46+
const pintClient = await PintClient.create(session);
47+
const progress = await pintClient.setup.getProgress();
48+
return new SandboxClient(pintClient, {
49+
hostToken: session.hostToken,
50+
tracer,
51+
}, progress);
52+
}
53+
4454
const { client: agentClient, joinResult } = await AgentClient.create({
4555
session,
4656
getSession,

src/bin/commands/build.ts

Lines changed: 222 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import {
1313
} from "@codesandbox/sdk";
1414
import { VmUpdateSpecsRequest } from "../../api-clients/client";
1515
import { getDefaultTemplateId, retryWithDelay } from "../../utils/api";
16-
import { getInferredApiKey } from "../../utils/constants";
16+
import { getInferredApiKey, getInferredRegistryUrl, isBetaAllowed, isLocalEnvironment } from "../../utils/constants";
1717
import { hashDirectory as getFilePaths } from "../utils/files";
1818
import { mkdir, writeFile } from "fs/promises";
1919
import { sleep } from "../../utils/sleep";
20+
import { buildDockerImage, prepareDockerBuild, pushDockerImage } from "../utils/docker";
21+
import { randomUUID } from "crypto";
2022

2123
export type BuildCommandArgs = {
2224
directory: string;
@@ -125,6 +127,12 @@ export const buildCommand: yargs.CommandModule<
125127
describe: "Relative path to log file, if any",
126128
type: "string",
127129
})
130+
.option("beta", {
131+
describe: "Use the beta Docker build process",
132+
type: "boolean",
133+
// TOOD: Remove after releasing to customers as beta feature
134+
hidden: true, // Do not show this flag in help
135+
})
128136
.positional("directory", {
129137
describe: "Path to the project that we'll create a snapshot from",
130138
type: "string",
@@ -174,6 +182,17 @@ export const buildCommand: yargs.CommandModule<
174182
}),
175183

176184
handler: async (argv) => {
185+
186+
// Beta build process using Docker
187+
// This uses the new architecture using bartender and gvisor
188+
if (argv.beta && isBetaAllowed()) {
189+
return betaCodeSandboxBuild(argv);
190+
} else if (argv.beta && !isBetaAllowed()) {
191+
console.error("The beta flag is not yet available for your account.");
192+
process.exit(1);
193+
}
194+
195+
// Existing build process
177196
const apiKey = getInferredApiKey();
178197
const api = new API({ apiKey, instrumentation: instrumentedFetch });
179198
const sdk = new CodeSandbox(apiKey);
@@ -234,8 +253,7 @@ export const buildCommand: yargs.CommandModule<
234253
spinner.start(
235254
updateSpinnerMessage(
236255
index,
237-
`Running setup ${steps.indexOf(step) + 1} / ${
238-
steps.length
256+
`Running setup ${steps.indexOf(step) + 1} / ${steps.length
239257
} - ${step.name}...`
240258
)
241259
);
@@ -448,9 +466,9 @@ export const buildCommand: yargs.CommandModule<
448466
argv.ci
449467
? String(error)
450468
: "Failed, please manually verify at https://codesandbox.io/s/" +
451-
id +
452-
" - " +
453-
String(error)
469+
id +
470+
" - " +
471+
String(error)
454472
)
455473
);
456474

@@ -605,3 +623,201 @@ function createAlias(directory: string, alias: string) {
605623
alias,
606624
};
607625
}
626+
627+
/**
628+
* Build a CodeSandbox Template using Docker for use in gvisor-based sandboxes.
629+
* @param argv arguments to csb build command
630+
*/
631+
export async function betaCodeSandboxBuild(argv: yargs.ArgumentsCamelCase<BuildCommandArgs>): Promise<void> {
632+
let dockerFileCleanupFn: (() => Promise<void>) | undefined;
633+
let client: SandboxClient | undefined;
634+
635+
try {
636+
const apiKey = getInferredApiKey();
637+
const api = new API({ apiKey, instrumentation: instrumentedFetch });
638+
const sdk = new CodeSandbox(apiKey);
639+
const sandboxTier = argv.vmTier
640+
? VMTier.fromName(argv.vmTier)
641+
: VMTier.Micro;
642+
643+
const resolvedDirectory = path.resolve(argv.directory);
644+
645+
const registry = getInferredRegistryUrl();
646+
const repository = "templates";
647+
const imageName = `image-${randomUUID().toLowerCase()}`;
648+
const tag = "latest";
649+
const fullImageName = `${registry}/${repository}/${imageName}:${tag}`;
650+
651+
let architecture = "amd64";
652+
// For dev environments with arm64 (Apple Silicon), use arm64 architecture
653+
if (process.arch === "arm64" && isLocalEnvironment()) {
654+
console.log("Using arm64 architecture for Docker build");
655+
architecture = "arm64";
656+
}
657+
658+
// Prepare Docker Build
659+
const dockerBuildPrepareSpinner = ora({ stream: process.stdout });
660+
dockerBuildPrepareSpinner.start("Preparing build environment...");
661+
662+
let dockerfilePath: string;
663+
664+
try {
665+
const result = await prepareDockerBuild(resolvedDirectory, (output: string) => {
666+
dockerBuildPrepareSpinner.text = `Preparing build environment: (${output})`;
667+
});
668+
dockerFileCleanupFn = result.cleanupFn;
669+
dockerfilePath = result.dockerfilePath;
670+
671+
dockerBuildPrepareSpinner.succeed("Build environment ready.");
672+
} catch (error) {
673+
dockerBuildPrepareSpinner.fail(`Failed to prepare build environment: ${(error as Error).message}`);
674+
throw error;
675+
}
676+
677+
678+
// Docker Build
679+
const dockerBuildSpinner = ora({ stream: process.stdout });
680+
dockerBuildSpinner.start("Building template docker image...");
681+
try {
682+
await buildDockerImage({
683+
dockerfilePath,
684+
imageName: fullImageName,
685+
context: resolvedDirectory,
686+
architecture,
687+
onOutput: (output: string) => {
688+
const cleanOutput = stripAnsiCodes(output);
689+
dockerBuildSpinner.text = `Building template Docker image: (${cleanOutput})`;
690+
},
691+
});
692+
} catch (error) {
693+
dockerBuildSpinner.fail(`Failed to build template Docker image: ${(error as Error).message}`);
694+
throw error;
695+
}
696+
dockerBuildSpinner.succeed("Template Docker image built successfully.");
697+
698+
// Push Docker Image
699+
const imagePushSpinner = ora({ stream: process.stdout });
700+
imagePushSpinner.start("Pushing template Docker image to CodeSandbox...");
701+
try {
702+
await pushDockerImage(
703+
fullImageName,
704+
(output: string) => {
705+
const cleanOutput = stripAnsiCodes(output);
706+
imagePushSpinner.text = `Pushing template Docker image to CodeSandbox: (${cleanOutput})`;
707+
},
708+
);
709+
} catch (error) {
710+
imagePushSpinner.fail(`Failed to push template Docker image: ${(error as Error).message}`);
711+
throw error;
712+
}
713+
imagePushSpinner.succeed("Template Docker image pushed to CodeSandbox.");
714+
715+
716+
// Create Template with Docker Image
717+
const templateData = await api.createTemplate({
718+
forkOf: argv.fromSandbox || getDefaultTemplateId(api.getClient()),
719+
title: argv.name,
720+
// We filter out sdk-templates on the dashboard
721+
tags: ["sdk-template"],
722+
// @ts-ignore
723+
image: {
724+
registry: registry,
725+
repository: "templates",
726+
name: imageName,
727+
tag: "latest",
728+
architecture: architecture
729+
},
730+
});
731+
732+
// Create a memory snapshot from the template sandboxes
733+
const templateBuildSpinner = ora({ stream: process.stdout });
734+
templateBuildSpinner.start("Preparing template snapshot...");
735+
736+
const sandboxId = templateData.sandboxes[0].id;
737+
try {
738+
templateBuildSpinner.text = "Preparing template snapshot: Starting sandbox to create snapshot...";
739+
const sandbox = await sdk.sandboxes.resume(sandboxId);
740+
741+
templateBuildSpinner.text = "Preparing template snapshot: Connecting to sandbox...";
742+
client = await sandbox.connect()
743+
744+
if (argv.ports && argv.ports.length > 0) {
745+
templateBuildSpinner.text = `Preparing template snapshot: Waiting for ports ${argv.ports.join(', ')} to be ready...`;
746+
await Promise.all(
747+
argv.ports.map(async (port) => {
748+
if (!client) throw new Error('Failed to connect to sandbox to wait for ports');
749+
const portInfo = await client.ports.waitForPort(port, {
750+
timeoutMs: 10_000,
751+
});
752+
})
753+
);
754+
} else {
755+
templateBuildSpinner.text = `Preparing template snapshot: No ports specified, waiting 10 seconds for tasks to run...`;
756+
await sleep(10000);
757+
}
758+
759+
templateBuildSpinner.text = "Preparing template snapshot: Sandbox is ready. Creating snapshot...";
760+
await sdk.sandboxes.hibernate(sandboxId);
761+
762+
templateBuildSpinner.succeed("Template snapshot created.");
763+
764+
} catch (error) {
765+
templateBuildSpinner.text = "Preparing template snapshot: Failed to create snapshot. Cleaning up...";
766+
await sdk.sandboxes.shutdown(sandboxId);
767+
templateBuildSpinner.fail(`Failed to create template reference and example: ${(error as Error).message}`);
768+
throw error;
769+
}
770+
771+
// Create alias if needed and output final instructions
772+
const templateFinaliseSpinner = ora({ stream: process.stdout });
773+
templateFinaliseSpinner.start(
774+
`\n\nCreating template reference and example...`
775+
);
776+
let referenceString;
777+
let id;
778+
779+
// Create alias if needed
780+
if (argv.alias) {
781+
const alias = createAlias(resolvedDirectory, argv.alias);
782+
await api.assignVmTagAlias(alias.namespace, alias.alias, {
783+
tag_id: templateData.tag,
784+
});
785+
786+
id = `${alias.namespace}@${alias.alias}`;
787+
referenceString = `Alias ${id} now referencing: ${templateData.tag}`;
788+
} else {
789+
id = templateData.tag;
790+
referenceString = `Template created with tag: ${templateData.tag}`;
791+
}
792+
793+
templateFinaliseSpinner.succeed(`${referenceString}\n\n
794+
Create sandbox from template using
795+
796+
SDK:
797+
798+
sdk.sandboxes.create({
799+
id: "${id}"
800+
})
801+
802+
CLI:
803+
804+
csb sandboxes fork ${id}\n`
805+
806+
);
807+
808+
process.exit(0);
809+
} catch (error) {
810+
console.error(error);
811+
process.exit(1);
812+
} finally {
813+
// Cleanup temporary Dockerfile if created
814+
if (dockerFileCleanupFn) {
815+
await dockerFileCleanupFn();
816+
}
817+
if (client) {
818+
await client.disconnect();
819+
client.dispose();
820+
client = undefined;
821+
}
822+
}
823+
}

0 commit comments

Comments
 (0)