diff --git a/.changeset/hot-insects-cry.md b/.changeset/hot-insects-cry.md new file mode 100644 index 000000000000..d9a102039178 --- /dev/null +++ b/.changeset/hot-insects-cry.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +feat: add service bindings support diff --git a/packages/wrangler/src/__tests__/configuration.test.ts b/packages/wrangler/src/__tests__/configuration.test.ts index c6ea462cfb57..e09d1ff77d1c 100644 --- a/packages/wrangler/src/__tests__/configuration.test.ts +++ b/packages/wrangler/src/__tests__/configuration.test.ts @@ -44,6 +44,7 @@ describe("normalizeAndValidateConfig()", () => { migrations: [], name: undefined, r2_buckets: [], + services: [], route: undefined, routes: undefined, rules: [], @@ -536,6 +537,13 @@ describe("normalizeAndValidateConfig()", () => { preview_bucket_name: "R2_PREVIEW_2", }, ], + services: [ + { + binding: "SERVICE_BINDING_1", + service: "SERVICE_BINDING_1", + environment: "SERVICE_BINDING_ENVIRONMENT_1", + }, + ], unsafe: { bindings: [ { name: "UNSAFE_BINDING_1", type: "UNSAFE_TYPE_1" }, @@ -1152,6 +1160,131 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("services field", () => { + it("should error if services is an object", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { services: {} } as unknown as RawConfig, + undefined, + undefined, + {} + ); + + expect(config).toEqual( + expect.not.objectContaining({ services: expect.anything }) + ); + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field \\"services\\" should be an array but got {}." + `); + }); + + it("should error if services is a string", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { services: "BAD" } as unknown as RawConfig, + undefined, + undefined, + {} + ); + + expect(config).toEqual( + expect.not.objectContaining({ services: expect.anything }) + ); + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field \\"services\\" should be an array but got \\"BAD\\"." + `); + }); + + it("should error if services is a number", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { services: 999 } as unknown as RawConfig, + undefined, + undefined, + {} + ); + + expect(config).toEqual( + expect.not.objectContaining({ services: expect.anything }) + ); + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field \\"services\\" should be an array but got 999." + `); + }); + + it("should error if services is null", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { services: null } as unknown as RawConfig, + undefined, + undefined, + {} + ); + + expect(config).toEqual( + expect.not.objectContaining({ services: expect.anything }) + ); + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field \\"services\\" should be an array but got null." + `); + }); + + it("should error if services bindings are not valid", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + services: [ + {}, + { binding: "SERVICE_BINDING_1" }, + { binding: 123, service: 456 }, + { binding: 123, service: 456, environment: 789 }, + { binding: "SERVICE_BINDING_1", service: 456, environment: 789 }, + { + binding: 123, + service: "SERVICE_BINDING_SERVICE_1", + environment: 789, + }, + { + binding: 123, + service: 456, + environment: "SERVICE_BINDING_ENVIRONMENT_1", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + {} + ); + + expect(config).toEqual( + expect.not.objectContaining({ + services: { bindings: expect.anything }, + }) + ); + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - \\"services[0]\\" bindings should have a string \\"binding\\" field but got {}. + - \\"services[0]\\" bindings should have a string \\"service\\" field but got {}. + - \\"services[1]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\"}. + - \\"services[2]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456}. + - \\"services[2]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456}. + - \\"services[3]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. + - \\"services[3]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. + - \\"services[3]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. + - \\"services[4]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}. + - \\"services[4]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}. + - \\"services[5]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}. + - \\"services[5]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}. + - \\"services[6]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}. + - \\"services[6]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}." + `); + }); + }); + describe("unsafe field", () => { it("should error if unsafe is an array", () => { const { config, diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/__tests__/dev2.test.tsx b/packages/wrangler/src/__tests__/dev2.test.tsx index 4d3c6fe61d10..baa06d8f0038 100644 --- a/packages/wrangler/src/__tests__/dev2.test.tsx +++ b/packages/wrangler/src/__tests__/dev2.test.tsx @@ -106,6 +106,7 @@ async function renderDev({ vars: {}, durable_objects: { bindings: [] }, r2_buckets: [], + services: [], wasm_modules: undefined, text_blobs: undefined, unsafe: [], diff --git a/packages/wrangler/src/__tests__/publish.test.ts b/packages/wrangler/src/__tests__/publish.test.ts index bae59a6fabb2..d68a26f4a94b 100644 --- a/packages/wrangler/src/__tests__/publish.test.ts +++ b/packages/wrangler/src/__tests__/publish.test.ts @@ -2104,6 +2104,37 @@ export default{ }); }); + describe("service bindings", () => { + it("should support service bindings", async () => { + writeWranglerToml({ + services: [ + { binding: "FOO", service: "foo-service", environment: "production" }, + ], + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "service", + name: "FOO", + service: "foo-service", + environment: "production", + }, + ], + }); + + await runWrangler("publish index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + test-name.test-sub-domain.workers.dev" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + }); + describe("unsafe bindings", () => { it("should warn if using unsafe bindings", async () => { writeWranglerToml({ diff --git a/packages/wrangler/src/config/environment.ts b/packages/wrangler/src/config/environment.ts index 1008648ec560..e9c49c80b71b 100644 --- a/packages/wrangler/src/config/environment.ts +++ b/packages/wrangler/src/config/environment.ts @@ -251,6 +251,18 @@ interface EnvironmentNonInheritable { preview_bucket_name?: string; }[]; + /** + * Specifies service bindings (worker-to-worker) that are bound to this Worker environment. + */ + services: { + /** The binding name used to refer to the bound service. */ + binding: string; + /** The name of the service. */ + service: string; + /** The environment of the service (e.g. production, staging, etc). */ + environment: string; + }[]; + /** * "Unsafe" tables for features that aren't directly supported by wrangler. * diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index 337863eebab2..8b4c1c43fb15 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -667,6 +667,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateR2Binding), [] ), + services: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "services", + validateBindingArray(envName, validateServiceBinding), + [] + ), unsafe: notInheritable( diagnostics, topLevelEnv, @@ -950,6 +960,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "json", "kv_namespace", "durable_object_namespace", + "service", ]; if (safeBindings.includes(value.type)) { @@ -1094,3 +1105,39 @@ const validateR2Binding: ValidatorFn = (diagnostics, field, value) => { } return isValid; }; + +const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"services" bindings should be objects, but got ${JSON.stringify(value)}` + ); + return false; + } + let isValid = true; + // Service bindings must have a binding, service, and environment. + if (!isRequiredProperty(value, "binding", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "binding" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + if (!isRequiredProperty(value, "service", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "service" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + if (!isOptionalProperty(value, "environment", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "environment" 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 53f0bc2f40f5..380b9355329a 100644 --- a/packages/wrangler/src/create-worker-upload-form.ts +++ b/packages/wrangler/src/create-worker-upload-form.ts @@ -45,6 +45,7 @@ export interface WorkerMetadata { script_name?: string; } | { type: "r2_bucket"; name: string; bucket_name: string } + | { type: "service"; name: string; service: string; environment: string } )[]; } @@ -66,6 +67,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { const metadataBindings: WorkerMetadata["bindings"] = []; + Object.entries(bindings.vars || {})?.forEach(([key, value]) => { + if (typeof value === "string") { + metadataBindings.push({ name: key, type: "plain_text", text: value }); + } else { + metadataBindings.push({ name: key, type: "json", json: value }); + } + }); + bindings.kv_namespaces?.forEach(({ id, binding }) => { metadataBindings.push({ name: binding, @@ -93,12 +102,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { }); }); - Object.entries(bindings.vars || {})?.forEach(([key, value]) => { - if (typeof value === "string") { - metadataBindings.push({ name: key, type: "plain_text", text: value }); - } else { - metadataBindings.push({ name: key, type: "json", json: value }); - } + bindings.services?.forEach(({ binding, service, environment }) => { + metadataBindings.push({ + name: binding, + type: "service", + service, + environment, + }); }); for (const [name, filePath] of Object.entries(bindings.wasm_modules || {})) { diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index b4644f843dd5..ccb61bcdb0cf 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -983,6 +983,7 @@ export async function main(argv: string[]): Promise { text_blobs: config.text_blobs, durable_objects: config.durable_objects, r2_buckets: config.r2_buckets, + services: config.services, unsafe: config.unsafe?.bindings, }} /> @@ -1359,6 +1360,7 @@ export async function main(argv: string[]): Promise { text_blobs: config.text_blobs, durable_objects: config.durable_objects, r2_buckets: config.r2_buckets, + services: config.services, unsafe: config.unsafe?.bindings, }} inspectorPort={await getPort({ port: 9229 })} @@ -1548,6 +1550,7 @@ export async function main(argv: string[]): Promise { vars: {}, durable_objects: { bindings: [] }, r2_buckets: [], + services: [], wasm_modules: {}, text_blobs: {}, unsafe: [], diff --git a/packages/wrangler/src/publish.ts b/packages/wrangler/src/publish.ts index 7af31dfdd6c4..55d4a133a520 100644 --- a/packages/wrangler/src/publish.ts +++ b/packages/wrangler/src/publish.ts @@ -170,6 +170,7 @@ export default async function publish(props: Props): Promise { }, durable_objects: config.durable_objects, r2_buckets: config.r2_buckets, + services: config.services, unsafe: config.unsafe?.bindings, }; diff --git a/packages/wrangler/src/worker.ts b/packages/wrangler/src/worker.ts index 022bf6e4debe..1ce43afef04a 100644 --- a/packages/wrangler/src/worker.ts +++ b/packages/wrangler/src/worker.ts @@ -104,6 +104,12 @@ interface CfR2Bucket { bucket_name: string; } +interface CfService { + binding: string; + service: string; + environment: string; +} + interface CfUnsafeBinding { name: string; type: string; @@ -148,6 +154,7 @@ export interface CfWorkerInit { text_blobs: CfTextBlobBindings | undefined; durable_objects: { bindings: CfDurableObject[] } | undefined; r2_buckets: CfR2Bucket[] | undefined; + services: CfService[] | undefined; unsafe: CfUnsafeBinding[] | undefined; }; migrations: undefined | CfDurableObjectMigrations;