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: add service bindings support #683

Closed
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
5 changes: 5 additions & 0 deletions .changeset/hot-insects-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

feat: add service bindings support
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a description of what service bindings are, and a short example of what a service configuration in wrangler.toml would look like.

133 changes: 133 additions & 0 deletions packages/wrangler/src/__tests__/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe("normalizeAndValidateConfig()", () => {
migrations: [],
name: undefined,
r2_buckets: [],
services: [],
route: undefined,
routes: undefined,
rules: [],
Expand Down Expand Up @@ -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" },
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/__tests__/dev2.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ async function renderDev({
vars: {},
durable_objects: { bindings: [] },
r2_buckets: [],
services: [],
wasm_modules: undefined,
text_blobs: undefined,
unsafe: [],
Expand Down
31 changes: 31 additions & 0 deletions packages/wrangler/src/__tests__/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
12 changes: 12 additions & 0 deletions packages/wrangler/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
47 changes: 47 additions & 0 deletions packages/wrangler/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,16 @@ function normalizeAndValidateEnvironment(
validateBindingArray(envName, validateR2Binding),
[]
),
services: notInheritable(
diagnostics,
topLevelEnv,
rawConfig,
rawEnv,
envName,
"services",
validateBindingArray(envName, validateServiceBinding),
[]
),
unsafe: notInheritable(
diagnostics,
topLevelEnv,
Expand Down Expand Up @@ -950,6 +960,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
"json",
"kv_namespace",
"durable_object_namespace",
"service",
];

if (safeBindings.includes(value.type)) {
Expand Down Expand Up @@ -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;
};
22 changes: 16 additions & 6 deletions packages/wrangler/src/create-worker-upload-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
)[];
}

Expand All @@ -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,
Expand Down Expand Up @@ -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 || {})) {
Expand Down
3 changes: 3 additions & 0 deletions packages/wrangler/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,7 @@ export async function main(argv: string[]): Promise<void> {
text_blobs: config.text_blobs,
durable_objects: config.durable_objects,
r2_buckets: config.r2_buckets,
services: config.services,
unsafe: config.unsafe?.bindings,
}}
/>
Expand Down Expand Up @@ -1359,6 +1360,7 @@ export async function main(argv: string[]): Promise<void> {
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 })}
Expand Down Expand Up @@ -1548,6 +1550,7 @@ export async function main(argv: string[]): Promise<void> {
vars: {},
durable_objects: { bindings: [] },
r2_buckets: [],
services: [],
wasm_modules: {},
text_blobs: {},
unsafe: [],
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export default async function publish(props: Props): Promise<void> {
},
durable_objects: config.durable_objects,
r2_buckets: config.r2_buckets,
services: config.services,
unsafe: config.unsafe?.bindings,
};

Expand Down
7 changes: 7 additions & 0 deletions packages/wrangler/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ interface CfR2Bucket {
bucket_name: string;
}

interface CfService {
binding: string;
service: string;
environment: string;
}

interface CfUnsafeBinding {
name: string;
type: string;
Expand Down Expand Up @@ -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;
Expand Down