Skip to content
Open
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
8 changes: 8 additions & 0 deletions .changeset/angry-bears-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@cloudflare/workers-utils": minor
"wrangler": minor
---

Add production_enabled and previews_enabled support for custom domain routes

Custom domain routes can now include optional production_enabled and previews_enabled boolean fields to control whether a domain serves production and/or preview traffic. When omitted, the API defaults apply (production enabled, previews disabled).
7 changes: 6 additions & 1 deletion packages/workers-utils/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ export type ZoneNameRoute = {
zone_name: string;
custom_domain?: boolean;
};
export type CustomDomainRoute = { pattern: string; custom_domain: boolean };
export type CustomDomainRoute = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is putting this in the custom domain route type set in stone? Long term I think something like the following would be clearer and would get away from locking us in to these config shapes just because we've previously had them:

routes: [{
  type: "preview",
  pattern: "*.example.com"
},{
  type: "custom_domain",
  pattern: "example.com"
}]

In the meantime, what about:

routes: [{
  preview: true,
  pattern: "*.example.com"
},{
  custom_domain: true,
  pattern: "example.com"
}]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previews are only supported for custom domains. I don't really see us adding them to regular routes in the future, since routes can already contain wildcards and there isn't a clear domain hierarchy to differentiate previews.

It's a little confusing to me to have separate "types" for custom_domain and previews, when they are actually the exact same thing at the API level. @1000hz has proposed a simplified domains API that would lend itself to this config shape:

{
  "domains": ["app.example.com", "my-app.cloudflare.app"],
  "preview_domains": ["previews.example.com", "my-app.cloudflare.app"]
{

which feels a lot simpler since users don't need to be aware of the different underlying route types. Regardless, do we want to make this config shape change now?

As-is adopting previews for existing users is as simple as adding "previews_enabled": true to their domain config. The wildcard syntax would be a Wrangler-only construct and isn't something users would be familiar with from any other touchpoint (dash, API, Terraform, etc.). The only place you would see it is in the actual DNS record that is created, which we're trying to abstract over here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@penalosa I definitely think we should change the domains config when we release the new config shape, but could we wait until then?

And yes I think just an array of domains like what @1000hz suggests is ideal

pattern: string;
custom_domain: boolean;
enabled?: boolean;
previews_enabled?: boolean;
};
export type Route =
| SimpleRoute
| ZoneIdRoute
Expand Down
43 changes: 33 additions & 10 deletions packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,22 +964,45 @@ function isValidRouteValue(item: unknown): boolean {
return false;
}

const otherKeys = Object.keys(item).length - 1; // minus one to subtract "pattern"

const hasZoneId =
hasProperty(item, "zone_id") && typeof item.zone_id === "string";
const hasZoneName =
hasProperty(item, "zone_name") && typeof item.zone_name === "string";
const hasCustomDomainFlag =
hasProperty(item, "custom_domain") &&
typeof item.custom_domain === "boolean";
const hasEnabled =
hasProperty(item, "enabled") && typeof item.enabled === "boolean";
const hasPreviewsEnabled =
hasProperty(item, "previews_enabled") &&
typeof item.previews_enabled === "boolean";

const recognizedKeys = [
hasZoneId,
hasZoneName,
hasCustomDomainFlag,
hasEnabled,
hasPreviewsEnabled,
].filter(Boolean).length;
const otherKeys = Object.keys(item).length - 1; // minus one to subtract "pattern"

if (otherKeys === 2 && hasCustomDomainFlag && (hasZoneId || hasZoneName)) {
return true;
} else if (
otherKeys === 1 &&
(hasZoneId || hasZoneName || hasCustomDomainFlag)
) {
// All keys must be recognized
if (recognizedKeys !== otherKeys) {
return false;
}

// zone_id and zone_name are mutually exclusive
if (hasZoneId && hasZoneName) {
return false;
}

// enabled and previews_enabled are only valid on custom domain routes
if ((hasEnabled || hasPreviewsEnabled) && !hasCustomDomainFlag) {
return false;
}

// Must have at least one of: zone_id, zone_name, or custom_domain
if (hasZoneId || hasZoneName || hasCustomDomainFlag) {
return true;
}
}
Expand Down Expand Up @@ -1030,7 +1053,7 @@ function mutateEmptyStringRouteValue(
const isRoute: ValidatorFn = (diagnostics, field, value) => {
if (value !== undefined && !isValidRouteValue(value)) {
diagnostics.errors.push(
`Expected "${field}" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got ${JSON.stringify(
`Expected "${field}" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but got ${JSON.stringify(
value
)}.`
);
Expand Down Expand Up @@ -1060,7 +1083,7 @@ const isRouteArray: ValidatorFn = (diagnostics, field, value) => {
}
if (invalidRoutes.length > 0) {
diagnostics.errors.push(
`Expected "${field}" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name }, but these weren't valid: ${JSON.stringify(
`Expected "${field}" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but these weren't valid: ${JSON.stringify(
invalidRoutes,
null,
2
Expand Down
3 changes: 3 additions & 0 deletions packages/workers-utils/src/construct-wrangler-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ function convertWorkerToWranglerConfig(config: APIWorkerConfig): RawConfig {
pattern: c.hostname as string,
zone_name: c.zone_name,
custom_domain: true,
enabled: (c as typeof c & { enabled: boolean }).enabled,
previews_enabled: (c as typeof c & { previews_enabled: boolean })
.previews_enabled,
})),
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1164,9 +1164,9 @@ describe("normalizeAndValidateConfig()", () => {
expect(diagnostics.hasWarnings()).toBe(false);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got 888.
- Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but got 888.
- Expected "account_id" to be of type string but got 222.
- Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name }, but these weren't valid: [
- Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but these weren't valid: [
666,
777,
{
Expand Down Expand Up @@ -5913,9 +5913,9 @@ describe("normalizeAndValidateConfig()", () => {
"Processing wrangler configuration:

- "env.ENV1" environment configuration
- Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got 888.
- Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but got 888.
- Expected "account_id" to be of type string but got 222.
- Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name }, but these weren't valid: [
- Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but these weren't valid: [
666,
777
].
Expand Down
8 changes: 7 additions & 1 deletion packages/wrangler/src/__tests__/deploy/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ export function mockCustomDomainsChangesetRequest({
environment: params.envName,
zone_name: "",
zone_id: "",
enabled: true,
previews_enabled: false,
};
}),
removed: [],
Expand Down Expand Up @@ -247,7 +249,11 @@ export function mockPublishCustomDomainsRequest({
override_existing_dns_record: boolean;
};
domains: Array<
{ hostname: string } & ({ zone_id?: string } | { zone_name?: string })
{
hostname: string;
enabled?: boolean;
previews_enabled?: boolean;
} & ({ zone_id?: string } | { zone_name?: string })
>;
env?: string | undefined;
useServiceEnvironments?: boolean | undefined;
Expand Down
54 changes: 54 additions & 0 deletions packages/wrangler/src/__tests__/deploy/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,44 @@ describe("deploy", () => {
expect(std.out).toContain("api.example.com (custom domain)");
});

it("should pass enabled and previews_enabled to the custom domains API", async ({
expect,
}) => {
writeWranglerConfig({
routes: [
{
pattern: "api.example.com",
custom_domain: true,
enabled: true,
previews_enabled: true,
},
],
});
writeWorkerSource();
mockUpdateWorkerSubdomain({ enabled: false });
mockUploadWorkerRequest({ expectedType: "esm" });
mockGetZones("api.example.com", [{ id: "api-example-com-id" }]);
mockGetZoneWorkerRoutes("api-example-com-id", []);
mockCustomDomainsChangesetRequest({});
mockPublishCustomDomainsRequest({
publishFlags: {
override_scope: true,
override_existing_origin: false,
override_existing_dns_record: false,
},
domains: [
{
hostname: "api.example.com",
enabled: true,
previews_enabled: true,
},
],
});
await runWrangler("deploy ./index");
expect(std.out).toContain("api.example.com (custom domain)");
expect(std.out).toContain("[enabled, previews: enabled]");
});

it("should confirm override if custom domain deploy would override an existing domain", async ({
expect,
}) => {
Expand All @@ -677,6 +715,8 @@ describe("deploy", () => {
hostname: "api.example.com",
service: "test-name",
environment: "",
enabled: true,
previews_enabled: false,
},
],
});
Expand All @@ -687,6 +727,8 @@ describe("deploy", () => {
hostname: "api.example.com",
service: "other-script",
environment: "",
enabled: true,
previews_enabled: false,
});
mockPublishCustomDomainsRequest({
publishFlags: {
Expand Down Expand Up @@ -729,6 +771,8 @@ Update them to point to this script instead?`,
hostname: "api.example.com",
service: "test-name",
environment: "",
enabled: true,
previews_enabled: false,
},
],
});
Expand Down Expand Up @@ -773,6 +817,8 @@ Update them to point to this script instead?`,
hostname: "api.example.com",
service: "test-name",
environment: "",
enabled: true,
previews_enabled: false,
},
],
dnsRecordConflicts: [
Expand All @@ -783,6 +829,8 @@ Update them to point to this script instead?`,
hostname: "api.example.com",
service: "test-name",
environment: "",
enabled: true,
previews_enabled: false,
},
],
});
Expand All @@ -793,6 +841,8 @@ Update them to point to this script instead?`,
hostname: "api.example.com",
service: "other-script",
environment: "",
enabled: true,
previews_enabled: false,
});
mockPublishCustomDomainsRequest({
publishFlags: {
Expand Down Expand Up @@ -874,6 +924,8 @@ Update them to point to this script instead?`,
hostname: "api.example.com",
service: "test-name",
environment: "",
enabled: true,
previews_enabled: false,
},
],
});
Expand All @@ -884,6 +936,8 @@ Update them to point to this script instead?`,
hostname: "api.example.com",
service: "other-script",
environment: "",
enabled: true,
previews_enabled: false,
});
mockConfirm({
text: `Custom Domains already exist for these domains:
Expand Down
22 changes: 22 additions & 0 deletions packages/wrangler/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ export type CustomDomain = {
hostname: string;
service: string;
environment: string;
enabled: boolean;
previews_enabled: boolean;
};
type UpdatedCustomDomain = CustomDomain & { modified: boolean };
type ConflictingCustomDomain = CustomDomain & {
Expand Down Expand Up @@ -248,6 +250,21 @@ export function renderRoute(route: Route): string {
} else if ("zone_name" in route) {
result += ` (zone name: ${route.zone_name})`;
}

if (isCustomDomain) {
const flags: string[] = [];
if ("enabled" in route && route.enabled !== undefined) {
flags.push(route.enabled ? "enabled" : "disabled");
}
if ("previews_enabled" in route && route.previews_enabled !== undefined) {
flags.push(
route.previews_enabled ? "previews: enabled" : "previews: disabled"
);
}
if (flags.length > 0) {
result += ` [${flags.join(", ")}]`;
}
}
}
return result;
}
Expand Down Expand Up @@ -286,6 +303,11 @@ export async function publishCustomDomains(
hostname: domainRoute.pattern,
zone_id: "zone_id" in domainRoute ? domainRoute.zone_id : undefined,
zone_name: "zone_name" in domainRoute ? domainRoute.zone_name : undefined,
enabled: "enabled" in domainRoute ? domainRoute.enabled : undefined,
previews_enabled:
"previews_enabled" in domainRoute
? domainRoute.previews_enabled
: undefined,
};
});

Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/utils/download-worker-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type CustomDomainsRes = {
service: string;
environment: string;
cert_id: string;
enabled: boolean;
previews_enabled: boolean;
}[];

type WorkerSubdomainRes = {
Expand Down
Loading