Skip to content

Commit

Permalink
feat(wrangler): identify draft and inherit bindings and provision dra…
Browse files Browse the repository at this point in the history
…ft 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]>
  • Loading branch information
edmundhung and emily-shen authored Dec 12, 2024
1 parent 4fe93de commit 3bc0f28
Show file tree
Hide file tree
Showing 13 changed files with 1,183 additions and 109 deletions.
7 changes: 7 additions & 0 deletions .changeset/swift-bulldogs-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

The `x-provision` experimental flag now identifies draft and inherit bindings by looking up the current binding settings.

Draft bindings can then be provisioned (connected to new or existing KV, D1, or R2 resources) during `wrangler deploy`.
5 changes: 5 additions & 0 deletions packages/wrangler/e2e/helpers/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function normalizeOutput(
removeWorkerPreviewUrl,
removeUUID,
removeBinding,
removeKVId,
normalizeErrorMarkers,
replaceByte,
stripTrailingWhitespace,
Expand Down Expand Up @@ -77,6 +78,10 @@ function removeBinding(str: string) {
);
}

function removeKVId(str: string) {
return str.replace(/([0-9a-f]{32})/g, "00000000000000000000000000000000");
}

/**
* Remove the Wrangler version/update check header
*/
Expand Down
195 changes: 195 additions & 0 deletions packages/wrangler/e2e/provision.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import assert from "node:assert";
import dedent from "ts-dedent";
import { fetch } from "undici";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id";
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
import { fetchText } from "./helpers/fetch-text";
import { generateResourceName } from "./helpers/generate-resource-name";
import { normalizeOutput } from "./helpers/normalize";
import { retry } from "./helpers/retry";

const TIMEOUT = 500_000;
const normalize = (str: string) => {
return normalizeOutput(str, {
[CLOUDFLARE_ACCOUNT_ID]: "CLOUDFLARE_ACCOUNT_ID",
});
};
const workerName = generateResourceName();

describe("provisioning", { timeout: TIMEOUT }, () => {
let deployedUrl: string;
let kvId: string;
let d1Id: string;
const helper = new WranglerE2ETestHelper();

it("can run dev without resource ids", async () => {
const worker = helper.runLongLived("wrangler dev --x-provision");

const { url } = await worker.waitForReady();
await fetch(url);

const text = await fetchText(url);

expect(text).toMatchInlineSnapshot(`"Hello World!"`);
});

beforeAll(async () => {
await helper.seed({
"wrangler.toml": dedent`
name = "${workerName}"
main = "src/index.ts"
compatibility_date = "2023-01-01"
[[kv_namespaces]]
binding = "KV"
[[r2_buckets]]
binding = "R2"
[[d1_databases]]
binding = "D1"
`,
"src/index.ts": dedent`
export default {
fetch(request) {
return new Response("Hello World!")
}
}`,
"package.json": dedent`
{
"name": "${workerName}",
"version": "0.0.0",
"private": true
}
`,
});
});

it("can provision resources and deploy worker", async () => {
const worker = helper.runLongLived(
`wrangler deploy --x-provision --x-auto-create`
);
await worker.exitCode;
const output = await worker.output;
expect(normalize(output)).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
The following bindings need to be provisioned:
- KV Namespaces:
- KV
- D1 Databases:
- D1
- R2 Buckets:
- R2
Provisioning KV (KV Namespace)...
🌀 Creating new KV Namespace "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-kv"...
✨ KV provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-kv
--------------------------------------
Provisioning D1 (D1 Database)...
🌀 Creating new D1 Database "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-d1"...
✨ D1 provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-d1
--------------------------------------
Provisioning R2 (R2 Bucket)...
🌀 Creating new R2 Bucket "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2"...
✨ R2 provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2
--------------------------------------
🎉 All resources provisioned, continuing with deployment...
Your worker has access to the following bindings:
- KV Namespaces:
- KV: 00000000000000000000000000000000
- D1 Databases:
- D1: 00000000-0000-0000-0000-000000000000
- R2 Buckets:
- R2: tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2
Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS)
Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS)
https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev
Current Version ID: 00000000-0000-0000-0000-000000000000"
`);
const urlMatch = output.match(
/(?<url>https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/
);
assert(urlMatch?.groups);
deployedUrl = urlMatch.groups.url;

const kvMatch = output.match(/- KV: (?<kv>[0-9a-f]{32})/);
assert(kvMatch?.groups);
kvId = kvMatch.groups.kv;

const d1Match = output.match(/- D1: (?<d1>\w{8}-\w{4}-\w{4}-\w{4}-\w{12})/);
assert(d1Match?.groups);
d1Id = d1Match.groups.d1;

const { text } = await retry(
(s) => s.status !== 200,
async () => {
const r = await fetch(deployedUrl);
return { text: await r.text(), status: r.status };
}
);
expect(text).toMatchInlineSnapshot('"Hello World!"');
});

it("can inherit bindings on re-deploy and won't re-provision", async () => {
const worker = helper.runLongLived(`wrangler deploy --x-provision`);
await worker.exitCode;
const output = await worker.output;
expect(normalize(output)).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
Your worker has access to the following bindings:
- KV Namespaces:
- KV
- D1 Databases:
- D1
- R2 Buckets:
- R2
Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS)
Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS)
https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev
Current Version ID: 00000000-0000-0000-0000-000000000000"
`);

const { text } = await retry(
(s) => s.status !== 200,
async () => {
const r = await fetch(deployedUrl);
return { text: await r.text(), status: r.status };
}
);
expect(text).toMatchInlineSnapshot('"Hello World!"');
});

afterAll(async () => {
// we need to add d1 back into the config because otherwise wrangler will
// call the api for all 5000 or so db's the e2e test account has
// :(
await helper.seed({
"wrangler.toml": dedent`
name = "${workerName}"
main = "src/index.ts"
compatibility_date = "2023-01-01"
[[d1_databases]]
binding = "D1"
database_name = "${workerName}-d1"
database_id = "${d1Id}"
`,
});
let output = await helper.run(`wrangler r2 bucket delete ${workerName}-r2`);
expect(output.stdout).toContain(`Deleted bucket`);
output = await helper.run(`wrangler d1 delete ${workerName}-d1 -y`);
expect(output.stdout).toContain(`Deleted '${workerName}-d1' successfully.`);
output = await helper.run(`wrangler delete`);
expect(output.stdout).toContain("Successfully deleted");
const status = await retry(
(s) => s === 200 || s === 500,
() => fetch(deployedUrl).then((r) => r.status)
);
expect(status).toBe(404);

output = await helper.run(
`wrangler kv namespace delete --namespace-id ${kvId}`
);
expect(output.stdout).toContain(`Deleted KV namespace`);
}, TIMEOUT);
});
72 changes: 4 additions & 68 deletions packages/wrangler/src/__tests__/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import { clearDialogs, mockConfirm } from "./helpers/mock-dialogs";
import { mockGetZoneFromHostRequest } from "./helpers/mock-get-zone-from-host";
import { useMockIsTTY } from "./helpers/mock-istty";
import { mockCollectKnownRoutesRequest } from "./helpers/mock-known-routes";
import { mockKeyListRequest } from "./helpers/mock-kv";
import {
mockKeyListRequest,
mockListKVNamespacesRequest,
} from "./helpers/mock-kv";
import {
mockExchangeRefreshTokenForAccessToken,
mockGetMemberships,
Expand Down Expand Up @@ -52,7 +55,6 @@ import { writeWranglerConfig } from "./helpers/write-wrangler-config";
import type { AssetManifest } from "../assets";
import type { Config } from "../config";
import type { CustomDomain, CustomDomainChangeset } from "../deploy/deploy";
import type { KVNamespaceInfo } from "../kv/helpers";
import type {
PostQueueBody,
PostTypedConsumerBody,
Expand Down Expand Up @@ -10549,58 +10551,6 @@ export default{
});
});

describe("--x-provision", () => {
it("should accept KV, R2 and D1 bindings without IDs in the configuration file", async () => {
writeWorkerSource();
writeWranglerConfig({
main: "index.js",
kv_namespaces: [{ binding: "KV_NAMESPACE" }],
r2_buckets: [{ binding: "R2_BUCKET" }],
d1_databases: [{ binding: "D1_DATABASE" }],
});
mockUploadWorkerRequest({
// We are treating them as inherited bindings temporarily to test the current implementation only
// This will be updated as we implement the actual provision logic
expectedBindings: [
{
name: "KV_NAMESPACE",
type: "inherit",
},
{
name: "R2_BUCKET",
type: "inherit",
},
{
name: "D1_DATABASE",
type: "inherit",
},
],
});
mockSubDomainRequest();

await expect(
runWrangler("deploy --x-provision")
).resolves.toBeUndefined();
expect(std.out).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your worker has access to the following bindings:
- KV Namespaces:
- KV_NAMESPACE: (remote)
- D1 Databases:
- D1_DATABASE: (remote)
- R2 Buckets:
- R2_BUCKET: (remote)
Uploaded test-name (TIMINGS)
Deployed test-name triggers (TIMINGS)
https://test-name.test-sub-domain.workers.dev
Current Version ID: Galaxy-Class"
`);
expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.warn).toMatchInlineSnapshot(`""`);
});
});

describe("queues", () => {
const queueId = "queue-id";
const queueName = "queue1";
Expand Down Expand Up @@ -12151,20 +12101,6 @@ function mockPublishCustomDomainsRequest({
);
}

/** Create a mock handler for the request to get a list of all KV namespaces. */
function mockListKVNamespacesRequest(...namespaces: KVNamespaceInfo[]) {
msw.use(
http.get(
"*/accounts/:accountId/storage/kv/namespaces",
({ params }) => {
expect(params.accountId).toEqual("some-account-id");
return HttpResponse.json(createFetchResult(namespaces));
},
{ once: true }
)
);
}

interface ExpectedAsset {
filePath: string;
content: string;
Expand Down
39 changes: 38 additions & 1 deletion packages/wrangler/src/__tests__/helpers/mock-kv.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { http, HttpResponse } from "msw";
import { createFetchResult, msw } from "./msw";
import type { NamespaceKeyInfo } from "../../kv/helpers";
import type { KVNamespaceInfo, NamespaceKeyInfo } from "../../kv/helpers";

export function mockKeyListRequest(
expectedNamespaceId: string,
Expand Down Expand Up @@ -44,3 +44,40 @@ export function mockKeyListRequest(
);
return requests;
}

export function mockListKVNamespacesRequest(...namespaces: KVNamespaceInfo[]) {
msw.use(
http.get(
"*/accounts/:accountId/storage/kv/namespaces",
({ params }) => {
expect(params.accountId).toEqual("some-account-id");
return HttpResponse.json(createFetchResult(namespaces));
},
{ once: true }
)
);
}

export function mockCreateKVNamespace(
options: {
resultId?: string;
assertTitle?: string;
} = {}
) {
msw.use(
http.post(
"*/accounts/:accountId/storage/kv/namespaces",
async ({ request }) => {
if (options.assertTitle) {
const requestBody = await request.json();
expect(requestBody).toEqual({ title: options.assertTitle });
}

return HttpResponse.json(
createFetchResult({ id: options.resultId ?? "some-namespace-id" })
);
},
{ once: true }
)
);
}
Loading

0 comments on commit 3bc0f28

Please sign in to comment.