Skip to content
Closed
5 changes: 5 additions & 0 deletions .changeset/hungry-turtles-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

Add support for custom instance types
34 changes: 13 additions & 21 deletions packages/containers-shared/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { spawn } from "child_process";
import { readFileSync } from "fs";
import path from "path";
import { BuildArgs, ContainerDevOptions, Logger } from "./types";
import { BuildArgs, DockerfileConfig, Logger } from "./types";

export async function constructBuildCommand(
options: BuildArgs,
/** wrangler config path. used to resolve relative dockerfile path */
configPath: string | undefined,
logger?: Logger
) {
const platform = options.platform ?? "linux/amd64";
Expand All @@ -27,12 +24,11 @@ export async function constructBuildCommand(
if (options.setNetworkToHost) {
buildCmd.push("--network", "host");
}
const baseDir = configPath ? path.dirname(configPath) : process.cwd();
const absDockerfilePath = path.resolve(baseDir, options.pathToDockerfile);
const dockerfile = readFileSync(absDockerfilePath, "utf-8");

const dockerfile = readFileSync(options.pathToDockerfile, "utf-8");
// pipe in the dockerfile
buildCmd.push("-f", "-");
buildCmd.push(options.buildContext ?? path.dirname(absDockerfilePath));
buildCmd.push(options.buildContext);
logger?.debug(`Building image with command: ${buildCmd.join(" ")}`);
return { buildCmd, dockerfile };
}
Expand Down Expand Up @@ -92,20 +88,16 @@ export function dockerBuild(

export async function buildImage(
dockerPath: string,
options: ContainerDevOptions,
configPath: string | undefined
options: DockerfileConfig,
imageTag: string
) {
// just let the tag default to latest
const { buildCmd, dockerfile } = await constructBuildCommand(
{
tag: options.imageTag,
pathToDockerfile: options.image,
buildContext: options.imageBuildContext,
args: options.args,
platform: "linux/amd64",
},
configPath
);
const { buildCmd, dockerfile } = await constructBuildCommand({
tag: imageTag,
pathToDockerfile: options.dockerfile,
buildContext: options.image_build_context,
args: options.image_vars,
platform: "linux/amd64",
});

return dockerBuild(dockerPath, { buildCmd, dockerfile });
}
32 changes: 17 additions & 15 deletions packages/containers-shared/src/images.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { buildImage } from "./build";
import {
getCloudflareContainerRegistry,
getDevContainerImageName,
isCloudflareRegistryLink,
} from "./knobs";
import { dockerLoginManagedRegistry } from "./login";
import { ContainerDevOptions } from "./types";
import { ContainerNormalisedConfig, RegistryLinkConfig } from "./types";
import {
checkExposedPorts,
isDockerfile,
runDockerCmd,
verifyDockerInstalled,
} from "./utils";

export async function pullImage(
dockerPath: string,
options: ContainerDevOptions
options: RegistryLinkConfig,
tag: string
): Promise<{ abort: () => void; ready: Promise<void> }> {
await dockerLoginManagedRegistry(dockerPath);
const pull = runDockerCmd(dockerPath, [
"pull",
options.image,
options.registry_link,
// All containers running on our platform need to be built for amd64 architecture, but by default docker pull seems to look for an image matching the host system, so we need to specify this here
"--platform",
"linux/amd64",
]);
const ready = pull.ready.then(async ({ aborted }: { aborted: boolean }) => {
if (!aborted) {
// re-tag image with the expected dev-formatted image tag for consistency
await runDockerCmd(dockerPath, ["tag", options.image, options.imageTag]);
await runDockerCmd(dockerPath, ["tag", options.registry_link, tag]);
}
});

Expand All @@ -51,14 +52,14 @@ export async function pullImage(
*/
export async function prepareContainerImagesForDev(
dockerPath: string,
containerOptions: ContainerDevOptions[],
configPath: string | undefined,
containerOptions: ContainerNormalisedConfig[],
containerBuildId: string,
onContainerImagePreparationStart: (args: {
containerOptions: ContainerDevOptions;
containerOptions: ContainerNormalisedConfig;
abort: () => void;
}) => void,
onContainerImagePreparationEnd: (args: {
containerOptions: ContainerDevOptions;
containerOptions: ContainerNormalisedConfig;
}) => void
) {
let aborted = false;
Expand All @@ -69,8 +70,9 @@ export async function prepareContainerImagesForDev(
}
await verifyDockerInstalled(dockerPath);
for (const options of containerOptions) {
if (isDockerfile(options.image, configPath)) {
const build = await buildImage(dockerPath, options, configPath);
const tag = getDevContainerImageName(options.class_name, containerBuildId);
if ("dockerfile" in options) {
const build = await buildImage(dockerPath, options, tag);
onContainerImagePreparationStart({
containerOptions: options,
abort: () => {
Expand All @@ -83,13 +85,13 @@ export async function prepareContainerImagesForDev(
containerOptions: options,
});
} else {
if (!isCloudflareRegistryLink(options.image)) {
if (!isCloudflareRegistryLink(options.registry_link)) {
throw new Error(
`Image "${options.image}" is a registry link but does not point to the Cloudflare container registry.\n` +
`Image "${options.registry_link}" is a registry link but does not point to the Cloudflare container registry.\n` +
`To use an existing image from another repository, see https://developers.cloudflare.com/containers/image-management/#using-existing-images`
);
}
const pull = await pullImage(dockerPath, options);
const pull = await pullImage(dockerPath, options, tag);
onContainerImagePreparationStart({
containerOptions: options,
abort: () => {
Expand All @@ -103,7 +105,7 @@ export async function prepareContainerImagesForDev(
});
}
if (!aborted) {
await checkExposedPorts(dockerPath, options);
await checkExposedPorts(dockerPath, options, tag);
}
}
}
Expand Down
62 changes: 51 additions & 11 deletions packages/containers-shared/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
CreateApplicationRolloutRequest,
InstanceType,
SchedulingPolicy,
} from "./client";

export interface Logger {
debug: (message: string) => void;
log: (message: string) => void;
Expand All @@ -11,7 +17,7 @@ export type BuildArgs = {
tag: string;
pathToDockerfile: string;
/** image_build_context or args.PATH. if not provided, defaults to the dockerfile directory */
buildContext?: string;
buildContext: string;
/** any env vars that should be passed in at build time */
args?: Record<string, string>;
/** platform to build for. defaults to linux/amd64 */
Expand All @@ -20,15 +26,49 @@ export type BuildArgs = {
setNetworkToHost?: boolean;
};

/** build/pull agnostic container options */
export type ContainerDevOptions = {
/** may be dockerfile or registry link */
image: string;
/** formatted as cloudflare-dev/workername-DOclassname:build-id */
imageTag: string;
export type ContainerNormalisedConfig = RegistryLinkConfig | DockerfileConfig;
export type DockerfileConfig = SharedContainerConfig & {
/** absolute path, resolved relative to the wrangler config file */
dockerfile: string;
/** absolute path, resolved relative to the wrangler config file. defaults to the directory of the dockerfile */
image_build_context: string;
image_vars?: Record<string, string>;
};
export type RegistryLinkConfig = SharedContainerConfig & {
registry_link: string;
};

export type InstanceTypeOrLimits =
| {
/** if undefined in config, defaults to instance_type */
disk_mb?: number;
vcpu?: number;
memory_mib?: number;
}
| {
/** if undefined in config, defaults to "dev" */
instance_type: InstanceType;
};

/**
* Shared container config that is used regardless of whether the image is from a dockerfile or a registry link.
*/
export type SharedContainerConfig = {
/** if undefined in config, defaults to worker_name[-envName]-class_name. */
name: string;
/** container's DO class name */
class_name: string;
imageBuildContext?: string;
/** build time args */
args?: Record<string, string>;
};
/** if undefined in config, defaults to 0 */
max_instances: number;
/** if undefined in config, defaults to "default" */
scheduling_policy: SchedulingPolicy;
/** if undefined in config, defaults to 25 */
rollout_step_percentage: number;
/** if undefined in config, defaults to "full_auto" */
rollout_kind: "full_auto" | "full_manual" | "none";
constraints?: {
regions?: string[];
cities?: string[];
tier?: number;
};
} & InstanceTypeOrLimits;
7 changes: 4 additions & 3 deletions packages/containers-shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { execFile, spawn, StdioOptions } from "child_process";
import { existsSync, statSync } from "fs";
import path from "path";
import { dockerImageInspect } from "./inspect";
import { ContainerDevOptions } from "./types";
import { ContainerNormalisedConfig } from "./types";

/** helper for simple docker command call that don't require any io handling */
export const runDockerCmd = (
Expand Down Expand Up @@ -201,10 +201,11 @@ const getContainerIdsFromImage = async (
*/
export async function checkExposedPorts(
dockerPath: string,
options: ContainerDevOptions
options: ContainerNormalisedConfig,
imageTag: string
) {
const output = await dockerImageInspect(dockerPath, {
imageTag: options.imageTag,
imageTag: imageTag,
formatString: "{{ len .Config.ExposedPorts }}",
});
if (output === "0") {
Expand Down
20 changes: 8 additions & 12 deletions packages/containers-shared/tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mkdirSync, writeFileSync } from "fs";
import { ContainerNormalisedConfig } from "../src/types";
import { checkExposedPorts, isDockerfile } from "./../src/utils";
import { runInTempDir } from "./helpers/run-in-tmp-dir";

Expand Down Expand Up @@ -71,6 +72,10 @@ vi.mock("../src/inspect", async (importOriginal) => {
};
});

const containerConfig = {
dockerfile: "",
class_name: "MyContainer",
} as ContainerNormalisedConfig;
describe("checkExposedPorts", () => {
beforeEach(() => {
docketImageInspectResult = "1";
Expand All @@ -79,23 +84,14 @@ describe("checkExposedPorts", () => {
it("should not error when some ports are exported", async () => {
docketImageInspectResult = "1";
await expect(
checkExposedPorts("./container-context/Dockerfile", {
image: "",
imageTag: "",
class_name: "MyContainer",
})
checkExposedPorts("docker", containerConfig, "image:tag")
).resolves.toBeUndefined();
});

it("should error, with an appropriate message when no ports are exported", async () => {
docketImageInspectResult = "0";
expect(
checkExposedPorts("./container-context/Dockerfile", {
image: "",
imageTag: "",
class_name: "MyContainer",
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
expect(checkExposedPorts("docker", containerConfig, "image:tag")).rejects
.toThrowErrorMatchingInlineSnapshot(`
[Error: The container "MyContainer" does not expose any ports. In your Dockerfile, please expose any ports you intend to connect to.
For additional information please see: https://developers.cloudflare.com/containers/local-dev/#exposing-ports.
]
Expand Down
Loading
Loading