Skip to content

Commit 1e92dd8

Browse files
authored
refactor containers config handling (#9961)
* refactor containers config handling Unfortunately I had to squash this to fix merge conflicts, so this commit contains: * add config normalisation function * refactor dev to use normalised config * refactor containers deploy to use normalised config * consolidate tests into containers/deploy.test.ts * fix up loop in build command and config handling in ensureDiskSize * stray fixups for cloudchamber apply * validate and normalise app name, constraints and observability too in config normalisation function * bring back dev specific options type * resolve image path in readConfig (main validation func) * fix custom instance limit units and add test * PR feedback * rebase fixups
1 parent 7245101 commit 1e92dd8

File tree

34 files changed

+3124
-4171
lines changed

34 files changed

+3124
-4171
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
root: true,
3+
extends: ["@cloudflare/eslint-config-worker"],
4+
};

packages/containers-shared/.eslintrc.json

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/containers-shared/src/build.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { spawn } from "child_process";
22
import { readFileSync } from "fs";
3-
import path from "path";
4-
import type { BuildArgs, ContainerDevOptions, Logger } from "./types";
3+
import type {
4+
BuildArgs,
5+
ContainerDevOptions,
6+
ImageURIConfig,
7+
Logger,
8+
} from "./types";
59

610
export async function constructBuildCommand(
711
options: BuildArgs,
8-
/** wrangler config path. used to resolve relative dockerfile path */
9-
configPath: string | undefined,
1012
logger?: Logger
1113
) {
1214
const platform = options.platform ?? "linux/amd64";
@@ -27,16 +29,11 @@ export async function constructBuildCommand(
2729
if (options.setNetworkToHost) {
2830
buildCmd.push("--network", "host");
2931
}
30-
const baseDir = configPath ? path.dirname(configPath) : process.cwd();
31-
const absDockerfilePath = path.resolve(baseDir, options.pathToDockerfile);
32-
const dockerfile = readFileSync(absDockerfilePath, "utf-8");
3332

34-
const absBuildContext = options.buildContext
35-
? path.resolve(baseDir, options.buildContext)
36-
: path.dirname(absDockerfilePath);
33+
const dockerfile = readFileSync(options.pathToDockerfile, "utf-8");
3734
// pipe in the dockerfile
3835
buildCmd.push("-f", "-");
39-
buildCmd.push(absBuildContext);
36+
buildCmd.push(options.buildContext);
4037
logger?.debug(`Building image with command: ${buildCmd.join(" ")}`);
4138
return { buildCmd, dockerfile };
4239
}
@@ -96,20 +93,15 @@ export function dockerBuild(
9693

9794
export async function buildImage(
9895
dockerPath: string,
99-
options: ContainerDevOptions,
100-
configPath: string | undefined
96+
options: Exclude<ContainerDevOptions, ImageURIConfig>
10197
) {
102-
// just let the tag default to latest
103-
const { buildCmd, dockerfile } = await constructBuildCommand(
104-
{
105-
tag: options.imageTag,
106-
pathToDockerfile: options.image,
107-
buildContext: options.imageBuildContext,
108-
args: options.args,
109-
platform: "linux/amd64",
110-
},
111-
configPath
112-
);
98+
const { buildCmd, dockerfile } = await constructBuildCommand({
99+
tag: options.image_tag,
100+
pathToDockerfile: options.dockerfile,
101+
buildContext: options.image_build_context,
102+
args: options.image_vars,
103+
platform: "linux/amd64",
104+
});
113105

114106
return dockerBuild(dockerPath, { buildCmd, dockerfile });
115107
}

packages/containers-shared/src/images.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,33 @@ import {
44
isCloudflareRegistryLink,
55
} from "./knobs";
66
import { dockerLoginManagedRegistry } from "./login";
7-
import { type ContainerDevOptions } from "./types";
87
import {
98
checkExposedPorts,
10-
isDockerfile,
119
runDockerCmd,
1210
verifyDockerInstalled,
1311
} from "./utils";
12+
import type { ContainerDevOptions, DockerfileConfig } from "./types";
1413

1514
export async function pullImage(
1615
dockerPath: string,
17-
options: ContainerDevOptions
16+
options: Exclude<ContainerDevOptions, DockerfileConfig>
1817
): Promise<{ abort: () => void; ready: Promise<void> }> {
1918
await dockerLoginManagedRegistry(dockerPath);
2019
const pull = runDockerCmd(dockerPath, [
2120
"pull",
22-
options.image,
21+
options.image_uri,
2322
// 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
2423
"--platform",
2524
"linux/amd64",
2625
]);
2726
const ready = pull.ready.then(async ({ aborted }: { aborted: boolean }) => {
2827
if (!aborted) {
2928
// re-tag image with the expected dev-formatted image tag for consistency
30-
await runDockerCmd(dockerPath, ["tag", options.image, options.imageTag]);
29+
await runDockerCmd(dockerPath, [
30+
"tag",
31+
options.image_uri,
32+
options.image_tag,
33+
]);
3134
}
3235
});
3336

@@ -49,9 +52,8 @@ export async function pullImage(
4952
* such as checking if the Docker CLI is installed, and if the container images
5053
* expose any ports.
5154
*/
52-
export async function prepareContainerImagesForDev(options: {
55+
export async function prepareContainerImagesForDev(args: {
5356
dockerPath: string;
54-
configPath?: string;
5557
containerOptions: ContainerDevOptions[];
5658
onContainerImagePreparationStart: (args: {
5759
containerOptions: ContainerDevOptions;
@@ -63,11 +65,10 @@ export async function prepareContainerImagesForDev(options: {
6365
}) {
6466
const {
6567
dockerPath,
66-
configPath,
6768
containerOptions,
6869
onContainerImagePreparationStart,
6970
onContainerImagePreparationEnd,
70-
} = options;
71+
} = args;
7172
let aborted = false;
7273
if (process.platform === "win32") {
7374
throw new Error(
@@ -76,8 +77,8 @@ export async function prepareContainerImagesForDev(options: {
7677
}
7778
await verifyDockerInstalled(dockerPath);
7879
for (const options of containerOptions) {
79-
if (isDockerfile(options.image, configPath)) {
80-
const build = await buildImage(dockerPath, options, configPath);
80+
if ("dockerfile" in options) {
81+
const build = await buildImage(dockerPath, options);
8182
onContainerImagePreparationStart({
8283
containerOptions: options,
8384
abort: () => {
@@ -90,9 +91,9 @@ export async function prepareContainerImagesForDev(options: {
9091
containerOptions: options,
9192
});
9293
} else {
93-
if (!isCloudflareRegistryLink(options.image)) {
94+
if (!isCloudflareRegistryLink(options.image_uri)) {
9495
throw new Error(
95-
`Image "${options.image}" is a registry link but does not point to the Cloudflare container registry.\n` +
96+
`Image "${options.image_uri}" is a registry link but does not point to the Cloudflare container registry.\n` +
9697
`To use an existing image from another repository, see https://developers.cloudflare.com/containers/image-management/#using-existing-images`
9798
);
9899
}
Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { InstanceType, SchedulingPolicy } from "./client";
2+
13
export interface Logger {
24
debug: (message: string) => void;
35
log: (message: string) => void;
@@ -11,7 +13,7 @@ export type BuildArgs = {
1113
tag: string;
1214
pathToDockerfile: string;
1315
/** image_build_context or args.PATH. if not provided, defaults to the dockerfile directory */
14-
buildContext?: string;
16+
buildContext: string;
1517
/** any env vars that should be passed in at build time */
1618
args?: Record<string, string>;
1719
/** platform to build for. defaults to linux/amd64 */
@@ -20,15 +22,60 @@ export type BuildArgs = {
2022
setNetworkToHost?: boolean;
2123
};
2224

25+
export type ContainerNormalizedConfig = SharedContainerConfig &
26+
(ImageURIConfig | DockerfileConfig);
27+
export type DockerfileConfig = {
28+
/** absolute path, resolved relative to the wrangler config file */
29+
dockerfile: string;
30+
/** absolute path, resolved relative to the wrangler config file. defaults to the directory of the dockerfile */
31+
image_build_context: string;
32+
image_vars?: Record<string, string>;
33+
};
34+
export type ImageURIConfig = {
35+
image_uri: string;
36+
};
37+
38+
export type InstanceTypeOrLimits =
39+
| {
40+
/** if undefined in config, defaults to instance_type */
41+
/** disk size is defined in config in mb but normalised here to bytes */
42+
disk_bytes: number;
43+
vcpu: number;
44+
memory_mib: number;
45+
}
46+
| {
47+
/** if undefined in config, defaults to "dev" */
48+
instance_type: InstanceType;
49+
};
50+
51+
/**
52+
* Shared container config that is used regardless of whether the image is from a dockerfile or a registry link.
53+
*/
54+
export type SharedContainerConfig = {
55+
/** if undefined in config, defaults to worker_name[-envName]-class_name. */
56+
name: string;
57+
/** container's DO class name */
58+
class_name: string;
59+
/** if undefined in config, defaults to 0 */
60+
max_instances: number;
61+
/** if undefined in config, defaults to "default" */
62+
scheduling_policy: SchedulingPolicy;
63+
/** if undefined in config, defaults to 25 */
64+
rollout_step_percentage: number;
65+
/** if undefined in config, defaults to "full_auto" */
66+
rollout_kind: "full_auto" | "full_manual" | "none";
67+
constraints: {
68+
regions?: string[];
69+
cities?: string[];
70+
tier: number;
71+
};
72+
observability: { logs_enabled: boolean };
73+
} & InstanceTypeOrLimits;
74+
2375
/** build/pull agnostic container options */
2476
export type ContainerDevOptions = {
25-
/** may be dockerfile or registry link */
26-
image: string;
2777
/** formatted as cloudflare-dev/workername-DOclassname:build-id */
28-
imageTag: string;
78+
image_tag: string;
2979
/** container's DO class name */
3080
class_name: string;
31-
imageBuildContext?: string;
32-
/** build time args */
33-
args?: Record<string, string>;
34-
};
81+
} & (DockerfileConfig | ImageURIConfig);

packages/containers-shared/src/utils.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { randomUUID } from "crypto";
33
import { existsSync, statSync } from "fs";
44
import path from "path";
55
import { dockerImageInspect } from "./inspect";
6-
import { type ContainerDevOptions } from "./types";
6+
import type { ContainerDevOptions } from "./types";
77
import type { StdioOptions } from "child_process";
88

99
/** helper for simple docker command call that don't require any io handling */
@@ -57,7 +57,7 @@ export const runDockerCmd = (
5757
}
5858
},
5959
ready,
60-
then: async (resolve, reject) => ready.then(resolve).catch(reject),
60+
then: async (onResolve, onReject) => ready.then(onResolve).catch(onReject),
6161
};
6262
};
6363

@@ -96,8 +96,8 @@ export const verifyDockerInstalled = async (
9696
}
9797
};
9898

99-
export function isDir(path: string) {
100-
const stats = statSync(path);
99+
export function isDir(inputPath: string) {
100+
const stats = statSync(inputPath);
101101
return stats.isDirectory();
102102
}
103103

@@ -176,7 +176,7 @@ export const cleanupContainers = async (
176176
["inherit", "pipe", "pipe"]
177177
);
178178
return true;
179-
} catch (error) {
179+
} catch {
180180
return false;
181181
}
182182
};
@@ -233,7 +233,7 @@ export async function checkExposedPorts(
233233
options: ContainerDevOptions
234234
) {
235235
const output = await dockerImageInspect(dockerPath, {
236-
imageTag: options.imageTag,
236+
imageTag: options.image_tag,
237237
formatString: "{{ len .Config.ExposedPorts }}",
238238
});
239239
if (output === "0") {

packages/containers-shared/tests/utils.test.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { mkdirSync, writeFileSync } from "fs";
22
import { checkExposedPorts, isDockerfile } from "./../src/utils";
33
import { runInTempDir } from "./helpers/run-in-tmp-dir";
4+
import type { ContainerDevOptions } from "../src/types";
45

56
describe("isDockerfile", () => {
67
const dockerfile =
@@ -71,6 +72,10 @@ vi.mock("../src/inspect", async (importOriginal) => {
7172
};
7273
});
7374

75+
const containerConfig = {
76+
dockerfile: "",
77+
class_name: "MyContainer",
78+
} as ContainerDevOptions;
7479
describe("checkExposedPorts", () => {
7580
beforeEach(() => {
7681
docketImageInspectResult = "1";
@@ -79,23 +84,14 @@ describe("checkExposedPorts", () => {
7984
it("should not error when some ports are exported", async () => {
8085
docketImageInspectResult = "1";
8186
await expect(
82-
checkExposedPorts("./container-context/Dockerfile", {
83-
image: "",
84-
imageTag: "",
85-
class_name: "MyContainer",
86-
})
87+
checkExposedPorts("docker", containerConfig)
8788
).resolves.toBeUndefined();
8889
});
8990

9091
it("should error, with an appropriate message when no ports are exported", async () => {
9192
docketImageInspectResult = "0";
92-
expect(
93-
checkExposedPorts("./container-context/Dockerfile", {
94-
image: "",
95-
imageTag: "",
96-
class_name: "MyContainer",
97-
})
98-
).rejects.toThrowErrorMatchingInlineSnapshot(`
93+
await expect(checkExposedPorts("docker", containerConfig)).rejects
94+
.toThrowErrorMatchingInlineSnapshot(`
9995
[Error: The container "MyContainer" does not expose any ports. In your Dockerfile, please expose any ports you intend to connect to.
10096
For additional information please see: https://developers.cloudflare.com/containers/local-dev/#exposing-ports.
10197
]

packages/containers-shared/tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"allowSyntheticDefaultImports": true,
88
"experimentalDecorators": true,
99
"skipLibCheck": true,
10-
"outDir": "./dist",
11-
"types": ["node"]
10+
"types": ["node"],
11+
"noUncheckedIndexedAccess": true
1212
},
1313
"include": ["src/**/*", "index.ts"],
1414
"exclude": ["dist", "node_modules"]

0 commit comments

Comments
 (0)