From a6f1cee08e9aea0e0366b5c15d28e9600df40d27 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 5 Jul 2022 12:13:19 +0100 Subject: [PATCH] feat: bind a worker with `[worker_namespaces]` (#1377) This feature les you bind a worker to a dynamic dispatch namespaces, which may have other workers bound inside it. (See https://blog.cloudflare.com/workers-for-platforms/). Inside your `wrangler.toml`, you would add ```toml [[worker_namespaces]] binding = 'dispatcher' # available as env.dispatcher in your worker namespace = 'namespace-name' # the name of the namespace being bound ``` Based on work by @aaronlisman in https://github.com/cloudflare/wrangler2/pull/1310 --- .changeset/sweet-wasps-clean.md | 15 +++ .../src/__tests__/configuration.test.ts | 108 ++++++++++++++++++ .../wrangler/src/__tests__/publish.test.ts | 43 +++++++ packages/wrangler/src/config/environment.ts | 16 +++ packages/wrangler/src/config/index.ts | 13 +++ packages/wrangler/src/config/validation.ts | 44 +++++++ .../wrangler/src/create-worker-upload-form.ts | 9 ++ packages/wrangler/src/dev.tsx | 6 +- packages/wrangler/src/index.tsx | 1 + packages/wrangler/src/publish.ts | 1 + packages/wrangler/src/worker.ts | 6 + 11 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 .changeset/sweet-wasps-clean.md diff --git a/.changeset/sweet-wasps-clean.md b/.changeset/sweet-wasps-clean.md new file mode 100644 index 000000000000..8a3c9aaafb6a --- /dev/null +++ b/.changeset/sweet-wasps-clean.md @@ -0,0 +1,15 @@ +--- +"wrangler": patch +--- + +feat: bind a worker with `[worker_namespaces]` + +This feature les you bind a worker to a dynamic dispatch namespaces, which may have other workers bound inside it. (See https://blog.cloudflare.com/workers-for-platforms/). Inside your `wrangler.toml`, you would add + +```toml +[[worker_namespaces]] +binding = 'dispatcher' # available as env.dispatcher in your worker +namespace = 'namespace-name' # the name of the namespace being bound +``` + +Based on work by @aaronlisman in https://github.com/cloudflare/wrangler2/pull/1310 diff --git a/packages/wrangler/src/__tests__/configuration.test.ts b/packages/wrangler/src/__tests__/configuration.test.ts index f233895e8906..38fd3ddc3ed3 100644 --- a/packages/wrangler/src/__tests__/configuration.test.ts +++ b/packages/wrangler/src/__tests__/configuration.test.ts @@ -16,6 +16,7 @@ describe("normalizeAndValidateConfig()", () => { expect(config).toEqual({ account_id: undefined, + assets: undefined, build: { command: undefined, cwd: undefined, @@ -55,6 +56,7 @@ describe("normalizeAndValidateConfig()", () => { unsafe: { bindings: [], }, + worker_namespaces: [], usage_model: undefined, vars: {}, define: {}, @@ -1839,6 +1841,112 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[worker_namespaces]", () => { + it("should log an experimental warning when worker_namespaces is used", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + worker_namespaces: [ + { + binding: "BINDING_1", + namespace: "NAMESPACE_1", + }, + ], + } as unknown as RawConfig, + undefined, + { env: undefined } + ); + expect(config).toEqual( + expect.not.objectContaining({ worker_namespaces: expect.anything }) + ); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - \\"worker_namespaces\\" fields are experimental and may change or break at any time." + `); + }); + + it("should error if worker_namespaces is not an array", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + worker_namespaces: "just a string", + } as unknown as RawConfig, + undefined, + { env: undefined } + ); + + expect(config).toEqual( + expect.not.objectContaining({ worker_namespaces: expect.anything }) + ); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - \\"worker_namespaces\\" fields are experimental and may change or break at any time." + `); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field \\"worker_namespaces\\" should be an array but got \\"just a string\\"." + `); + }); + + it("should error on non valid worker_namespaces", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + worker_namespaces: [ + "a string", + 123, + { + binding: 123, + namespace: 456, + }, + { + binding: "WORKER_NAMESPACE_BINDING_1", + namespace: 456, + }, + // this one is valid + { + binding: "WORKER_NAMESPACE_BINDING_1", + namespace: "WORKER_NAMESPACE_BINDING_NAMESPACE_1", + }, + { + binding: 123, + namespace: "WORKER_NAMESPACE_BINDING_SERVICE_1", + }, + { + binding: 123, + service: 456, + }, + ], + } as unknown as RawConfig, + undefined, + { env: undefined } + ); + expect(config).toEqual( + expect.not.objectContaining({ + worker_namespaces: expect.anything, + }) + ); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - \\"worker_namespaces\\" fields are experimental and may change or break at any time." + `); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - \\"worker_namespaces[0]\\" binding should be objects, but got \\"a string\\" + - \\"worker_namespaces[1]\\" binding should be objects, but got 123 + - \\"worker_namespaces[2]\\" should have a string \\"binding\\" field but got {\\"binding\\":123,\\"namespace\\":456}. + - \\"worker_namespaces[2]\\" should have a string \\"namespace\\" field but got {\\"binding\\":123,\\"namespace\\":456}. + - \\"worker_namespaces[3]\\" should have a string \\"namespace\\" field but got {\\"binding\\":\\"WORKER_NAMESPACE_BINDING_1\\",\\"namespace\\":456}. + - \\"worker_namespaces[5]\\" should have a string \\"binding\\" field but got {\\"binding\\":123,\\"namespace\\":\\"WORKER_NAMESPACE_BINDING_SERVICE_1\\"}. + - \\"worker_namespaces[6]\\" should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456}. + - \\"worker_namespaces[6]\\" should have a string \\"namespace\\" field but got {\\"binding\\":123,\\"service\\":456}." + `); + }); + }); + describe("[unsafe.bindings]", () => { it("should error if unsafe is an array", () => { const { config, diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/__tests__/publish.test.ts b/packages/wrangler/src/__tests__/publish.test.ts index 625abbb59cf2..acb9036aebba 100644 --- a/packages/wrangler/src/__tests__/publish.test.ts +++ b/packages/wrangler/src/__tests__/publish.test.ts @@ -5303,6 +5303,48 @@ addEventListener('fetch', event => {});` }); }); + describe("[worker_namespaces]", () => { + it("should support bindings to a worker namespace", async () => { + writeWranglerToml({ + worker_namespaces: [ + { + binding: "foo", + namespace: "Foo", + }, + ], + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "namespace", + name: "foo", + namespace: "Foo", + }, + ], + }); + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Your worker has access to the following bindings: + - Worker Namespaces: + - foo: Foo + Total Upload: 0xx KiB / gzip: 0xx KiB + Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(` + "▲ [WARNING] Processing wrangler.toml configuration: + + - \\"worker_namespaces\\" fields are experimental and may change or break at any time. + + " + `); + }); + }); + describe("[unsafe]", () => { it("should warn if using unsafe bindings", async () => { writeWranglerToml({ @@ -5962,6 +6004,7 @@ addEventListener('fetch', event => {});` } `); }); + it("should print the bundle size, with API errors", async () => { setMockRawResponse( "/accounts/:accountId/workers/scripts/:scriptName", diff --git a/packages/wrangler/src/config/environment.ts b/packages/wrangler/src/config/environment.ts index 96f5e07bdeaf..437c5c572a3b 100644 --- a/packages/wrangler/src/config/environment.ts +++ b/packages/wrangler/src/config/environment.ts @@ -201,6 +201,22 @@ interface EnvironmentInheritable { */ node_compat: boolean | undefined; + /** + * Specifies namespace bindings that are bound to this Worker environment. + * + * NOTE: This field is not automatically inherited from the top level environment, + * and so must be specified in every named environment. + * + * @default `[]` + * @nonInheritable + */ + worker_namespaces: { + /** The binding name used to refer to the bound service. */ + binding: string; + /** The namespace to bind to. */ + namespace: string; + }[]; + /** * TODO: remove this as it has been deprecated. * diff --git a/packages/wrangler/src/config/index.ts b/packages/wrangler/src/config/index.ts index 9608694f3243..6c1b715caee4 100644 --- a/packages/wrangler/src/config/index.ts +++ b/packages/wrangler/src/config/index.ts @@ -90,6 +90,7 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) { unsafe, vars, wasm_modules, + worker_namespaces, } = bindings; if (data_blobs !== undefined && Object.keys(data_blobs).length > 0) { @@ -205,6 +206,18 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) { }); } + if (worker_namespaces !== undefined && worker_namespaces.length > 0) { + output.push({ + type: "Worker Namespaces", + entries: worker_namespaces.map(({ binding, namespace }) => { + return { + key: binding, + value: namespace, + }; + }), + }); + } + if (output.length === 0) { return; } diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index 449c6d496d11..290262ec93d7 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -832,6 +832,7 @@ function normalizeAndValidateEnvironment( experimental(diagnostics, rawEnv, "unsafe"); experimental(diagnostics, rawEnv, "services"); + experimental(diagnostics, rawEnv, "worker_namespaces"); const route = normalizeAndValidateRoute(diagnostics, topLevelEnv, rawEnv); @@ -1017,6 +1018,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateServiceBinding), [] ), + worker_namespaces: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "worker_namespaces", + validateBindingArray(envName, validateWorkerNamespaceBinding), + [] + ), unsafe: notInheritable( diagnostics, topLevelEnv, @@ -1664,6 +1675,7 @@ const validateBindingsHaveUniqueNames = ( return !hasDuplicates; }; + const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => { if (typeof value !== "object" || value === null) { diagnostics.errors.push( @@ -1699,3 +1711,35 @@ const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => { } return isValid; }; + +const validateWorkerNamespaceBinding: ValidatorFn = ( + diagnostics, + field, + value +) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"${field}" binding should be objects, but got ${JSON.stringify(value)}` + ); + return false; + } + let isValid = true; + // Worker namespace bindings must have a binding, and a namespace. + if (!isRequiredProperty(value, "binding", "string")) { + diagnostics.errors.push( + `"${field}" should have a string "binding" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + if (!isRequiredProperty(value, "namespace", "string")) { + diagnostics.errors.push( + `"${field}" should have a string "namespace" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + return isValid; +}; diff --git a/packages/wrangler/src/create-worker-upload-form.ts b/packages/wrangler/src/create-worker-upload-form.ts index 6b457ed7f33b..f6beb30306df 100644 --- a/packages/wrangler/src/create-worker-upload-form.ts +++ b/packages/wrangler/src/create-worker-upload-form.ts @@ -50,6 +50,7 @@ export interface WorkerMetadata { } | { type: "r2_bucket"; name: string; bucket_name: string } | { type: "service"; name: string; service: string; environment?: string } + | { type: "namespace"; name: string; namespace: string } )[]; } @@ -116,6 +117,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { }); }); + bindings.worker_namespaces?.forEach(({ binding, namespace }) => { + metadataBindings.push({ + name: binding, + type: "namespace", + namespace, + }); + }); + for (const [name, filePath] of Object.entries(bindings.wasm_modules || {})) { metadataBindings.push({ name, diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index 1a83ad0d0e13..d1b27ff68527 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -25,6 +25,7 @@ import { import type { Config } from "./config"; import type { Route } from "./config/environment"; +import type { CfWorkerInit } from "./worker"; import type { RequestInit } from "undici"; import type { Argv, ArgumentsCamelCase } from "yargs"; @@ -371,7 +372,9 @@ export async function startDev(args: ArgumentsCamelCase) { } // eslint-disable-next-line no-inner-declarations - async function getBindings(configParam: Config) { + async function getBindings( + configParam: Config + ): Promise { return { kv_namespaces: configParam.kv_namespaces?.map( ({ binding, preview_id, id: _id }) => { @@ -415,6 +418,7 @@ export async function startDev(args: ArgumentsCamelCase) { }; } ), + worker_namespaces: configParam.worker_namespaces, services: configParam.services, unsafe: configParam.unsafe?.bindings, }; diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index 66ce425b1e27..8fca6715e6dd 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -916,6 +916,7 @@ function createCLIParser(argv: string[]) { wasm_modules: {}, text_blobs: {}, data_blobs: {}, + worker_namespaces: [], unsafe: [], }, modules: [], diff --git a/packages/wrangler/src/publish.ts b/packages/wrangler/src/publish.ts index 0a81bb0e638d..b43ceffc337f 100644 --- a/packages/wrangler/src/publish.ts +++ b/packages/wrangler/src/publish.ts @@ -426,6 +426,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m durable_objects: config.durable_objects, r2_buckets: config.r2_buckets, services: config.services, + worker_namespaces: config.worker_namespaces, unsafe: config.unsafe?.bindings, }; diff --git a/packages/wrangler/src/worker.ts b/packages/wrangler/src/worker.ts index 1c266a4d2e1c..edb63fc4cc4f 100644 --- a/packages/wrangler/src/worker.ts +++ b/packages/wrangler/src/worker.ts @@ -122,6 +122,11 @@ interface CfService { environment?: string; } +interface CfWorkerNamespace { + binding: string; + namespace: string; +} + interface CfUnsafeBinding { name: string; type: string; @@ -168,6 +173,7 @@ export interface CfWorkerInit { durable_objects: { bindings: CfDurableObject[] } | undefined; r2_buckets: CfR2Bucket[] | undefined; services: CfService[] | undefined; + worker_namespaces: CfWorkerNamespace[] | undefined; unsafe: CfUnsafeBinding[] | undefined; }; migrations: CfDurableObjectMigrations | undefined;