Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wrangler): identify draft and inherit bindings and provision draft ones #7427

Merged
merged 15 commits into from
Dec 12, 2024
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!"');
});

emily-shen marked this conversation as resolved.
Show resolved Hide resolved
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
Loading