Skip to content

Commit eb32a3a

Browse files
authored
fix re-deploying after container deploys fail in an incomplete state (#10253)
* use remote digest if no previous deployment * changeset * pr feedback * fixup * remove random comment from another pr...
1 parent b524a6f commit eb32a3a

File tree

6 files changed

+61
-71
lines changed

6 files changed

+61
-71
lines changed

.changeset/whole-readers-admire.md

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+
fix redeploying container apps when previous deploy failed or container (but not image) was deleted.
6+
7+
Previously this failed with `No changes detected but no previous image found` as we assumed there would be a previous deployment when an image exists in the registry.

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

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
SchedulingPolicy,
88
} from "@cloudflare/containers-shared";
99
import { http, HttpResponse } from "msw";
10-
import { maybeBuildContainer } from "../../cloudchamber/deploy";
1110
import { clearCachedAccount } from "../../cloudchamber/locations";
1211
import { mockAccountV4 as mockContainersAccount } from "../cloudchamber/utils";
1312
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
@@ -27,30 +26,12 @@ import { writeWranglerConfig } from "../helpers/write-wrangler-config";
2726
import type {
2827
AccountRegistryToken,
2928
Application,
30-
ContainerNormalizedConfig,
3129
CreateApplicationRequest,
3230
ImageRegistryCredentialsConfiguration,
3331
} from "@cloudflare/containers-shared";
3432
import type { ChildProcess } from "node:child_process";
3533

3634
vi.mock("node:child_process");
37-
describe("maybeBuildContainer", () => {
38-
it("Should return imageUpdate: true if using an image URI", async () => {
39-
const config = {
40-
image_uri: "registry.cloudflare.com/some-image:uri",
41-
class_name: "Test",
42-
} as ContainerNormalizedConfig;
43-
const result = await maybeBuildContainer(
44-
config,
45-
"some-tag:thing",
46-
false,
47-
"/usr/bin/docker"
48-
);
49-
expect(result.newImageLink).toEqual(
50-
"registry.cloudflare.com/some-image:uri"
51-
);
52-
});
53-
});
5435

5536
describe("wrangler deploy with containers", () => {
5637
runInTempDir();

packages/wrangler/src/cloudchamber/build.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,24 @@ export function pushYargs(yargs: CommonYargsArgv) {
7979
.positional("TAG", { type: "string", demandOption: true });
8080
}
8181

82+
/**
83+
*
84+
* `{ remoteDigest: string }` implies the image already exists remotely. we will
85+
* try and replace this with the image tag from the last deployment if possible.
86+
* If a deployment failed between push and deploy, we can't know for certain
87+
* what the tag of the last push was, so we will use the digest instead.
88+
*
89+
* `{ newTag: string }` implies the image was built and pushed and the deployment
90+
* should be associated with a new tag.
91+
*/
92+
export type ImageRef = { remoteDigest: string } | { newTag: string };
93+
8294
export async function buildAndMaybePush(
8395
args: BuildArgs,
8496
pathToDocker: string,
8597
push: boolean,
8698
containerConfig?: Exclude<ContainerNormalizedConfig, ImageURIConfig>
87-
): Promise<{ image: string; pushed: boolean }> {
99+
): Promise<ImageRef> {
88100
try {
89101
const imageTag = `${getCloudflareContainerRegistry()}/${args.tag}`;
90102
const { buildCmd, dockerfile } = await constructBuildCommand(
@@ -104,7 +116,6 @@ export async function buildAndMaybePush(
104116
dockerfile,
105117
}).ready;
106118

107-
let pushed = false;
108119
if (push) {
109120
/**
110121
* Get `RepoDigests` and `Id`:
@@ -199,7 +210,7 @@ export async function buildAndMaybePush(
199210
`Untagging built image: ${args.tag} since there was no change.`
200211
);
201212
await runDockerCmd(pathToDocker, ["image", "rm", imageTag]);
202-
return { image: remoteDigest, pushed: false };
213+
return { remoteDigest };
203214
}
204215
} catch (error) {
205216
if (error instanceof Error) {
@@ -213,17 +224,15 @@ export async function buildAndMaybePush(
213224
account.external_account_id,
214225
args.tag
215226
);
216-
217227
logger.log(
218228
`Image does not exist remotely, pushing: ${namespacedImageTag}`
219229
);
220230
await runDockerCmd(pathToDocker, ["tag", imageTag, namespacedImageTag]);
221231
await runDockerCmd(pathToDocker, ["push", namespacedImageTag]);
222232
await runDockerCmd(pathToDocker, ["image", "rm", namespacedImageTag]);
223-
pushed = true;
224233
}
225234

226-
return { image: imageTag, pushed };
235+
return { newTag: imageTag };
227236
} catch (error) {
228237
if (error instanceof Error) {
229238
throw new UserError(error.message, { cause: error });

packages/wrangler/src/cloudchamber/deploy.ts

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,23 @@ import { logger } from "../logger";
88
import { fetchVersion } from "../versions/api";
99
import { buildAndMaybePush } from "./build";
1010
import { fillOpenAPIConfiguration } from "./common";
11-
import type { ContainerNormalizedConfig } from "@cloudflare/containers-shared/src/types";
11+
import type { ImageRef } from "./build";
12+
import type {
13+
ContainerNormalizedConfig,
14+
ImageURIConfig,
15+
} from "@cloudflare/containers-shared/src/types";
1216

13-
export async function maybeBuildContainer(
14-
containerConfig: ContainerNormalizedConfig,
17+
export async function buildContainer(
18+
containerConfig: Exclude<ContainerNormalizedConfig, ImageURIConfig>,
1519
/** just the tag component. will be prefixed with the container name */
1620
imageTag: string,
1721
dryRun: boolean,
1822
pathToDocker: string
19-
): Promise<{ newImageLink: string | undefined }> {
20-
if ("image_uri" in containerConfig) {
21-
return {
22-
// We don't know at this point whether the image has changed
23-
// but we need to make sure API checks so
24-
// we always set this to the registry link.
25-
newImageLink: containerConfig.image_uri,
26-
};
27-
}
23+
): Promise<ImageRef> {
2824
const imageFullName = containerConfig.name + ":" + imageTag.split("-")[0];
2925
logger.log("Building image", imageFullName);
3026

31-
const buildResult = await buildAndMaybePush(
27+
return await buildAndMaybePush(
3228
{
3329
tag: imageFullName,
3430
pathToDockerfile: containerConfig.dockerfile,
@@ -39,13 +35,6 @@ export async function maybeBuildContainer(
3935
!dryRun,
4036
containerConfig
4137
);
42-
43-
if (buildResult.pushed) {
44-
return { newImageLink: buildResult.image };
45-
}
46-
// if the image has not changed, it will not have been pushed
47-
// so we don't need to update anything when we apply the container config
48-
return { newImageLink: undefined };
4938
}
5039

5140
export type DeployContainersArgs = {
@@ -65,13 +54,18 @@ export async function deployContainers(
6554

6655
const pathToDocker = getDockerPath();
6756
const version = await fetchVersion(config, accountId, scriptName, versionId);
57+
let imageRef: ImageRef;
6858
for (const container of normalisedContainerConfig) {
69-
const buildResult = await maybeBuildContainer(
70-
container,
71-
versionId,
72-
false,
73-
pathToDocker
74-
);
59+
if ("dockerfile" in container) {
60+
imageRef = await buildContainer(
61+
container,
62+
versionId,
63+
false,
64+
pathToDocker
65+
);
66+
} else {
67+
imageRef = { newTag: container.image_uri };
68+
}
7569
const targetDurableObject = version.resources.bindings.find(
7670
(durableObject) =>
7771
durableObject.type === "durable_object_namespace" &&
@@ -94,7 +88,7 @@ export async function deployContainers(
9488

9589
await apply(
9690
{
97-
newImageLink: buildResult.newImageLink,
91+
imageRef,
9892
durable_object_namespace_id: targetDurableObject.namespace_id,
9993
},
10094
container,

packages/wrangler/src/containers/deploy.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* Note! Much of this is copied and modified from cloudchamber/apply.ts
33
* However this code is only used for containers interactions, not cloudchamber ones!
44
*/
5-
import assert from "node:assert";
65
import {
76
endSection,
87
log,
@@ -30,6 +29,7 @@ import {
3029
sortObjectRecursive,
3130
stripUndefined,
3231
} from "../utils/sortObjectRecursive";
32+
import type { ImageRef } from "../cloudchamber/build";
3333
import type { Config } from "../config";
3434
import type { ContainerApp } from "../config/environment";
3535
import type {
@@ -191,12 +191,7 @@ function containerConfigToCreateRequest(
191191

192192
export async function apply(
193193
args: {
194-
imageUpdateRequired?: boolean;
195-
/**
196-
* If the image was built and pushed, or is a registry link, we have to update the image ref and this will be defined
197-
* If it is undefined, the image has not changed, and we do not need to update the image ref
198-
*/
199-
newImageLink: string | undefined;
194+
imageRef: ImageRef;
200195
durable_object_namespace_id: string;
201196
},
202197
containerConfig: ContainerNormalizedConfig,
@@ -224,14 +219,16 @@ export async function apply(
224219
(app) => app.name === containerConfig.name
225220
);
226221

222+
// if there is a remote digest, it indicates that the image already exists in the managed registry
223+
// so we should try and use the tag from the previous deployment if possible.
224+
// however deployments that fail after push may result in no previous app but the image still existing
225+
const imageRef =
226+
"remoteDigest" in args.imageRef
227+
? prevApp?.configuration.image ?? args.imageRef.remoteDigest
228+
: args.imageRef.newTag;
227229
log(dim("Container application changes\n"));
228230

229231
const accountId = config.account_id || (await getAccountId(config));
230-
const imageRef = args.newImageLink ?? prevApp?.configuration.image;
231-
232-
// image ref is undefined if the image is a dockerfile and has not changed since the last deploy
233-
// if image ref is undefined and there is no previous app, something weird has happened.
234-
assert(imageRef, "No changes detected but no previous image found");
235232

236233
// let's always convert normalised container config -> CreateApplicationRequest
237234
// since CreateApplicationRequest is a superset of ModifyApplicationRequestBody

packages/wrangler/src/deploy/deploy.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import PQueue from "p-queue";
88
import { Response } from "undici";
99
import { syncAssets } from "../assets";
1010
import { fetchListResult, fetchResult } from "../cfetch";
11-
import { deployContainers, maybeBuildContainer } from "../cloudchamber/deploy";
11+
import { buildContainer, deployContainers } from "../cloudchamber/deploy";
1212
import { configFileName, formatConfigSnippet } from "../config";
1313
import { getNormalizedContainerOptions } from "../containers/config";
1414
import { getBindings, provisionBindings } from "../deployment-bundle/bindings";
@@ -797,12 +797,14 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
797797
if (props.dryRun) {
798798
if (normalisedContainerConfig.length) {
799799
for (const container of normalisedContainerConfig) {
800-
await maybeBuildContainer(
801-
container,
802-
workerTag ?? "worker-tag",
803-
props.dryRun,
804-
dockerPath
805-
);
800+
if ("dockerfile" in container) {
801+
await buildContainer(
802+
container,
803+
workerTag ?? "worker-tag",
804+
props.dryRun,
805+
dockerPath
806+
);
807+
}
806808
}
807809
}
808810

0 commit comments

Comments
 (0)