Skip to content

Commit ae047ee

Browse files
Adds option to skip container rollout on deploy (cloudflare#12656)
Co-authored-by: emily-shen <eshen@cloudflare.com>
1 parent 1d8924f commit ae047ee

7 files changed

Lines changed: 144 additions & 16 deletions

File tree

‎.changeset/dry-sloths-roll.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@cloudflare/containers-shared": minor
3+
"wrangler": minor
4+
---
5+
6+
Add `--containers-rollout=none`
7+
8+
This allows you to skip deploying a container. This is useful if you know that your container is not going to be updated or you don't have Docker locally, but still want to make changes to your Worker.

‎packages/containers-shared/src/utils.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,17 @@ export const verifyDockerInstalled = async (
110110
) => {
111111
const dockerIsRunning = await isDockerRunning(dockerPath);
112112
if (!dockerIsRunning) {
113-
throw new UserError(
113+
let message =
114114
`The Docker CLI could not be launched. Please ensure that the Docker CLI is installed and the daemon is running.\n` +
115-
`Other container tooling that is compatible with the Docker CLI and engine may work, but is not yet guaranteed to do so. You can specify an executable with the environment variable WRANGLER_DOCKER_BIN and a socket with DOCKER_HOST.` +
116-
`${isDev ? "\nTo suppress this error if you do not intend on triggering any container instances, set dev.enable_containers to false in your Wrangler config or passing in --enable-containers=false." : ""}`,
117-
{ telemetryMessage: false }
118-
);
115+
`Other container tooling that is compatible with the Docker CLI and engine may work, but is not yet guaranteed to do so. You can specify an executable with the environment variable WRANGLER_DOCKER_BIN and a socket with DOCKER_HOST.`;
116+
if (isDev) {
117+
message +=
118+
"\nTo suppress this error if you do not intend on triggering any container instances, set dev.enable_containers to false in your Wrangler config or passing in --enable-containers=false.";
119+
} else {
120+
message +=
121+
"\nIf you cannot run Docker locally, you can still deploy your Worker by passing --containers-rollout=none. This will not deploy or update your Container.";
122+
}
123+
throw new UserError(message, { telemetryMessage: false });
119124
}
120125
};
121126

‎packages/wrangler/src/__tests__/containers/config.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,40 @@ describe("getNormalizedContainerOptions", () => {
720720
expect(result[0].rollout_step_percentage).toBe(100);
721721
});
722722

723+
it("should set rollout_kind to none when containersRollout is none", async ({
724+
expect,
725+
}) => {
726+
const config: Config = {
727+
name: "test-worker",
728+
configPath: "/test/wrangler.toml",
729+
userConfigPath: "/test/wrangler.toml",
730+
topLevelName: "test-worker",
731+
containers: [
732+
{
733+
class_name: "TestContainer",
734+
image: `${getCloudflareContainerRegistry()}/test:latest`,
735+
name: "test-container",
736+
max_instances: 10,
737+
rollout_kind: "full_auto",
738+
},
739+
],
740+
durable_objects: {
741+
bindings: [
742+
{
743+
name: "TEST_DO",
744+
class_name: "TestContainer",
745+
},
746+
],
747+
},
748+
} as Partial<Config> as Config;
749+
750+
const result = await getNormalizedContainerOptions(config, {
751+
containersRollout: "none",
752+
});
753+
expect(result).toHaveLength(1);
754+
expect(result[0].rollout_kind).toBe("none");
755+
});
756+
723757
describe("image validation and resolution", async () => {
724758
it("should allow any image registry", async ({ expect }) => {
725759
const config: Config = {

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

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ describe("wrangler deploy with containers", () => {
7373
).rejects.toThrowErrorMatchingInlineSnapshot(
7474
`
7575
[Error: The Docker CLI could not be launched. Please ensure that the Docker CLI is installed and the daemon is running.
76-
Other container tooling that is compatible with the Docker CLI and engine may work, but is not yet guaranteed to do so. You can specify an executable with the environment variable WRANGLER_DOCKER_BIN and a socket with DOCKER_HOST.]
76+
Other container tooling that is compatible with the Docker CLI and engine may work, but is not yet guaranteed to do so. You can specify an executable with the environment variable WRANGLER_DOCKER_BIN and a socket with DOCKER_HOST.
77+
If you cannot run Docker locally, you can still deploy your Worker by passing --containers-rollout=none. This will not deploy or update your Container.]
7778
`
7879
);
7980
});
@@ -1132,6 +1133,36 @@ describe("wrangler deploy with containers", () => {
11321133
});
11331134
});
11341135

1136+
it("should skip Docker check and container deploy when --containers-rollout=none", async ({
1137+
expect,
1138+
}) => {
1139+
vi.stubEnv("WRANGLER_DOCKER_BIN", "/usr/bin/bad-docker-path");
1140+
writeWranglerConfig({
1141+
...DEFAULT_DURABLE_OBJECTS,
1142+
containers: [DEFAULT_CONTAINER_FROM_DOCKERFILE],
1143+
});
1144+
1145+
fs.writeFileSync("./Dockerfile", "FROM scratch");
1146+
1147+
mockGetVersion("Galaxy-Class");
1148+
mockGetApplications([]);
1149+
mockCreateApplication(expect);
1150+
1151+
fs.writeFileSync(
1152+
"index.js",
1153+
`export class ExampleDurableObject {}; export default{};`
1154+
);
1155+
1156+
// Without --containers-rollout=none, this would fail with a Docker error.
1157+
// With the flag, it should skip Docker verification entirely and proceed
1158+
// to deploy the Worker (the deploy itself may fail for unrelated mock reasons,
1159+
// but the key assertion is that no Docker error is thrown).
1160+
1161+
await expect(
1162+
runWrangler("deploy index.js --containers-rollout=none")
1163+
).resolves.not.toThrow();
1164+
});
1165+
11351166
describe("observability config resolution", () => {
11361167
const sharedGetApplicationResult = {
11371168
id: "abc",
@@ -2392,7 +2423,9 @@ describe("wrangler deploy with containers dry run", () => {
23922423
vi.unstubAllEnvs();
23932424
});
23942425

2395-
it("builds the image without pushing", async ({ expect }) => {
2426+
it("builds the image without pushing when given a dockerfile", async ({
2427+
expect,
2428+
}) => {
23962429
// Reduced mock chain for dry run (no delete, push)
23972430
vi.mocked(spawn)
23982431
.mockImplementationOnce(mockDockerInfo(expect))
@@ -2435,7 +2468,49 @@ describe("wrangler deploy with containers dry run", () => {
24352468
expect(cliStd.stdout).toMatchInlineSnapshot(`""`);
24362469
});
24372470

2438-
it("builds the image without pushing", async ({ expect }) => {
2471+
it("does not build when --containers-rollout=none", async ({ expect }) => {
2472+
// Reduced mock chain for dry run (no delete, push)
2473+
vi.mocked(spawn)
2474+
.mockImplementationOnce(mockDockerInfo(expect))
2475+
.mockImplementationOnce(
2476+
mockDockerBuild(
2477+
expect,
2478+
"my-container",
2479+
"worker",
2480+
"FROM scratch",
2481+
process.cwd()
2482+
)
2483+
);
2484+
vi.stubEnv("WRANGLER_DOCKER_BIN", "/usr/bin/docker");
2485+
fs.writeFileSync("./Dockerfile", "FROM scratch");
2486+
fs.writeFileSync(
2487+
"index.js",
2488+
`export class ExampleDurableObject {}; export default{};`
2489+
);
2490+
writeWranglerConfig({
2491+
...DEFAULT_DURABLE_OBJECTS,
2492+
containers: [DEFAULT_CONTAINER_FROM_DOCKERFILE],
2493+
});
2494+
2495+
await runWrangler("deploy --dry-run --containers-rollout=none index.js");
2496+
expect(std.out).toMatchInlineSnapshot(`
2497+
"
2498+
⛅️ wrangler x.x.x
2499+
──────────────────
2500+
Total Upload: xx KiB / gzip: xx KiB
2501+
Your Worker has access to the following bindings:
2502+
Binding Resource
2503+
env.EXAMPLE_DO_BINDING (ExampleDurableObject) Durable Object
2504+
2505+
The following containers are available:
2506+
- my-container (<cwd>/Dockerfile)
2507+
2508+
--dry-run: exiting now."
2509+
`);
2510+
expect(cliStd.stdout).toMatchInlineSnapshot(`""`);
2511+
});
2512+
2513+
it("does not push when given a registry link", async ({ expect }) => {
24392514
// No docker mocks at all
24402515

24412516
fs.writeFileSync(

‎packages/wrangler/src/containers/config.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const getNormalizedContainerOptions = async (
4949
config: Config,
5050
args: {
5151
/** set by args.containersRollout */
52-
containersRollout?: "gradual" | "immediate";
52+
containersRollout?: "gradual" | "immediate" | "none";
5353
dryRun?: boolean;
5454
}
5555
): Promise<ContainerNormalizedConfig[]> => {
@@ -126,7 +126,10 @@ export const getNormalizedContainerOptions = async (
126126
? 100
127127
: (container.rollout_step_percentage ??
128128
rolloutStepPercentageFallback),
129-
rollout_kind: container.rollout_kind ?? "full_auto",
129+
rollout_kind:
130+
args?.containersRollout === "none"
131+
? "none"
132+
: (container.rollout_kind ?? "full_auto"),
130133
rollout_active_grace_period: container.rollout_active_grace_period ?? 0,
131134
observability: {
132135
logs_enabled:

‎packages/wrangler/src/deploy/deploy.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ type Props = {
136136
dispatchNamespace: string | undefined;
137137
experimentalAutoCreate: boolean;
138138
metafile: string | boolean | undefined;
139-
containersRollout: "immediate" | "gradual" | undefined;
139+
containersRollout: "immediate" | "gradual" | "none" | undefined;
140140
strict: boolean | undefined;
141141
tag: string | undefined;
142142
message: string | undefined;
@@ -968,7 +968,10 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
968968
// and we have containers so that we don't get into a
969969
// disjointed state where the worker updates but the container
970970
// fails.
971-
if (normalisedContainerConfig.length) {
971+
if (
972+
normalisedContainerConfig.length &&
973+
props.containersRollout !== "none"
974+
) {
972975
// if you have a registry url specified, you don't need docker
973976
const hasDockerfiles = normalisedContainerConfig.some(
974977
(container) => "dockerfile" in container
@@ -981,7 +984,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
981984
if (props.dryRun) {
982985
if (normalisedContainerConfig.length) {
983986
for (const container of normalisedContainerConfig) {
984-
if ("dockerfile" in container) {
987+
if ("dockerfile" in container && props.containersRollout !== "none") {
985988
await buildContainer(
986989
container,
987990
workerTag ?? "worker-tag",
@@ -1289,7 +1292,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
12891292

12901293
logger.log("Uploaded", workerName, formatTime(uploadMs));
12911294

1292-
if (normalisedContainerConfig.length) {
1295+
if (normalisedContainerConfig.length && props.containersRollout !== "none") {
12931296
assert(versionId && accountId);
12941297
await deployContainers(config, normalisedContainerConfig, {
12951298
versionId,

‎packages/wrangler/src/deploy/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export const deployCommand = createCommand({
8585
},
8686
"containers-rollout": {
8787
describe:
88-
"Rollout strategy for Containers changes. If set to immediate, it will override `rollout_percentage_steps` if configured and roll out to 100% of instances in one step. ",
89-
choices: ["immediate", "gradual"] as const,
88+
"Rollout strategy for Containers changes. If set to immediate, it will override `rollout_percentage_steps` if configured and roll out to 100% of instances in one step. If set to none, the Worker will be deployed without building or updating any Containers.",
89+
choices: ["immediate", "gradual", "none"] as const,
9090
},
9191
strict: {
9292
describe:

0 commit comments

Comments
 (0)