Skip to content

Commit

Permalink
feat(wrangler): identify draft and inherit bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung committed Dec 3, 2024
1 parent 2eed4b3 commit a1a17b7
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-bulldogs-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

The `x-provision` experimental flag now identifies draft and inherit bindings by looking up the current binding settings.
56 changes: 53 additions & 3 deletions packages/wrangler/src/__tests__/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ 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 { Settings } from "../deployment-bundle/bindings";
import type { KVNamespaceInfo } from "../kv/helpers";
import type {
PostQueueBody,
Expand Down Expand Up @@ -10468,17 +10469,36 @@ export default{
});

describe("--x-provision", () => {
it("should accept KV, R2 and D1 bindings without IDs in the configuration file", async () => {
it("should inherit KV, R2 and D1 bindings based on the settings", async () => {
writeWorkerSource();
writeWranglerConfig({
main: "index.js",
kv_namespaces: [{ binding: "KV_NAMESPACE" }],
r2_buckets: [{ binding: "R2_BUCKET" }],
d1_databases: [{ binding: "D1_DATABASE" }],
});
mockGetSettings({
result: {
bindings: [
{
type: "kv_namespace",
name: "KV_NAMESPACE",
namespace_id: "kv-id",
},
{
type: "r2_bucket",
name: "R2_BUCKET",
bucket_name: "test-bucket",
},
{
type: "d1",
name: "D1_DATABASE",
id: "d1-id",
},
],
},
});
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",
Expand Down Expand Up @@ -12325,6 +12345,36 @@ function mockServiceScriptData(options: {
}
}

function mockGetSettings(
options: {
result?: Settings;
assertAccountId?: string;
assertScriptName?: string;
} = {}
) {
msw.use(
http.get(
"*/accounts/:accountId/workers/scripts/:scriptName/settings",
async ({ params }) => {
if (options.assertAccountId) {
expect(params.accountId).toEqual(options.assertAccountId);
}

if (options.assertScriptName) {
expect(params.scriptName).toEqual(options.assertScriptName);
}

return HttpResponse.json({
success: true,
errors: [],
messages: [],
result: options.result ?? { bindings: [] },
});
}
)
);
}

function mockGetQueueByName(queueName: string, queue: QueueResponse | null) {
const requests = { count: 0 };
msw.use(
Expand Down
47 changes: 29 additions & 18 deletions packages/wrangler/src/d1/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ import type {
} from "../yargs-types";
import type { DatabaseCreationResult } from "./types";

export async function createD1Database(
accountId: string,
name: string,
location?: string
) {
try {
return await fetchResult<DatabaseCreationResult>(
`/accounts/${accountId}/d1/database`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name,
...(location && { primary_location_hint: location }),
}),
}
);
} catch (e) {
if ((e as { code: number }).code === 7502) {
throw new UserError("A database with that name already exists");
}

throw e;
}
}

export function Options(yargs: CommonYargsArgv) {
return yargs
.positional("name", {
Expand Down Expand Up @@ -42,24 +70,7 @@ export const Handler = withConfig<HandlerOptions>(
}
}

let db: DatabaseCreationResult;
try {
db = await fetchResult(`/accounts/${accountId}/d1/database`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name,
...(location && { primary_location_hint: location }),
}),
});
} catch (e) {
if ((e as { code: number }).code === 7502) {
throw new UserError("A database with that name already exists");
}
throw e;
}
const db = await createD1Database(accountId, name, location);

logger.log(
`✅ Successfully created DB '${db.name}'${
Expand Down
3 changes: 2 additions & 1 deletion packages/wrangler/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { cancel } from "@cloudflare/cli";
import { syncAssets } from "../assets";
import { fetchListResult, fetchResult } from "../cfetch";
import { configFileName, formatConfigSnippet, printBindings } from "../config";
import { getBindings } from "../deployment-bundle/bindings";
import { getBindings, provisionBindings } from "../deployment-bundle/bindings";
import { bundleWorker } from "../deployment-bundle/bundle";
import {
printBundleSize,
Expand Down Expand Up @@ -788,6 +788,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
} else {
assert(accountId, "Missing accountId");

await provisionBindings(bindings, accountId, scriptName);
await ensureQueuesExistByConfig(config);
let bindingsPrinted = false;

Expand Down
172 changes: 160 additions & 12 deletions packages/wrangler/src/deployment-bundle/bindings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { randomUUID } from "node:crypto";
import { spinner } from "@cloudflare/cli/interactive";
import { fetchResult } from "../cfetch";
import { printBindings } from "../config";
import { createD1Database } from "../d1/create";
import { confirm } from "../dialogs";
import { FatalError } from "../errors";
import { createKVNamespace } from "../kv/helpers";
import { logger } from "../logger";
import { createR2Bucket } from "../r2/helpers";
import type { Config } from "../config";
import type { WorkerMetadataBinding } from "./create-worker-upload-form";
import type { CfWorkerInit } from "./worker";

/**
Expand All @@ -13,10 +24,7 @@ export function getBindings(
}
): CfWorkerInit["bindings"] {
return {
kv_namespaces: config?.kv_namespaces?.map((kv) => ({
...kv,
id: kv.id ?? INHERIT_SYMBOL,
})),
kv_namespaces: config?.kv_namespaces,
send_email: options?.pages ? undefined : config?.send_email,
vars: config?.vars,
wasm_modules: options?.pages ? undefined : config?.wasm_modules,
Expand All @@ -30,14 +38,8 @@ export function getBindings(
queues: config?.queues.producers?.map((producer) => {
return { binding: producer.binding, queue_name: producer.queue };
}),
r2_buckets: config?.r2_buckets?.map((r2) => ({
...r2,
bucket_name: r2.bucket_name ?? INHERIT_SYMBOL,
})),
d1_databases: config?.d1_databases.map((d1) => ({
...d1,
database_id: d1.database_id ?? INHERIT_SYMBOL,
})),
r2_buckets: config?.r2_buckets,
d1_databases: config?.d1_databases,
vectorize: config?.vectorize,
hyperdrive: config?.hyperdrive,
services: config?.services,
Expand All @@ -62,3 +64,149 @@ export function getBindings(
},
};
}

export type Settings = {
bindings: Array<WorkerMetadataBinding>;
};

export type PendingResource =
| {
name: string;
type: "kv";
create: (title: string) => Promise<string>;
}
| {
name: string;
type: "r2";
create: (
bucketName: string,
location?: string,
jurisdiction?: string,
storageClass?: string
) => Promise<string>;
}
| {
name: string;
type: "d1";
create: (name: string, location?: string) => Promise<string>;
};

export async function provisionBindings(
bindings: CfWorkerInit["bindings"],
accountId: string,
scriptName: string
): Promise<void> {
const pendingResources: Array<PendingResource> = [];
let settings: Settings | undefined;

try {
settings = await getSettings(accountId, scriptName);
} catch (error) {
logger.log("No existing bindings found");
}

for (const kv of bindings.kv_namespaces ?? []) {
if (!kv.id) {
if (hasBindingSettings(settings, "kv_namespace", kv.binding)) {
kv.id = INHERIT_SYMBOL;
} else {
pendingResources.push({
type: "kv",
name: kv.binding,
async create(title) {
const id = await createKVNamespace(accountId, title);
kv.id = id;
return id;
},
});
}
}
}

for (const r2 of bindings.r2_buckets ?? []) {
if (!r2.bucket_name) {
if (hasBindingSettings(settings, "r2_bucket", r2.binding)) {
r2.bucket_name = INHERIT_SYMBOL;
} else {
pendingResources.push({
type: "r2",
name: r2.binding,
async create(bucketName, location, jurisdiction, storageClass) {
await createR2Bucket(
accountId,
bucketName,
location,
jurisdiction,
storageClass
);
r2.bucket_name = bucketName;
return bucketName;
},
});
}
}
}

for (const d1 of bindings.d1_databases ?? []) {
if (!d1.database_id) {
if (hasBindingSettings(settings, "d1", d1.binding)) {
d1.database_id = INHERIT_SYMBOL;
} else {
pendingResources.push({
type: "d1",
name: d1.binding,
async create(name, location) {
const db = await createD1Database(accountId, name, location);
d1.database_id = db.uuid;
return db.uuid;
},
});
}
}
}

if (pendingResources.length > 0) {
printBindings(bindings);

// Stylistic newline
logger.log();

const ok = await confirm(
"Would you like Wrangler to provision these resources on your behalf and bind them to your project?"
);

if (ok) {
logger.log("Provisioning resources...");

// After asking the user, create the ones we need to create, mutating `bindings` in the process
for (const binding of pendingResources) {
const s = spinner();

s.start(`- Provisioning ${binding.name}...`);
const id = await binding.create(`${binding.name}-${randomUUID()}`);
s.stop(`- ${binding.name} provisioned with ID "${id}"`);
}
} else {
throw new FatalError("Deployment aborted");
}

logger.log(`All resources provisioned, continuing deployment...`);
}
}

function hasBindingSettings<Type extends WorkerMetadataBinding["type"]>(
settings: Settings | undefined,
type: Type,
name: string
): Extract<WorkerMetadataBinding, { type: Type }> | undefined {
return settings?.bindings.find(
(binding): binding is Extract<WorkerMetadataBinding, { type: Type }> =>
binding.type === type && binding.name === name
);
}

function getSettings(accountId: string, scriptName: string) {
return fetchResult<Settings>(
`/accounts/${accountId}/workers/scripts/${scriptName}/settings`
);
}

0 comments on commit a1a17b7

Please sign in to comment.