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: implement support for service bindings #906

Merged
merged 1 commit into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/hot-insects-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"wrangler": patch
---

feat: implement support for service bindings

This adds experimental support for service bindings, aka worker-to-worker bindings. It's lets you "call" a worker from another worker, without incurring any network cost, and (ideally) with much less latency. To use it, define a `[services]` field in `wrangler.toml`, which is a map of bindings to worker names (and environment). Let's say you already have a worker named "my-worker" deployed. In another worker's configuration, you can create a service binding to it like so:

```toml
[[services]]
binding = "MYWORKER"
service = "my-worker"
environment = "production" # optional, defaults to the worker's `default_environment` for now
threepointone marked this conversation as resolved.
Show resolved Hide resolved
```

And in your worker, you can call it like so:

```js
export default {
fetch(req, env, ctx) {
return env.MYWORKER.fetch(new Request("http://domain/some-path"));
},
};
```

Fixes https://github.com/cloudflare/wrangler2/issues/1026
178 changes: 159 additions & 19 deletions packages/wrangler/src/__tests__/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe("normalizeAndValidateConfig()", () => {
migrations: [],
name: undefined,
r2_buckets: [],
services: [],
route: undefined,
routes: undefined,
rules: [],
Expand Down Expand Up @@ -644,6 +645,13 @@ describe("normalizeAndValidateConfig()", () => {
preview_bucket_name: "R2_PREVIEW_2",
},
],
services: [
{
binding: "SERVICE_BINDING_1",
service: "SERVICE_TYPE_1",
environment: "SERVICE_BINDING_ENVIRONMENT_1",
},
],
unsafe: {
bindings: [
{ name: "UNSAFE_BINDING_1", type: "UNSAFE_TYPE_1" },
Expand All @@ -667,9 +675,10 @@ describe("normalizeAndValidateConfig()", () => {
);
expect(diagnostics.hasErrors()).toBe(false);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"unsafe\\" fields are experimental and may change or break at any time."
`);
"Processing wrangler configuration:
- \\"unsafe\\" fields are experimental and may change or break at any time.
- \\"services\\" fields are experimental and may change or break at any time."
`);
});

it("should error on invalid environment values", () => {
Expand Down Expand Up @@ -1395,6 +1404,151 @@ describe("normalizeAndValidateConfig()", () => {
});
});

describe("services field", () => {
it("should error if services is an object", () => {
const { config, diagnostics } = normalizeAndValidateConfig(
{ services: {} } as unknown as RawConfig,
undefined,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
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,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
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,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
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,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({ services: expect.anything })
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
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,
{ env: undefined }
);

expect(config).toEqual(
expect.not.objectContaining({
services: { bindings: expect.anything },
})
);
expect(diagnostics.hasWarnings()).toBe(true);
expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- \\"services\\" fields are experimental and may change or break at any time."
`);
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(
Expand Down Expand Up @@ -1659,14 +1813,7 @@ describe("normalizeAndValidateConfig()", () => {
- Deprecation: \\"zone_id\\":
This is unnecessary since we can deduce this from routes directly.
- Deprecation: \\"experimental_services\\":
The \\"experimental_services\\" field is no longer supported. Instead, use [[unsafe.bindings]] to enable experimental features. Add this to your wrangler.toml:
\`\`\`
[[unsafe.bindings]]
name = \\"mock-name\\"
type = \\"service\\"
service = \\"SERVICE\\"
environment = \\"ENV\\"
\`\`\`"
The \\"experimental_services\\" field is no longer supported. Simply rename the [experimental_services] field to [services]."
`);
});
});
Expand Down Expand Up @@ -2906,14 +3053,7 @@ describe("normalizeAndValidateConfig()", () => {
- Deprecation: \\"zone_id\\":
This is unnecessary since we can deduce this from routes directly.
- Deprecation: \\"experimental_services\\":
The \\"experimental_services\\" field is no longer supported. Instead, use [[unsafe.bindings]] to enable experimental features. Add this to your wrangler.toml:
\`\`\`
[[unsafe.bindings]]
name = \\"mock-name\\"
type = \\"service\\"
service = \\"SERVICE\\"
environment = \\"ENV\\"
\`\`\`"
The \\"experimental_services\\" field is no longer supported. Simply rename the [experimental_services] field to [services]."
`);
});
});
Expand Down
28 changes: 28 additions & 0 deletions packages/wrangler/src/__tests__/dev.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,34 @@ describe("wrangler dev", () => {
`);
});
});

describe("service bindings", () => {
it("should warn when using service bindings", async () => {
writeWranglerToml({
services: [
{ binding: "WorkerA", service: "A" },
{ binding: "WorkerB", service: "B", environment: "staging" },
],
});
fs.writeFileSync("index.js", `export default {};`);
await runWrangler("dev index.js");
expect(std).toMatchInlineSnapshot(`
Object {
"debug": "",
"err": "",
"out": "",
"warn": "▲ [WARNING] Processing wrangler.toml configuration:

- \\"services\\" fields are experimental and may change or break at any time.


▲ [WARNING] This worker is bound to live services: WorkerA (A), WorkerB (B@staging)

",
}
`);
});
});
});
threepointone marked this conversation as resolved.
Show resolved Hide resolved

function mockGetZones(domain: string, zones: { id: string }[] = []) {
Expand Down
53 changes: 47 additions & 6 deletions packages/wrangler/src/__tests__/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3165,6 +3165,12 @@ addEventListener('fetch', event => {});`
mockUploadWorkerRequest({
expectedType: "sw",
expectedBindings: [
{ json: 123, name: "ENV_VAR_ONE", type: "json" },
{
name: "ENV_VAR_TWO",
text: "Hello, I'm an environment variable",
type: "plain_text",
},
threepointone marked this conversation as resolved.
Show resolved Hide resolved
{
name: "KV_NAMESPACE_ONE",
namespace_id: "kv-ns-one-id",
Expand Down Expand Up @@ -3198,12 +3204,6 @@ addEventListener('fetch', event => {});`
name: "R2_BUCKET_TWO",
type: "r2_bucket",
},
{ json: 123, name: "ENV_VAR_ONE", type: "json" },
{
name: "ENV_VAR_TWO",
text: "Hello, I'm an environment variable",
type: "plain_text",
},
{
name: "WASM_MODULE_ONE",
part: "WASM_MODULE_ONE",
Expand Down Expand Up @@ -4125,6 +4125,47 @@ addEventListener('fetch', event => {});`
});
});

describe("[services]", () => {
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(`
"▲ [WARNING] Processing wrangler.toml configuration:

- \\"services\\" fields are experimental and may change or break at any time.

"
`);
});
});

describe("[unsafe]", () => {
it("should warn if using unsafe bindings", async () => {
writeWranglerToml({
Expand Down
18 changes: 18 additions & 0 deletions packages/wrangler/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,24 @@ interface EnvironmentNonInheritable {
preview_bucket_name?: string;
}[];

/**
* Specifies service bindings (worker-to-worker) 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
*/
threepointone marked this conversation as resolved.
Show resolved Hide resolved
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.
*
Expand Down
Loading