Skip to content

Commit b013b57

Browse files
Build container without account ID
This allows us to build containers in dry run mode. When we push the container to the managed registry, we re-tag it to include the account ID. We only ever push when not in dry run mode though, so when pushing we can safely grab the account information. Co-authored-by: Hasip Timurtaş <[email protected]>
1 parent a727db3 commit b013b57

File tree

10 files changed

+257
-125
lines changed

10 files changed

+257
-125
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Build container images without the user's account ID. This allows containers to be built and verified in dry run mode (where we do not necessarily have the user's account info).
6+
7+
When we push the image to the managed registry, we first re-tag the image to include the user's account ID so that the image has the full resolved image name.

packages/containers-shared/src/images.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { buildImage } from "./build";
2-
import { isCloudflareRegistryLink } from "./knobs";
2+
import { getCloudflareContainerRegistry, isCloudflareRegistryLink } from "./knobs";
33
import { dockerLoginManagedRegistry } from "./login";
44
import { ContainerDevOptions } from "./types";
55
import {
@@ -61,3 +61,32 @@ export async function prepareContainerImagesForDev(
6161
await checkExposedPorts(dockerPath, options);
6262
}
6363
}
64+
65+
/**
66+
* Resolve an image name to the full unambiguous name.
67+
*
68+
* For now, this only converts images stored in the managed registry to contain
69+
* the user's account ID in the path.
70+
*/
71+
export async function resolveImageName(
72+
accountId: string,
73+
image: string
74+
): Promise<string> {
75+
let url: URL;
76+
try {
77+
url = new URL(`http://${image}`);
78+
} catch (_) {
79+
return image;
80+
}
81+
82+
if (url.hostname !== getCloudflareContainerRegistry()) {
83+
return image;
84+
}
85+
86+
if (url.pathname.startsWith(`/${accountId}`)) {
87+
return image;
88+
}
89+
90+
return `${url.hostname}/${accountId}${url.pathname}`;
91+
}
92+

packages/wrangler/src/__tests__/cloudchamber/build.test.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe("buildAndMaybePush", () => {
6262
buildCmd: [
6363
"build",
6464
"-t",
65-
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
65+
`${getCloudflareContainerRegistry()}/test-app:tag`,
6666
"--platform",
6767
"linux/amd64",
6868
"--provenance=false",
@@ -73,7 +73,7 @@ describe("buildAndMaybePush", () => {
7373
dockerfile,
7474
});
7575
expect(dockerImageInspect).toHaveBeenCalledWith("/custom/docker/path", {
76-
imageTag: `${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
76+
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
7777
formatString:
7878
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
7979
});
@@ -94,7 +94,7 @@ describe("buildAndMaybePush", () => {
9494
buildCmd: [
9595
"build",
9696
"-t",
97-
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
97+
`${getCloudflareContainerRegistry()}/test-app:tag`,
9898
"--platform",
9999
"linux/amd64",
100100
"--provenance=false",
@@ -104,14 +104,26 @@ describe("buildAndMaybePush", () => {
104104
],
105105
dockerfile,
106106
});
107-
expect(runDockerCmd).toHaveBeenCalledTimes(1);
108-
expect(runDockerCmd).toHaveBeenCalledWith("docker", [
107+
108+
// 3 calls: docker tag + docker push + docker rm
109+
expect(runDockerCmd).toHaveBeenCalledTimes(3);
110+
expect(runDockerCmd).toHaveBeenNthCalledWith(1, "docker", [
111+
"tag",
112+
`${getCloudflareContainerRegistry()}/test-app:tag`,
113+
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
114+
])
115+
expect(runDockerCmd).toHaveBeenNthCalledWith(2, "docker", [
109116
"push",
110117
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
111118
]);
119+
expect(runDockerCmd).toHaveBeenNthCalledWith(3, "docker", [
120+
"image",
121+
"rm",
122+
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
123+
]);
112124
expect(dockerImageInspect).toHaveBeenCalledOnce();
113125
expect(dockerImageInspect).toHaveBeenCalledWith("docker", {
114-
imageTag: `${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
126+
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
115127
formatString:
116128
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
117129
});
@@ -121,7 +133,7 @@ describe("buildAndMaybePush", () => {
121133
it("should be able to build image and not push if it already exists in remote", async () => {
122134
vi.mocked(runDockerCmd).mockResolvedValueOnce();
123135
vi.mocked(dockerImageInspect).mockResolvedValue(
124-
'53387881 2 ["registry.cloudflare.com/some-account-id/test-app@sha256:three"]'
136+
'53387881 2 ["registry.cloudflare.com/test-app@sha256:three"]'
125137
);
126138
await runWrangler(
127139
"containers build ./container-context -t test-app:tag -p"
@@ -130,7 +142,7 @@ describe("buildAndMaybePush", () => {
130142
buildCmd: [
131143
"build",
132144
"-t",
133-
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
145+
`${getCloudflareContainerRegistry()}/test-app:tag`,
134146
"--platform",
135147
"linux/amd64",
136148
"--provenance=false",
@@ -154,11 +166,11 @@ describe("buildAndMaybePush", () => {
154166
expect(runDockerCmd).toHaveBeenNthCalledWith(2, "docker", [
155167
"image",
156168
"rm",
157-
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
169+
`${getCloudflareContainerRegistry()}/test-app:tag`,
158170
]);
159171
expect(dockerImageInspect).toHaveBeenCalledOnce();
160172
expect(dockerImageInspect).toHaveBeenCalledWith("docker", {
161-
imageTag: `${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
173+
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
162174
formatString:
163175
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
164176
});
@@ -172,7 +184,7 @@ describe("buildAndMaybePush", () => {
172184
buildCmd: [
173185
"build",
174186
"-t",
175-
`${getCloudflareContainerRegistry()}/some-account-id/test-app`,
187+
`${getCloudflareContainerRegistry()}/test-app`,
176188
"--platform",
177189
"linux/amd64",
178190
"--provenance=false",
@@ -194,7 +206,7 @@ describe("buildAndMaybePush", () => {
194206
buildCmd: [
195207
"build",
196208
"-t",
197-
`${getCloudflareContainerRegistry()}/some-account-id/test-app`,
209+
`${getCloudflareContainerRegistry()}/test-app`,
198210
"--platform",
199211
"linux/amd64",
200212
"--provenance=false",
@@ -270,27 +282,24 @@ describe("buildAndMaybePush", () => {
270282
});
271283

272284
describe("resolveAppDiskSize", () => {
273-
const accountBase = {
274-
limits: { disk_mb_per_deployment: 2000 },
275-
} as CompleteAccountCustomer;
276285
it("should return parsed app disk size", () => {
277-
const result = resolveAppDiskSize(accountBase, {
286+
const result = resolveAppDiskSize({
278287
...defaultConfiguration,
279288
configuration: { image: "", disk: { size: "500MB" } },
280289
});
281290
expect(result).toBeCloseTo(500 * 1000 * 1000, -5);
282291
});
283292

284293
it("should return default size when disk size not set", () => {
285-
const result = resolveAppDiskSize(accountBase, {
294+
const result = resolveAppDiskSize({
286295
...defaultConfiguration,
287296
configuration: { image: "" },
288297
});
289298
expect(result).toBeCloseTo(2 * 1000 * 1000 * 1000, -5);
290299
});
291300

292301
it("should return undefined if app is not passed", () => {
293-
expect(resolveAppDiskSize(accountBase, undefined)).toBeUndefined();
302+
expect(resolveAppDiskSize(undefined)).toBeUndefined();
294303
});
295304
});
296305
});

packages/wrangler/src/__tests__/cloudchamber/deploy.test.ts

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "@cloudflare/containers-shared";
88
import { http, HttpResponse } from "msw";
99
import { maybeBuildContainer } from "../../cloudchamber/deploy";
10+
import { clearCachedAccount } from "../../cloudchamber/locations";
1011
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
1112
import { mockConsoleMethods } from "../helpers/mock-console";
1213
import { mockLegacyScriptData } from "../helpers/mock-legacy-script";
@@ -33,7 +34,7 @@ vi.mock("node:child_process");
3334
describe("maybeBuildContainer", () => {
3435
it("Should return imageUpdate: true if using an image URI", async () => {
3536
const config = {
36-
image: "registry.cloudflare.com/some-account-id/some-image:uri",
37+
image: "registry.cloudflare.com/some-image:uri",
3738
class_name: "Test",
3839
};
3940
const result = await maybeBuildContainer(
@@ -121,8 +122,10 @@ describe("wrangler deploy with containers", () => {
121122
)
122123
.mockImplementationOnce(mockDockerImageInspect("my-container", "Galaxy"))
123124
.mockImplementationOnce(mockDockerLogin("mockpassword"))
124-
.mockImplementationOnce(mockDockerManifestInspect("my-container", true))
125-
.mockImplementationOnce(mockDockerPush("my-container", "Galaxy"));
125+
.mockImplementationOnce(mockDockerManifestInspect("some-account-id/my-container", true))
126+
.mockImplementationOnce(mockDockerTag("my-container", "some-account-id/my-container", "Galaxy"))
127+
.mockImplementationOnce(mockDockerPush("some-account-id/my-container", "Galaxy"))
128+
.mockImplementationOnce(mockDockerImageDelete("some-account-id/my-container", "Galaxy"));
126129

127130
writeWranglerConfig({
128131
durable_objects: {
@@ -247,8 +250,10 @@ describe("wrangler deploy with containers", () => {
247250
)
248251
.mockImplementationOnce(mockDockerImageInspect("my-container", "Galaxy"))
249252
.mockImplementationOnce(mockDockerLogin("mockpassword"))
250-
.mockImplementationOnce(mockDockerManifestInspect("my-container", true))
251-
.mockImplementationOnce(mockDockerPush("my-container", "Galaxy"));
253+
.mockImplementationOnce(mockDockerManifestInspect("some-account-id/my-container", true))
254+
.mockImplementationOnce(mockDockerTag("my-container", "some-account-id/my-container", "Galaxy"))
255+
.mockImplementationOnce(mockDockerPush("some-account-id/my-container", "Galaxy"))
256+
.mockImplementationOnce(mockDockerImageDelete("some-account-id/my-container", "Galaxy"));
252257

253258
mockContainersAccount();
254259

@@ -336,8 +341,10 @@ describe("wrangler deploy with containers", () => {
336341
)
337342
.mockImplementationOnce(mockDockerImageInspect("my-container", "Galaxy"))
338343
.mockImplementationOnce(mockDockerLogin("mockpassword"))
339-
.mockImplementationOnce(mockDockerManifestInspect("my-container", true))
340-
.mockImplementationOnce(mockDockerPush("my-container", "Galaxy"));
344+
.mockImplementationOnce(mockDockerManifestInspect("some-account-id/my-container", true))
345+
.mockImplementationOnce(mockDockerTag("my-container", "some-account-id/my-container", "Galaxy"))
346+
.mockImplementationOnce(mockDockerPush("some-account-id/my-container", "Galaxy"))
347+
.mockImplementationOnce(mockDockerImageDelete("some-account-id/my-container", "Galaxy"));
341348

342349
fs.mkdirSync("nested/src", { recursive: true });
343350
fs.writeFileSync("Dockerfile", "FROM alpine");
@@ -437,6 +444,73 @@ describe("wrangler deploy with containers", () => {
437444
});
438445
});
439446

447+
// This is a separate describe block because we intentionally do not mock any
448+
// API tokens, account ID, or authorization. The purpose of these tests is to
449+
// ensure that --dry-run mode works without requiring any API access.
450+
describe("wrangler deploy with containers dry run", () => {
451+
runInTempDir();
452+
const std = mockConsoleMethods();
453+
454+
beforeEach(() => {
455+
clearCachedAccount();
456+
});
457+
458+
afterEach(() => {
459+
vi.unstubAllEnvs();
460+
});
461+
462+
it("builds the image without pushing", async () => {
463+
vi.mocked(spawn)
464+
.mockImplementationOnce(mockDockerInfo())
465+
.mockImplementationOnce(
466+
mockDockerBuild("my-container", "worker", "FROM scratch", process.cwd())
467+
)
468+
.mockImplementationOnce(mockDockerImageInspect("my-container", "worker"))
469+
.mockImplementationOnce(mockDockerLogin("mockpassword"))
470+
.mockImplementationOnce(mockDockerManifestInspect("my-container", true))
471+
.mockImplementationOnce(mockDockerPush("my-container", "worker"));
472+
473+
vi.stubEnv("WRANGLER_DOCKER_BIN", "/usr/bin/docker");
474+
475+
fs.writeFileSync("Dockerfile", "FROM scratch");
476+
fs.writeFileSync(
477+
"index.js",
478+
`export class ExampleDurableObject {}; export default{};`
479+
);
480+
481+
writeWranglerConfig({
482+
durable_objects: {
483+
bindings: [
484+
{
485+
name: "EXAMPLE_DO_BINDING",
486+
class_name: "ExampleDurableObject",
487+
},
488+
],
489+
},
490+
containers: [
491+
{
492+
image: "./Dockerfile",
493+
name: "my-container",
494+
instances: 10,
495+
class_name: "ExampleDurableObject",
496+
},
497+
],
498+
migrations: [{ tag: "v1", new_sqlite_classes: ["ExampleDurableObject"] }],
499+
});
500+
501+
await runWrangler("deploy --dry-run index.js");
502+
expect(std.out).toMatchInlineSnapshot(`
503+
"Total Upload: xx KiB / gzip: xx KiB
504+
Building image my-container:worker
505+
Your Worker has access to the following bindings:
506+
Binding Resource
507+
env.EXAMPLE_DO_BINDING (ExampleDurableObject) Durable Object
508+
509+
--dry-run: exiting now."
510+
`);
511+
});
512+
});
513+
440514
function mockGetVersion(versionId: string) {
441515
msw.use(
442516
http.get(
@@ -549,7 +623,7 @@ function mockDockerBuild(
549623
expect(args).toEqual([
550624
"build",
551625
"-t",
552-
`${getCloudflareContainerRegistry()}/some-account-id/${containerName}:${tag}`,
626+
`${getCloudflareContainerRegistry()}/${containerName}:${tag}`,
553627
"--platform",
554628
"linux/amd64",
555629
"--provenance=false",
@@ -590,7 +664,7 @@ function mockDockerImageInspect(containerName: string, tag: string) {
590664
expect(args).toEqual([
591665
"image",
592666
"inspect",
593-
`${getCloudflareContainerRegistry()}/some-account-id/${containerName}:${tag}`,
667+
`${getCloudflareContainerRegistry()}/${containerName}:${tag}`,
594668
"--format",
595669
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
596670
]);
@@ -612,14 +686,26 @@ function mockDockerImageInspect(containerName: string, tag: string) {
612686
setImmediate(() => {
613687
stdout.emit(
614688
"data",
615-
`123456 4 ["${getCloudflareContainerRegistry()}/some-account-id/${containerName}@sha256:three"]`
689+
`123456 4 ["${getCloudflareContainerRegistry()}/${containerName}@sha256:three"]`
616690
);
617691
});
618692

619693
return child as unknown as ChildProcess;
620694
};
621695
}
622696

697+
function mockDockerImageDelete(containerName: string, tag: string) {
698+
return (cmd: string, args: readonly string[]) => {
699+
expect(cmd).toBe("/usr/bin/docker");
700+
expect(args).toEqual([
701+
"image",
702+
"rm",
703+
`${getCloudflareContainerRegistry()}/${containerName}:${tag}`,
704+
]);
705+
return defaultChildProcess();
706+
};
707+
}
708+
623709
function mockDockerLogin(expectedPassword: string) {
624710
return (cmd: string, _args: readonly string[]) => {
625711
expect(cmd).toBe("/usr/bin/docker");
@@ -653,7 +739,7 @@ function mockDockerManifestInspect(containerName: string, shouldFail = true) {
653739
expect(args).toEqual([
654740
"manifest",
655741
"inspect",
656-
`${getCloudflareContainerRegistry()}/some-account-id/${containerName}@three`,
742+
`${getCloudflareContainerRegistry()}/${containerName}@three`,
657743
]);
658744
const readable = new Writable({
659745
write() {},
@@ -679,7 +765,19 @@ function mockDockerPush(containerName: string, tag: string) {
679765
expect(cmd).toBe("/usr/bin/docker");
680766
expect(args).toEqual([
681767
"push",
682-
`${getCloudflareContainerRegistry()}/some-account-id/${containerName}:${tag}`,
768+
`${getCloudflareContainerRegistry()}/${containerName}:${tag}`,
769+
]);
770+
return defaultChildProcess();
771+
};
772+
}
773+
774+
function mockDockerTag(from: string, to: string, tag: string) {
775+
return (cmd: string, args: readonly string[]) => {
776+
expect(cmd).toBe("/usr/bin/docker");
777+
expect(args).toEqual([
778+
"tag",
779+
`${getCloudflareContainerRegistry()}/${from}:${tag}`,
780+
`${getCloudflareContainerRegistry()}/${to}:${tag}`,
683781
]);
684782
return defaultChildProcess();
685783
};

0 commit comments

Comments
 (0)