Skip to content

Commit 48590c0

Browse files
edmundhungemily-shen
authored andcommitted
feat(wrangler): identify draft and inherit bindings and provision draft ones (#7427)
* feat(wrangler): identify draft and inherit bindings * add provisioning ui * fixup * skip question if no existing resources * consolidate repetitive bits * add tests * add default new resource value * update changeset * revert some unnecessary renaming * fix tests * use wrangler select instead of cli inputPrompt * add e2e tests * pr feedback * test fixup * error on service environments --------- Co-authored-by: emily-shen <[email protected]>
1 parent cd042b6 commit 48590c0

File tree

13 files changed

+1183
-109
lines changed

13 files changed

+1183
-109
lines changed
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+
The `x-provision` experimental flag now identifies draft and inherit bindings by looking up the current binding settings.
6+
7+
Draft bindings can then be provisioned (connected to new or existing KV, D1, or R2 resources) during `wrangler deploy`.

packages/wrangler/e2e/helpers/normalize.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function normalizeOutput(
1212
removeWorkerPreviewUrl,
1313
removeUUID,
1414
removeBinding,
15+
removeKVId,
1516
normalizeErrorMarkers,
1617
replaceByte,
1718
stripTrailingWhitespace,
@@ -77,6 +78,10 @@ function removeBinding(str: string) {
7778
);
7879
}
7980

81+
function removeKVId(str: string) {
82+
return str.replace(/([0-9a-f]{32})/g, "00000000000000000000000000000000");
83+
}
84+
8085
/**
8186
* Remove the Wrangler version/update check header
8287
*/
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import assert from "node:assert";
2+
import dedent from "ts-dedent";
3+
import { fetch } from "undici";
4+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
5+
import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id";
6+
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
7+
import { fetchText } from "./helpers/fetch-text";
8+
import { generateResourceName } from "./helpers/generate-resource-name";
9+
import { normalizeOutput } from "./helpers/normalize";
10+
import { retry } from "./helpers/retry";
11+
12+
const TIMEOUT = 500_000;
13+
const normalize = (str: string) => {
14+
return normalizeOutput(str, {
15+
[CLOUDFLARE_ACCOUNT_ID]: "CLOUDFLARE_ACCOUNT_ID",
16+
});
17+
};
18+
const workerName = generateResourceName();
19+
20+
describe("provisioning", { timeout: TIMEOUT }, () => {
21+
let deployedUrl: string;
22+
let kvId: string;
23+
let d1Id: string;
24+
const helper = new WranglerE2ETestHelper();
25+
26+
it("can run dev without resource ids", async () => {
27+
const worker = helper.runLongLived("wrangler dev --x-provision");
28+
29+
const { url } = await worker.waitForReady();
30+
await fetch(url);
31+
32+
const text = await fetchText(url);
33+
34+
expect(text).toMatchInlineSnapshot(`"Hello World!"`);
35+
});
36+
37+
beforeAll(async () => {
38+
await helper.seed({
39+
"wrangler.toml": dedent`
40+
name = "${workerName}"
41+
main = "src/index.ts"
42+
compatibility_date = "2023-01-01"
43+
44+
[[kv_namespaces]]
45+
binding = "KV"
46+
47+
[[r2_buckets]]
48+
binding = "R2"
49+
50+
[[d1_databases]]
51+
binding = "D1"
52+
`,
53+
"src/index.ts": dedent`
54+
export default {
55+
fetch(request) {
56+
return new Response("Hello World!")
57+
}
58+
}`,
59+
"package.json": dedent`
60+
{
61+
"name": "${workerName}",
62+
"version": "0.0.0",
63+
"private": true
64+
}
65+
`,
66+
});
67+
});
68+
69+
it("can provision resources and deploy worker", async () => {
70+
const worker = helper.runLongLived(
71+
`wrangler deploy --x-provision --x-auto-create`
72+
);
73+
await worker.exitCode;
74+
const output = await worker.output;
75+
expect(normalize(output)).toMatchInlineSnapshot(`
76+
"Total Upload: xx KiB / gzip: xx KiB
77+
The following bindings need to be provisioned:
78+
- KV Namespaces:
79+
- KV
80+
- D1 Databases:
81+
- D1
82+
- R2 Buckets:
83+
- R2
84+
Provisioning KV (KV Namespace)...
85+
🌀 Creating new KV Namespace "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-kv"...
86+
✨ KV provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-kv
87+
--------------------------------------
88+
Provisioning D1 (D1 Database)...
89+
🌀 Creating new D1 Database "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-d1"...
90+
✨ D1 provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-d1
91+
--------------------------------------
92+
Provisioning R2 (R2 Bucket)...
93+
🌀 Creating new R2 Bucket "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2"...
94+
✨ R2 provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2
95+
--------------------------------------
96+
🎉 All resources provisioned, continuing with deployment...
97+
Your worker has access to the following bindings:
98+
- KV Namespaces:
99+
- KV: 00000000000000000000000000000000
100+
- D1 Databases:
101+
- D1: 00000000-0000-0000-0000-000000000000
102+
- R2 Buckets:
103+
- R2: tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2
104+
Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS)
105+
Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS)
106+
https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev
107+
Current Version ID: 00000000-0000-0000-0000-000000000000"
108+
`);
109+
const urlMatch = output.match(
110+
/(?<url>https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/
111+
);
112+
assert(urlMatch?.groups);
113+
deployedUrl = urlMatch.groups.url;
114+
115+
const kvMatch = output.match(/- KV: (?<kv>[0-9a-f]{32})/);
116+
assert(kvMatch?.groups);
117+
kvId = kvMatch.groups.kv;
118+
119+
const d1Match = output.match(/- D1: (?<d1>\w{8}-\w{4}-\w{4}-\w{4}-\w{12})/);
120+
assert(d1Match?.groups);
121+
d1Id = d1Match.groups.d1;
122+
123+
const { text } = await retry(
124+
(s) => s.status !== 200,
125+
async () => {
126+
const r = await fetch(deployedUrl);
127+
return { text: await r.text(), status: r.status };
128+
}
129+
);
130+
expect(text).toMatchInlineSnapshot('"Hello World!"');
131+
});
132+
133+
it("can inherit bindings on re-deploy and won't re-provision", async () => {
134+
const worker = helper.runLongLived(`wrangler deploy --x-provision`);
135+
await worker.exitCode;
136+
const output = await worker.output;
137+
expect(normalize(output)).toMatchInlineSnapshot(`
138+
"Total Upload: xx KiB / gzip: xx KiB
139+
Your worker has access to the following bindings:
140+
- KV Namespaces:
141+
- KV
142+
- D1 Databases:
143+
- D1
144+
- R2 Buckets:
145+
- R2
146+
Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS)
147+
Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS)
148+
https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev
149+
Current Version ID: 00000000-0000-0000-0000-000000000000"
150+
`);
151+
152+
const { text } = await retry(
153+
(s) => s.status !== 200,
154+
async () => {
155+
const r = await fetch(deployedUrl);
156+
return { text: await r.text(), status: r.status };
157+
}
158+
);
159+
expect(text).toMatchInlineSnapshot('"Hello World!"');
160+
});
161+
162+
afterAll(async () => {
163+
// we need to add d1 back into the config because otherwise wrangler will
164+
// call the api for all 5000 or so db's the e2e test account has
165+
// :(
166+
await helper.seed({
167+
"wrangler.toml": dedent`
168+
name = "${workerName}"
169+
main = "src/index.ts"
170+
compatibility_date = "2023-01-01"
171+
172+
[[d1_databases]]
173+
binding = "D1"
174+
database_name = "${workerName}-d1"
175+
database_id = "${d1Id}"
176+
`,
177+
});
178+
let output = await helper.run(`wrangler r2 bucket delete ${workerName}-r2`);
179+
expect(output.stdout).toContain(`Deleted bucket`);
180+
output = await helper.run(`wrangler d1 delete ${workerName}-d1 -y`);
181+
expect(output.stdout).toContain(`Deleted '${workerName}-d1' successfully.`);
182+
output = await helper.run(`wrangler delete`);
183+
expect(output.stdout).toContain("Successfully deleted");
184+
const status = await retry(
185+
(s) => s === 200 || s === 500,
186+
() => fetch(deployedUrl).then((r) => r.status)
187+
);
188+
expect(status).toBe(404);
189+
190+
output = await helper.run(
191+
`wrangler kv namespace delete --namespace-id ${kvId}`
192+
);
193+
expect(output.stdout).toContain(`Deleted KV namespace`);
194+
}, TIMEOUT);
195+
});

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

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import { clearDialogs, mockConfirm } from "./helpers/mock-dialogs";
2222
import { mockGetZoneFromHostRequest } from "./helpers/mock-get-zone-from-host";
2323
import { useMockIsTTY } from "./helpers/mock-istty";
2424
import { mockCollectKnownRoutesRequest } from "./helpers/mock-known-routes";
25-
import { mockKeyListRequest } from "./helpers/mock-kv";
25+
import {
26+
mockKeyListRequest,
27+
mockListKVNamespacesRequest,
28+
} from "./helpers/mock-kv";
2629
import {
2730
mockExchangeRefreshTokenForAccessToken,
2831
mockGetMemberships,
@@ -52,7 +55,6 @@ import { writeWranglerConfig } from "./helpers/write-wrangler-config";
5255
import type { AssetManifest } from "../assets";
5356
import type { Config } from "../config";
5457
import type { CustomDomain, CustomDomainChangeset } from "../deploy/deploy";
55-
import type { KVNamespaceInfo } from "../kv/helpers";
5658
import type {
5759
PostQueueBody,
5860
PostTypedConsumerBody,
@@ -10549,58 +10551,6 @@ export default{
1054910551
});
1055010552
});
1055110553

10552-
describe("--x-provision", () => {
10553-
it("should accept KV, R2 and D1 bindings without IDs in the configuration file", async () => {
10554-
writeWorkerSource();
10555-
writeWranglerConfig({
10556-
main: "index.js",
10557-
kv_namespaces: [{ binding: "KV_NAMESPACE" }],
10558-
r2_buckets: [{ binding: "R2_BUCKET" }],
10559-
d1_databases: [{ binding: "D1_DATABASE" }],
10560-
});
10561-
mockUploadWorkerRequest({
10562-
// We are treating them as inherited bindings temporarily to test the current implementation only
10563-
// This will be updated as we implement the actual provision logic
10564-
expectedBindings: [
10565-
{
10566-
name: "KV_NAMESPACE",
10567-
type: "inherit",
10568-
},
10569-
{
10570-
name: "R2_BUCKET",
10571-
type: "inherit",
10572-
},
10573-
{
10574-
name: "D1_DATABASE",
10575-
type: "inherit",
10576-
},
10577-
],
10578-
});
10579-
mockSubDomainRequest();
10580-
10581-
await expect(
10582-
runWrangler("deploy --x-provision")
10583-
).resolves.toBeUndefined();
10584-
expect(std.out).toMatchInlineSnapshot(`
10585-
"Total Upload: xx KiB / gzip: xx KiB
10586-
Worker Startup Time: 100 ms
10587-
Your worker has access to the following bindings:
10588-
- KV Namespaces:
10589-
- KV_NAMESPACE: (remote)
10590-
- D1 Databases:
10591-
- D1_DATABASE: (remote)
10592-
- R2 Buckets:
10593-
- R2_BUCKET: (remote)
10594-
Uploaded test-name (TIMINGS)
10595-
Deployed test-name triggers (TIMINGS)
10596-
https://test-name.test-sub-domain.workers.dev
10597-
Current Version ID: Galaxy-Class"
10598-
`);
10599-
expect(std.err).toMatchInlineSnapshot(`""`);
10600-
expect(std.warn).toMatchInlineSnapshot(`""`);
10601-
});
10602-
});
10603-
1060410554
describe("queues", () => {
1060510555
const queueId = "queue-id";
1060610556
const queueName = "queue1";
@@ -12151,20 +12101,6 @@ function mockPublishCustomDomainsRequest({
1215112101
);
1215212102
}
1215312103

12154-
/** Create a mock handler for the request to get a list of all KV namespaces. */
12155-
function mockListKVNamespacesRequest(...namespaces: KVNamespaceInfo[]) {
12156-
msw.use(
12157-
http.get(
12158-
"*/accounts/:accountId/storage/kv/namespaces",
12159-
({ params }) => {
12160-
expect(params.accountId).toEqual("some-account-id");
12161-
return HttpResponse.json(createFetchResult(namespaces));
12162-
},
12163-
{ once: true }
12164-
)
12165-
);
12166-
}
12167-
1216812104
interface ExpectedAsset {
1216912105
filePath: string;
1217012106
content: string;

packages/wrangler/src/__tests__/helpers/mock-kv.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { http, HttpResponse } from "msw";
22
import { createFetchResult, msw } from "./msw";
3-
import type { NamespaceKeyInfo } from "../../kv/helpers";
3+
import type { KVNamespaceInfo, NamespaceKeyInfo } from "../../kv/helpers";
44

55
export function mockKeyListRequest(
66
expectedNamespaceId: string,
@@ -44,3 +44,40 @@ export function mockKeyListRequest(
4444
);
4545
return requests;
4646
}
47+
48+
export function mockListKVNamespacesRequest(...namespaces: KVNamespaceInfo[]) {
49+
msw.use(
50+
http.get(
51+
"*/accounts/:accountId/storage/kv/namespaces",
52+
({ params }) => {
53+
expect(params.accountId).toEqual("some-account-id");
54+
return HttpResponse.json(createFetchResult(namespaces));
55+
},
56+
{ once: true }
57+
)
58+
);
59+
}
60+
61+
export function mockCreateKVNamespace(
62+
options: {
63+
resultId?: string;
64+
assertTitle?: string;
65+
} = {}
66+
) {
67+
msw.use(
68+
http.post(
69+
"*/accounts/:accountId/storage/kv/namespaces",
70+
async ({ request }) => {
71+
if (options.assertTitle) {
72+
const requestBody = await request.json();
73+
expect(requestBody).toEqual({ title: options.assertTitle });
74+
}
75+
76+
return HttpResponse.json(
77+
createFetchResult({ id: options.resultId ?? "some-namespace-id" })
78+
);
79+
},
80+
{ once: true }
81+
)
82+
);
83+
}

0 commit comments

Comments
 (0)