Skip to content

Commit b550b1c

Browse files
committed
Update buildAndMaybePush to use more robust method to find local digests
Older versions of docker sometimes will report the digest as "<none>" which would break with the previous implementation. This implementation should correctly grab the values.
1 parent 6c6afbd commit b550b1c

File tree

9 files changed

+92
-89
lines changed

9 files changed

+92
-89
lines changed

.changeset/orange-teams-glow.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cloudflare/containers-shared": patch
3+
"wrangler": patch
4+
---
5+
6+
Update container builds to use a more robust method for detecting if the currently built image already exists.

packages/containers-shared/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@ export * from "./src/utils";
66
export * from "./src/types";
77
export * from "./src/inspect";
88
export * from "./src/registry";
9-
export * from "./src/images";

packages/containers-shared/src/images.ts

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

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,14 +240,14 @@ describe("cloudchamber apply", () => {
240240
{
241241
id: "abc",
242242
name: "my-container-app",
243-
max_instances: 3,
243+
max_instances: 4,
244244
instances: 3,
245245
created_at: new Date().toString(),
246246
account_id: "1",
247247
version: 1,
248248
scheduling_policy: SchedulingPolicy.DEFAULT,
249249
configuration: {
250-
image: "./Dockerfile2",
250+
image: "./Dockerfile",
251251
},
252252
constraints: {
253253
tier: 1,
@@ -267,12 +267,11 @@ describe("cloudchamber apply", () => {
267267
268268
├ EDIT my-container-app
269269
270-
│ [containers.configuration]
271-
│ - image = \\"./Dockerfile2\\"
272-
│ + image = \\"./Dockerfile\\"
273-
274-
│ [containers.constraints]
275-
│ ...
270+
│ [[containers]]
271+
│ instances = 0
272+
│ - max_instances = 4
273+
│ + max_instances = 3
274+
│ name = \\"my-container-app\\"
276275
277276
├ NEW my-container-app-2
278277

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
dockerImageInspect,
55
dockerLoginManagedRegistry,
66
getCloudflareContainerRegistry,
7-
getDockerImageDigest,
87
runDockerCmd,
98
} from "@cloudflare/containers-shared";
109
import { ensureDiskLimits } from "../../cloudchamber/build";
@@ -31,7 +30,6 @@ vi.mock("@cloudflare/containers-shared", async (importOriginal) => {
3130
runDockerCmd: vi.fn(),
3231
dockerBuild: vi.fn(),
3332
dockerImageInspect: vi.fn(),
34-
getDockerImageDigest: vi.fn(),
3533
});
3634
});
3735

@@ -46,8 +44,7 @@ describe("buildAndMaybePush", () => {
4644

4745
beforeEach(() => {
4846
vi.clearAllMocks();
49-
vi.mocked(dockerImageInspect).mockResolvedValue("53387881 2");
50-
vi.mocked(getDockerImageDigest).mockRejectedValue("failed");
47+
vi.mocked(dockerImageInspect).mockResolvedValue("53387881 2 []");
5148
mkdirSync("./container-context");
5249

5350
writeFileSync("./container-context/Dockerfile", dockerfile);
@@ -77,7 +74,8 @@ describe("buildAndMaybePush", () => {
7774
});
7875
expect(dockerImageInspect).toHaveBeenCalledWith("/custom/docker/path", {
7976
imageTag: `${getCloudflareContainerRegistry()}/test_account_id/test-app:tag`,
80-
formatString: "{{ .Size }} {{ len .RootFS.Layers }}",
77+
formatString:
78+
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
8179
});
8280
expect(runDockerCmd).toHaveBeenCalledWith("/custom/docker/path", [
8381
"push",
@@ -114,14 +112,17 @@ describe("buildAndMaybePush", () => {
114112
expect(dockerImageInspect).toHaveBeenCalledOnce();
115113
expect(dockerImageInspect).toHaveBeenCalledWith("docker", {
116114
imageTag: `${getCloudflareContainerRegistry()}/test_account_id/test-app:tag`,
117-
formatString: "{{ .Size }} {{ len .RootFS.Layers }}",
115+
formatString:
116+
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
118117
});
119118
expect(dockerLoginManagedRegistry).toHaveBeenCalledOnce();
120119
});
121120

122121
it("should be able to build image and not push if it already exists in remote", async () => {
123-
vi.mocked(getDockerImageDigest).mockResolvedValue("three");
124122
vi.mocked(runDockerCmd).mockResolvedValueOnce();
123+
vi.mocked(dockerImageInspect).mockResolvedValue(
124+
'53387881 2 ["registry.cloudflare.com/test_account_id/test-app@sha256:three"]'
125+
);
125126
await runWrangler(
126127
"containers build ./container-context -t test-app:tag -p"
127128
);
@@ -146,7 +147,7 @@ describe("buildAndMaybePush", () => {
146147
[
147148
"manifest",
148149
"inspect",
149-
`${getCloudflareContainerRegistry()}/test_account_id/test-app@three`,
150+
`${getCloudflareContainerRegistry()}/test_account_id/test-app@sha256:three`,
150151
],
151152
"ignore"
152153
);
@@ -158,7 +159,8 @@ describe("buildAndMaybePush", () => {
158159
expect(dockerImageInspect).toHaveBeenCalledOnce();
159160
expect(dockerImageInspect).toHaveBeenCalledWith("docker", {
160161
imageTag: `${getCloudflareContainerRegistry()}/test_account_id/test-app:tag`,
161-
formatString: "{{ .Size }} {{ len .RootFS.Layers }}",
162+
formatString:
163+
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
162164
});
163165
expect(dockerLoginManagedRegistry).toHaveBeenCalledOnce();
164166
});

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

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Buffer } from "node:buffer";
2-
import { execFile, spawn, spawnSync } from "node:child_process";
2+
import { spawn, spawnSync } from "node:child_process";
33
import { randomFillSync } from "node:crypto";
44
import * as fs from "node:fs";
55
import * as path from "node:path";
@@ -8711,28 +8711,6 @@ addEventListener('fetch', event => {});`
87118711
},
87128712
} as unknown as ChildProcess;
87138713
}
8714-
vi.mocked(execFile)
8715-
// docker images first call
8716-
.mockImplementationOnce((cmd, args, callback) => {
8717-
expect(cmd).toBe("/usr/bin/docker");
8718-
expect(args).toEqual([
8719-
"images",
8720-
"--digests",
8721-
"--format",
8722-
"{{.Digest}}",
8723-
getCloudflareContainerRegistry() +
8724-
"/test_account_id/my-container:Galaxy",
8725-
]);
8726-
if (callback) {
8727-
const back = callback as (
8728-
error: Error | null,
8729-
stdout: string,
8730-
stderr: string
8731-
) => void;
8732-
back(null, "three\n", "");
8733-
}
8734-
return {} as ChildProcess;
8735-
});
87368714

87378715
vi.mocked(spawn)
87388716
// 1. docker build
@@ -8783,7 +8761,7 @@ addEventListener('fetch', event => {});`
87838761
"inspect",
87848762
`${getCloudflareContainerRegistry()}/test_account_id/my-container:Galaxy`,
87858763
"--format",
8786-
"{{ .Size }} {{ len .RootFS.Layers }}",
8764+
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
87878765
]);
87888766

87898767
const stdout = new PassThrough();
@@ -8803,7 +8781,10 @@ addEventListener('fetch', event => {});`
88038781

88048782
// Simulate docker output
88058783
setImmediate(() => {
8806-
stdout.emit("data", "123456 4\n");
8784+
stdout.emit(
8785+
"data",
8786+
`123456 4 ["registry.cloudflare.com/test_account_id/test-app@sha256:three"]\n`
8787+
);
88078788
});
88088789

88098790
return child as unknown as ChildProcess;

packages/wrangler/src/cloudchamber/apply.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,12 @@ function sortObjectRecursive<T = Record<string | number, unknown>>(
314314
}
315315

316316
export async function apply(
317-
args: { skipDefaults: boolean | undefined; json: boolean; env?: string },
317+
args: {
318+
skipDefaults: boolean | undefined;
319+
json: boolean;
320+
env?: string;
321+
pushed?: boolean;
322+
},
318323
config: Config
319324
) {
320325
startSection(
@@ -378,7 +383,7 @@ export async function apply(
378383

379384
for (const appConfigNoDefaults of config.containers) {
380385
const application = applicationByNames[appConfigNoDefaults.name];
381-
if (!appConfigNoDefaults.configuration.image && application) {
386+
if (!args.pushed && application) {
382387
appConfigNoDefaults.configuration.image = application.configuration.image;
383388
}
384389
const appConfig = containerAppToCreateApplication(
@@ -742,7 +747,12 @@ export async function applyCommand(
742747
config: Config
743748
) {
744749
return apply(
745-
{ skipDefaults: args.skipDefaults, env: args.env, json: args.json },
750+
{
751+
skipDefaults: args.skipDefaults,
752+
env: args.env,
753+
json: args.json,
754+
pushed: false,
755+
},
746756
config
747757
);
748758
}

packages/wrangler/src/cloudchamber/build.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
dockerImageInspect,
77
dockerLoginManagedRegistry,
88
getCloudflareRegistryWithAccountNamespace,
9-
getDockerImageDigest,
109
isDir,
1110
runDockerCmd,
1211
} from "@cloudflare/containers-shared";
@@ -79,7 +78,7 @@ export async function buildAndMaybePush(
7978
pathToDocker: string,
8079
push: boolean,
8180
containerConfig?: ContainerApp
82-
): Promise<string> {
81+
): Promise<{ image: string; pushed: boolean }> {
8382
try {
8483
// account is also used to check limits below, so it is better to just pull the entire
8584
// account information here
@@ -110,10 +109,11 @@ export async function buildAndMaybePush(
110109
// account's disk size limits
111110
const inspectOutput = await dockerImageInspect(pathToDocker, {
112111
imageTag,
113-
formatString: "{{ .Size }} {{ len .RootFS.Layers }}",
112+
formatString:
113+
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
114114
});
115115

116-
const [sizeStr, layerStr] = inspectOutput.split(" ");
116+
const [sizeStr, layerStr, repoDigests] = inspectOutput.split(" ");
117117
const size = parseInt(sizeStr, 10);
118118
const layers = parseInt(layerStr, 10);
119119

@@ -126,18 +126,38 @@ export async function buildAndMaybePush(
126126
account: account,
127127
containerApp: containerConfig,
128128
});
129-
129+
let pushed = false;
130130
if (push) {
131131
await dockerLoginManagedRegistry(pathToDocker);
132132
try {
133+
// we don't try to parse repoDigests until here because we
134+
// don't want to fail on parse errors if we aren't pushing anyways
135+
// as it's only relevant when pushing.
136+
const parsedDigests = JSON.parse(repoDigests);
137+
if (!Array.isArray(parsedDigests)) {
138+
// If it's not the format we expect, fall back to pushing
139+
// since it's annoying but safe.
140+
throw new Error(
141+
"Expected RepoDigests from docker inspect to be an array but it wasn't"
142+
);
143+
}
144+
133145
const repositoryOnly = imageTag.split(":")[0];
134146
// if this succeeds it means this image already exists remotely
135147
// if it fails it means it doesn't exist remotely and should be pushed.
136-
const localDigest = await getDockerImageDigest(pathToDocker, imageTag);
137-
const digest = repositoryOnly + "@" + localDigest;
148+
const digests = parsedDigests.filter(
149+
(d): d is string =>
150+
typeof d === "string" && d.split("@")[0] === repositoryOnly
151+
);
152+
if (digests.length !== 1) {
153+
throw new Error(
154+
`Expected there to only be 1 valid digests for this repository: ${repositoryOnly} but there were ${digests.length}`
155+
);
156+
}
157+
138158
await runDockerCmd(
139159
pathToDocker,
140-
["manifest", "inspect", digest],
160+
["manifest", "inspect", digests[0]],
141161
"ignore"
142162
);
143163

@@ -146,14 +166,20 @@ export async function buildAndMaybePush(
146166
`Untagging built image: ${imageTag} since there was no change.`
147167
);
148168
await runDockerCmd(pathToDocker, ["image", "rm", imageTag]);
149-
return "";
169+
return { image: digests[0], pushed: false };
150170
} catch (error) {
151171
logger.log(`Image does not exist remotely, pushing: ${imageTag}`);
172+
if (error instanceof Error) {
173+
logger.debug(
174+
`Checking for local image ${imageTag} failed with error: ${error.message}`
175+
);
176+
}
152177

153178
await runDockerCmd(pathToDocker, ["push", imageTag]);
179+
pushed = true;
154180
}
155181
}
156-
return imageTag;
182+
return { image: imageTag, pushed: pushed };
157183
} catch (error) {
158184
if (error instanceof Error) {
159185
throw new UserError(error.message);

packages/wrangler/src/cloudchamber/deploy.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ export async function maybeBuildContainer(
1717
imageTag: string,
1818
dryRun: boolean,
1919
pathToDocker: string
20-
) {
20+
): Promise<{ image: string; pushed: boolean }> {
2121
try {
2222
if (
2323
!isDockerfile(
2424
containerConfig.image ?? containerConfig.configuration.image
2525
)
2626
) {
27-
return containerConfig.image ?? containerConfig.configuration.image;
27+
const image =
28+
containerConfig.image ?? containerConfig.configuration.image;
29+
return { image: image, pushed: false };
2830
}
2931
} catch (err) {
3032
if (err instanceof Error) {
@@ -36,13 +38,13 @@ export async function maybeBuildContainer(
3638

3739
const options = getBuildArguments(containerConfig, imageTag);
3840
logger.log("Building image", options.tag);
39-
const tag = await buildAndMaybePush(
41+
const buildResult = await buildAndMaybePush(
4042
options,
4143
pathToDocker,
4244
!dryRun,
4345
containerConfig
4446
);
45-
return tag;
47+
return buildResult;
4648
}
4749

4850
export type DeployContainersArgs = {
@@ -105,17 +107,19 @@ export async function deployContainers(
105107
],
106108
};
107109

108-
const image = await maybeBuildContainer(
110+
const buildResult = await maybeBuildContainer(
109111
container,
110112
versionId,
111113
dryRun,
112114
pathToDocker
113115
);
116+
container.configuration.image = buildResult.image;
117+
container.image = buildResult.image;
114118

115-
container.configuration.image = image;
116-
container.image = image;
117-
118-
await apply({ skipDefaults: false, json: true, env }, configuration);
119+
await apply(
120+
{ skipDefaults: false, json: true, env, pushed: buildResult.pushed },
121+
configuration
122+
);
119123
}
120124
}
121125

0 commit comments

Comments
 (0)