Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/cyan-planets-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"wrangler": minor
---

Add container support to worker previews

Worker previews now support containers via a new `previews.containers` configuration block. Container configuration is non-inheritable: declare containers explicitly in the `previews` block to enable them for previews, mirroring how `previews.durable_objects` works today. Preview container application names are auto-generated by wrangler in the form `{worker_name}_{preview_slug}_{class_name}` and are not user-configurable — the config validator rejects entries that set a `name` field. Container applications bound to Durable Object classes implemented by another Worker (via `script_name`) are intentionally skipped, since the implementing Worker owns its own container application.

Container applications are created on `wrangler preview` and removed on `wrangler preview delete`. Cleanup matches applications by their auto-generated name prefix (`{worker_name}_{preview_slug}_`), so deletions are scoped to the specific preview being torn down. Failures on individual application deletes are logged as warnings but do not block the preview deletion itself.
53 changes: 51 additions & 2 deletions packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2081,7 +2081,7 @@ function normalizeAndValidateEnvironment(
topLevelEnv,
rawEnv,
"previews",
validatePreviewsConfig(envName),
validatePreviewsConfig(envName, rawEnv.name, configPath),
undefined
),
};
Expand Down Expand Up @@ -3159,6 +3159,40 @@ const validateBindingArray =
return isValid;
};

/**
* Validate `previews.containers`. Mirrors `validateContainerApp` but rejects
* any user-provided `name` field — preview container application names are
* auto-generated by wrangler from the worker name, preview slug, and
* class name so they can be reliably created and cleaned up alongside the
* preview itself.
*/
function validatePreviewsContainers(
envName: string,
topLevelName: string | undefined,
configPath: string | undefined
): ValidatorFn {
const innerValidator = validateContainerApp(
envName,
topLevelName,
configPath
);
return (diagnostics, field, value, config) => {
if (
Array.isArray(value) &&
value.some(
(entry) =>
entry && typeof entry === "object" && entry.name !== undefined
)
) {
diagnostics.errors.push(
`"name" is not allowed on "${field}" entries; preview container application names are auto-generated by wrangler from the worker name, preview slug, and class name.`
);
return false;
}
return innerValidator(diagnostics, field, value, config);
};
}

function validateContainerApp(
envName: string,
topLevelName: string | undefined,
Expand Down Expand Up @@ -5183,7 +5217,11 @@ function normalizeAndValidateLimits(
}

const validatePreviewsConfig =
(envName: string): ValidatorFn =>
(
envName: string,
topLevelName: string | undefined,
configPath: string | undefined
): ValidatorFn =>
(diagnostics, field, value) => {
if (value === undefined) {
return true;
Expand Down Expand Up @@ -5235,6 +5273,7 @@ const validatePreviewsConfig =
"ratelimits",
"vpc_services",
"version_metadata",
"containers",
"logpush",
"observability",
"limits",
Expand Down Expand Up @@ -5514,6 +5553,16 @@ const validatePreviewsConfig =
) && isValid;
}

if (previews.containers !== undefined) {
isValid =
validatePreviewsContainers(envName, topLevelName, configPath)(
diagnostics,
`${field}.containers`,
previews.containers,
undefined
) && isValid;
}

isValid =
isBoolean(diagnostics, `${field}.logpush`, previews.logpush, undefined) &&
isValid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9973,6 +9973,81 @@ describe("normalizeAndValidateConfig()", () => {
'The field "previews.browser" should be an object'
);
});

it("should accept previews.containers without a name", ({ expect }) => {
const rawConfig = {
name: "test-worker",
previews: {
containers: [
{
class_name: "MyContainer",
image: "registry.cloudflare.com/test:latest",
},
],
},
} as unknown as RawConfig;

const { diagnostics } = normalizeAndValidateConfig(
rawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(false);
});

it("should reject previews.containers entries that set a name", ({
expect,
}) => {
const rawConfig = {
name: "test-worker",
previews: {
containers: [
{
class_name: "MyContainer",
image: "registry.cloudflare.com/test:latest",
name: "custom-name",
},
],
},
} as unknown as RawConfig;

const { diagnostics } = normalizeAndValidateConfig(
rawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderErrors()).toContain(
'"name" is not allowed on "previews.containers" entries'
);
});

it("should reject previews.containers entries missing image", ({
expect,
}) => {
const rawConfig = {
name: "test-worker",
previews: {
containers: [{ class_name: "MyContainer" }],
},
} as unknown as RawConfig;

const { diagnostics } = normalizeAndValidateConfig(
rawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderErrors()).toContain(
'"containers.image" field must be defined'
);
});
});
});
});
Expand Down
Loading
Loading