Skip to content

Commit 69b4303

Browse files
romeupalospenalosa
andauthored
Use zone_name for deployment of custom hostnames (#4232)
* Use `zone_name` for deployment of custom hostnames * Simplify getting zone id from `zone_name` based on PR comments * Add some more tests * Add mock auth to tests --------- Co-authored-by: Samuel Macleod <[email protected]>
1 parent 109b159 commit 69b4303

File tree

5 files changed

+239
-24
lines changed

5 files changed

+239
-24
lines changed

.changeset/rich-schools-fry.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
fix: use `zone_name` to determine a zone when the pattern is a custom hostname
6+
7+
In Cloudflare for SaaS, custom hostnames of third party domain owners can be used in Cloudflare.
8+
Workers are allowed to intercept these requests based on the routes configuration.
9+
Before this change, the same logic used by `wrangler dev` was used in `wrangler deploy`, which caused wrangler to fail with:
10+
11+
[ERROR] Could not find zone for [partner-saas-domain.com]

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

+114
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,84 @@ describe("deploy", () => {
713713
`);
714714
});
715715

716+
it("should deploy to a route with a SaaS domain", async () => {
717+
writeWranglerToml({
718+
workers_dev: false,
719+
routes: [
720+
{
721+
pattern: "partner.com/*",
722+
zone_name: "owned-zone.com",
723+
},
724+
],
725+
});
726+
writeWorkerSource();
727+
mockUpdateWorkerRequest({ enabled: false });
728+
mockUploadWorkerRequest({ available_on_subdomain: false });
729+
mockGetZones("owned-zone.com", [{ id: "owned-zone-id-1" }]);
730+
mockGetWorkerRoutes("owned-zone-id-1");
731+
mockPublishRoutesRequest({
732+
routes: [
733+
{
734+
pattern: "partner.com/*",
735+
zone_name: "owned-zone.com",
736+
},
737+
],
738+
});
739+
await runWrangler("deploy ./index");
740+
expect(std).toMatchInlineSnapshot(`
741+
Object {
742+
"debug": "",
743+
"err": "",
744+
"info": "",
745+
"out": "Total Upload: xx KiB / gzip: xx KiB
746+
Uploaded test-name (TIMINGS)
747+
Published test-name (TIMINGS)
748+
partner.com/* (zone name: owned-zone.com)
749+
Current Deployment ID: Galaxy-Class",
750+
"warn": "",
751+
}
752+
`);
753+
});
754+
755+
it("should deploy to a route with a SaaS subdomain", async () => {
756+
writeWranglerToml({
757+
workers_dev: false,
758+
routes: [
759+
{
760+
pattern: "subdomain.partner.com/*",
761+
zone_name: "owned-zone.com",
762+
},
763+
],
764+
});
765+
writeWorkerSource();
766+
mockUpdateWorkerRequest({ enabled: false });
767+
mockUploadWorkerRequest({ available_on_subdomain: false });
768+
mockGetZones("owned-zone.com", [{ id: "owned-zone-id-1" }]);
769+
mockGetWorkerRoutes("owned-zone-id-1");
770+
mockPublishRoutesRequest({
771+
routes: [
772+
{
773+
pattern: "subdomain.partner.com/*",
774+
zone_name: "owned-zone.com",
775+
},
776+
],
777+
});
778+
await runWrangler("deploy ./index");
779+
expect(std).toMatchInlineSnapshot(`
780+
Object {
781+
"debug": "",
782+
"err": "",
783+
"info": "",
784+
"out": "Total Upload: xx KiB / gzip: xx KiB
785+
Uploaded test-name (TIMINGS)
786+
Published test-name (TIMINGS)
787+
subdomain.partner.com/* (zone name: owned-zone.com)
788+
Current Deployment ID: Galaxy-Class",
789+
"warn": "",
790+
}
791+
`);
792+
});
793+
716794
it("should deploy to a route with a pattern/{zone_id|zone_name} combo (service environments)", async () => {
717795
writeWranglerToml({
718796
env: {
@@ -8911,6 +8989,42 @@ function mockUnauthorizedPublishRoutesRequest({
89118989
);
89128990
}
89138991

8992+
function mockGetZones(domain: string, zones: { id: string }[] = []) {
8993+
msw.use(
8994+
rest.get("*/zones", (req, res, ctx) => {
8995+
expect([...req.url.searchParams.entries()]).toEqual([["name", domain]]);
8996+
8997+
return res(
8998+
ctx.status(200),
8999+
ctx.json({
9000+
success: true,
9001+
errors: [],
9002+
messages: [],
9003+
result: zones,
9004+
})
9005+
);
9006+
})
9007+
);
9008+
}
9009+
9010+
function mockGetWorkerRoutes(zoneId: string) {
9011+
msw.use(
9012+
rest.get("*/zones/:zoneId/workers/routes", (req, res, ctx) => {
9013+
expect(req.params.zoneId).toEqual(zoneId);
9014+
9015+
return res(
9016+
ctx.status(200),
9017+
ctx.json({
9018+
success: true,
9019+
errors: [],
9020+
messages: [],
9021+
result: [],
9022+
})
9023+
);
9024+
})
9025+
);
9026+
}
9027+
89149028
function mockPublishRoutesFallbackRequest(route: {
89159029
pattern: string;
89169030
script: string;

packages/wrangler/src/__tests__/dev.test.tsx

-16
Original file line numberDiff line numberDiff line change
@@ -377,22 +377,6 @@ describe("wrangler dev", () => {
377377
expect(std.err).toMatchInlineSnapshot(`""`);
378378
});
379379

380-
it("should fail for non-existing zones", async () => {
381-
writeWranglerToml({
382-
main: "index.js",
383-
routes: [
384-
{
385-
pattern: "https://subdomain.does-not-exist.com/*",
386-
zone_name: "exists.com",
387-
},
388-
],
389-
});
390-
await fs.promises.writeFile("index.js", `export default {};`);
391-
await expect(runWrangler("dev --remote")).rejects.toEqual(
392-
new Error("Could not find zone for subdomain.does-not-exist.com")
393-
);
394-
});
395-
396380
it("should fail for non-existing zones, when falling back from */*", async () => {
397381
writeWranglerToml({
398382
main: "index.js",

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

+103-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
1-
import { getHostFromUrl } from "../zones";
1+
import { rest } from "msw";
2+
import { getHostFromUrl, getZoneForRoute } from "../zones";
3+
import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
4+
import { msw } from "./helpers/msw";
5+
function mockGetZones(domain: string, zones: { id: string }[] = []) {
6+
msw.use(
7+
rest.get("*/zones", (req, res, ctx) => {
8+
expect([...req.url.searchParams.entries()]).toEqual([["name", domain]]);
9+
10+
return res.once(
11+
ctx.status(200),
12+
ctx.json({
13+
success: true,
14+
errors: [],
15+
messages: [],
16+
result: zones,
17+
})
18+
);
19+
})
20+
);
21+
}
222

323
describe("Zones", () => {
24+
mockAccountId();
25+
mockApiToken();
426
describe("getHostFromUrl", () => {
527
test.each`
628
pattern | host
@@ -35,4 +57,84 @@ describe("Zones", () => {
3557
expect(getHostFromUrl(pattern)).toBe(host);
3658
});
3759
});
60+
describe("getZoneForRoute", () => {
61+
test("string route", async () => {
62+
mockGetZones("example.com", [{ id: "example-id" }]);
63+
expect(await getZoneForRoute("example.com/*")).toEqual({
64+
host: "example.com",
65+
id: "example-id",
66+
});
67+
});
68+
69+
test("string route (not a zone)", async () => {
70+
mockGetZones("wrong.com", []);
71+
await expect(
72+
getZoneForRoute("wrong.com/*")
73+
).rejects.toMatchInlineSnapshot(
74+
`[Error: Could not find zone for wrong.com]`
75+
);
76+
});
77+
test("zone_id route", async () => {
78+
// example-id and other-id intentionally different to show that the API is not called
79+
// when a zone_id is provided in the route
80+
mockGetZones("example.com", [{ id: "example-id" }]);
81+
expect(
82+
await getZoneForRoute({ pattern: "example.com/*", zone_id: "other-id" })
83+
).toEqual({
84+
host: "example.com",
85+
id: "other-id",
86+
});
87+
});
88+
test("zone_id route (custom hostname)", async () => {
89+
// example-id and other-id intentionally different to show that the API is not called
90+
// when a zone_id is provided in the route
91+
mockGetZones("example.com", [{ id: "example-id" }]);
92+
expect(
93+
await getZoneForRoute({
94+
pattern: "some.third-party.com/*",
95+
zone_id: "other-id",
96+
})
97+
).toEqual({
98+
host: "some.third-party.com",
99+
id: "other-id",
100+
});
101+
});
102+
103+
test("zone_name route (apex)", async () => {
104+
mockGetZones("example.com", [{ id: "example-id" }]);
105+
expect(
106+
await getZoneForRoute({
107+
pattern: "example.com/*",
108+
zone_name: "example.com",
109+
})
110+
).toEqual({
111+
host: "example.com",
112+
id: "example-id",
113+
});
114+
});
115+
test("zone_name route (subdomain)", async () => {
116+
mockGetZones("example.com", [{ id: "example-id" }]);
117+
expect(
118+
await getZoneForRoute({
119+
pattern: "subdomain.example.com/*",
120+
zone_name: "example.com",
121+
})
122+
).toEqual({
123+
host: "subdomain.example.com",
124+
id: "example-id",
125+
});
126+
});
127+
test("zone_name route (custom hostname)", async () => {
128+
mockGetZones("example.com", [{ id: "example-id" }]);
129+
expect(
130+
await getZoneForRoute({
131+
pattern: "some.third-party.com/*",
132+
zone_name: "example.com",
133+
})
134+
).toEqual({
135+
host: "some.third-party.com",
136+
id: "example-id",
137+
});
138+
});
139+
});
38140
});

packages/wrangler/src/zones.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,24 @@ export function getHostFromRoute(route: Route): string | undefined {
4242
}
4343

4444
/**
45-
* Try to compute the a zone ID and host name for one or more routes.
45+
* Try to compute the a zone ID and host name for a route.
4646
*
4747
* When we're given a route, we do 2 things:
4848
* - We try to extract a host from it
4949
* - We try to get a zone id from the host
5050
*/
5151
export async function getZoneForRoute(route: Route): Promise<Zone | undefined> {
5252
const host = getHostFromRoute(route);
53-
const id =
54-
typeof route === "object" && "zone_id" in route
55-
? route.zone_id
56-
: host
57-
? await getZoneIdFromHost(host)
58-
: undefined;
53+
let id: string | undefined;
54+
55+
if (typeof route === "object" && "zone_id" in route) {
56+
id = route.zone_id;
57+
} else if (typeof route === "object" && "zone_name" in route) {
58+
id = await getZoneIdFromHost(route.zone_name);
59+
} else if (host) {
60+
id = await getZoneIdFromHost(host);
61+
}
62+
5963
return id && host ? { id, host } : undefined;
6064
}
6165

0 commit comments

Comments
 (0)