Skip to content

Commit b1c9139

Browse files
authored
Do not update container apps image if the image is unchanged (#9565)
Previously we would build the image and retag even if the only change was the build-related metadata. This changes that to detect if the build has resulted in a new image, and if it has not remove the added tag and ensures the applications image is not updated.
1 parent c2b6f11 commit b1c9139

File tree

9 files changed

+133
-7
lines changed

9 files changed

+133
-7
lines changed

.changeset/rare-wasps-cough.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Ensure that a container applications image configuration is not updated if there were not changes to the image.

packages/containers-shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ 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/build.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ export async function constructBuildCommand(
77
logger?: Logger
88
) {
99
const platform = options.platform ?? "linux/amd64";
10-
const buildCmd = ["build", "-t", options.tag, "--platform", platform];
10+
const buildCmd = [
11+
"build",
12+
"-t",
13+
options.tag,
14+
"--platform",
15+
platform,
16+
"--provenance=false",
17+
];
1118

1219
if (options.args) {
1320
for (const arg in options.args) {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { execFile } from "child_process";
2+
3+
// Returns a list of docker image ids matching the provided repository:[tag]
4+
export async function getDockerImageDigest(
5+
dockerPath: string,
6+
imageTag: string
7+
): Promise<string> {
8+
return new Promise((resolve, reject) => {
9+
execFile(
10+
dockerPath,
11+
["images", "--digests", "--format", "{{.Digest}}", imageTag],
12+
(error, stdout, stderr) => {
13+
if (error) {
14+
return reject(
15+
new Error(
16+
`Failed getting docker image digest for image: ${imageTag} with error: ${error}.`
17+
)
18+
);
19+
}
20+
return resolve(stdout.trim());
21+
}
22+
);
23+
});
24+
}

packages/containers-shared/src/login.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export async function dockerLoginManagedRegistry(pathToDocker: string) {
1515
getCloudflareContainerRegistry(),
1616
{
1717
expiration_minutes: expirationMinutes,
18-
permissions: ["push"] as ImageRegistryPermissions[],
18+
permissions: ["push", "pull"] as ImageRegistryPermissions[],
1919
}
2020
);
2121

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ describe("buildAndMaybePush", () => {
6565
`${getCloudflareContainerRegistry()}/test_account_id/test-app:tag`,
6666
"--platform",
6767
"linux/amd64",
68+
"--provenance=false",
6869
"-f",
6970
"-",
7071
"./container-context",
@@ -95,6 +96,7 @@ describe("buildAndMaybePush", () => {
9596
`${getCloudflareContainerRegistry()}/test_account_id/test-app:tag`,
9697
"--platform",
9798
"linux/amd64",
99+
"--provenance=false",
98100
"-f",
99101
"-",
100102
"./container-context",
@@ -124,6 +126,7 @@ describe("buildAndMaybePush", () => {
124126
`${getCloudflareContainerRegistry()}/test_account_id/test-app`,
125127
"--platform",
126128
"linux/amd64",
129+
"--provenance=false",
127130
"-f",
128131
"-",
129132
"./container-context",
@@ -145,6 +148,7 @@ describe("buildAndMaybePush", () => {
145148
`${getCloudflareContainerRegistry()}/test_account_id/test-app`,
146149
"--platform",
147150
"linux/amd64",
151+
"--provenance=false",
148152
"--network",
149153
"host",
150154
"-f",

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

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Buffer } from "node:buffer";
2-
import { spawn, spawnSync } from "node:child_process";
2+
import { execFile, 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";
@@ -8708,6 +8708,28 @@ addEventListener('fetch', event => {});`
87088708
},
87098709
} as unknown as ChildProcess;
87108710
}
8711+
vi.mocked(execFile)
8712+
// docker images first call
8713+
.mockImplementationOnce((cmd, args, callback) => {
8714+
expect(cmd).toBe("/usr/bin/docker");
8715+
expect(args).toEqual([
8716+
"images",
8717+
"--digests",
8718+
"--format",
8719+
"{{.Digest}}",
8720+
getCloudflareContainerRegistry() +
8721+
"/test_account_id/my-container:Galaxy",
8722+
]);
8723+
if (callback) {
8724+
const back = callback as (
8725+
error: Error | null,
8726+
stdout: string,
8727+
stderr: string
8728+
) => void;
8729+
back(null, "three\n", "");
8730+
}
8731+
return {} as ChildProcess;
8732+
});
87118733

87128734
vi.mocked(spawn)
87138735
// 1. docker build
@@ -8720,6 +8742,7 @@ addEventListener('fetch', event => {});`
87208742
"/test_account_id/my-container:Galaxy",
87218743
"--platform",
87228744
"linux/amd64",
8745+
"--provenance=false",
87238746
"-f",
87248747
"-",
87258748
".",
@@ -8808,7 +8831,40 @@ addEventListener('fetch', event => {});`
88088831
},
88098832
} as unknown as ChildProcess;
88108833
})
8811-
// 4. docker push
8834+
// 4. docker manifest inspect
8835+
.mockImplementationOnce((cmd, args) => {
8836+
expect(cmd).toBe("/usr/bin/docker");
8837+
expect(args[0]).toBe("manifest");
8838+
expect(args[1]).toBe("inspect");
8839+
expect(args[2]).toEqual("my-container@three");
8840+
expect(args).toEqual([
8841+
"manifest",
8842+
"inspect",
8843+
`${getCloudflareContainerRegistry()}/test_account_id/my-container@three`,
8844+
]);
8845+
const readable = new Writable({
8846+
write() {},
8847+
final() {},
8848+
});
8849+
return {
8850+
stdout: Buffer.from(
8851+
"i promise I am an unsuccessful docker manifest call"
8852+
),
8853+
stdin: readable,
8854+
on: function (
8855+
reason: string,
8856+
cbPassed: (code: number) => unknown
8857+
) {
8858+
if (reason === "close") {
8859+
// We always fail this for this test because this is meant to stop
8860+
// us pushing if it succeeds and we want to go through the push workflow also
8861+
cbPassed(1);
8862+
}
8863+
return this;
8864+
},
8865+
} as unknown as ChildProcess;
8866+
})
8867+
// 5. docker push
88128868
.mockImplementationOnce((cmd, args) => {
88138869
expect(cmd).toBe("/usr/bin/docker");
88148870
expect(args).toEqual([
@@ -8868,7 +8924,7 @@ addEventListener('fetch', event => {});`
88688924
async ({ request }) => {
88698925
const json =
88708926
(await request.json()) as ImageRegistryCredentialsConfiguration;
8871-
expect(json.permissions).toEqual(["push"]);
8927+
expect(json.permissions).toEqual(["push", "pull"]);
88728928

88738929
return HttpResponse.json({
88748930
account_id: "test_account_id",
@@ -8928,6 +8984,7 @@ addEventListener('fetch', event => {});`
89288984
89298985
Uploaded test-name (TIMINGS)
89308986
Building image my-container:Galaxy
8987+
Image does not exist remotely, pushing: registry.cloudflare.com/test_account_id/my-container:Galaxy
89318988
Deployed test-name triggers (TIMINGS)
89328989
https://test-name.test-sub-domain.workers.dev
89338990
Current Version ID: Galaxy-Class"

packages/wrangler/src/cloudchamber/apply.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,12 +377,15 @@ export async function apply(
377377
log(dim("Container application changes\n"));
378378

379379
for (const appConfigNoDefaults of config.containers) {
380+
const application = applicationByNames[appConfigNoDefaults.name];
381+
if (!appConfigNoDefaults.configuration.image && application) {
382+
appConfigNoDefaults.configuration.image = application.configuration.image;
383+
}
380384
const appConfig = containerAppToCreateApplication(
381385
appConfigNoDefaults,
382386
args.skipDefaults
383387
);
384388

385-
const application = applicationByNames[appConfig.name];
386389
if (application !== undefined && application !== null) {
387390
// we need to sort the objects (by key) because the diff algorithm works with
388391
// lines

packages/wrangler/src/cloudchamber/build.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
dockerImageInspect,
77
dockerLoginManagedRegistry,
88
getCloudflareRegistryWithAccountNamespace,
9+
getDockerImageDigest,
910
isDir,
1011
runDockerCmd,
1112
} from "@cloudflare/containers-shared";
@@ -99,10 +100,12 @@ export async function buildAndMaybePush(
99100
},
100101
logger
101102
);
103+
102104
await dockerBuild(pathToDocker, {
103105
buildCmd,
104106
dockerfile,
105107
});
108+
106109
// ensure the account is not allowed to build anything that exceeds the current
107110
// account's disk size limits
108111
const inspectOutput = await dockerImageInspect(pathToDocker, {
@@ -126,7 +129,29 @@ export async function buildAndMaybePush(
126129

127130
if (push) {
128131
await dockerLoginManagedRegistry(pathToDocker);
129-
await runDockerCmd(pathToDocker, ["push", imageTag]);
132+
try {
133+
const repositoryOnly = imageTag.split(":")[0];
134+
// if this succeeds it means this image already exists remotely
135+
// 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;
138+
await runDockerCmd(
139+
pathToDocker,
140+
["manifest", "inspect", digest],
141+
"ignore"
142+
);
143+
144+
logger.log("Image already exists remotely, skipping push");
145+
logger.debug(
146+
`Untagging built image: ${imageTag} since there was no change.`
147+
);
148+
await runDockerCmd(pathToDocker, ["image", "rm", imageTag]);
149+
return "";
150+
} catch (error) {
151+
logger.log(`Image does not exist remotely, pushing: ${imageTag}`);
152+
153+
await runDockerCmd(pathToDocker, ["push", imageTag]);
154+
}
130155
}
131156
return imageTag;
132157
} catch (error) {

0 commit comments

Comments
 (0)