Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 7a78784

Browse files
authored
Add support for built-in workerd services (#435)
Specifically, this allows custom `network`, `external` and `disk` services to be specified as service bindings. Requests to `network` services are dispatched according to URL. Requests to `external` services are dispatched to the specified remote server. Requests to `disk` services are dispatched to an HTTP service backed by an on-disk directory. This PR also fixes a bug where custom function service bindings with the same name in different Workers would dispatch all requests to the first Workers' function.
1 parent 339ae67 commit 7a78784

File tree

6 files changed

+146
-36
lines changed

6 files changed

+146
-36
lines changed

packages/tre/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ parameter in module format Workers.
275275
Record mapping binding name to paths containing arbitrary binary data to
276276
inject as `ArrayBuffer` bindings into this Worker.
277277

278-
- `serviceBindings?: Record<string, string | (request: Request) => Awaitable<Response>>`
278+
- `serviceBindings?: Record<string, string | { network: Network } | { external: ExternalServer } | { disk: DiskDirectory } | (request: Request) => Awaitable<Response>>`
279279

280280
Record mapping binding name to service designators to inject as
281281
`{ fetch: typeof fetch }`
@@ -284,10 +284,22 @@ parameter in module format Workers.
284284

285285
- If the designator is a `string`, requests will be dispatched to the Worker
286286
with that `name`.
287+
- If the designator is an object of the form `{ network: { ... } }`, where
288+
`network` is a
289+
[`workerd` `Network` struct](https://github.com/cloudflare/workerd/blob/bdbd6075c7c53948050c52d22f2dfa37bf376253/src/workerd/server/workerd.capnp#L555-L598),
290+
requests will be dispatched according to the `fetch`ed URL.
291+
- If the designator is an object of the form `{ external: { ... } }` where
292+
`external` is a
293+
[`workerd` `ExternalServer` struct](https://github.com/cloudflare/workerd/blob/bdbd6075c7c53948050c52d22f2dfa37bf376253/src/workerd/server/workerd.capnp#L504-L553),
294+
requests will be dispatched to the specified remote server.
295+
- If the designator is an object of the form `{ disk: { ... } }` where `disk`
296+
is a
297+
[`workerd` `DiskDirectory` struct](https://github.com/cloudflare/workerd/blob/bdbd6075c7c53948050c52d22f2dfa37bf376253/src/workerd/server/workerd.capnp#L600-L643),
298+
requests will be dispatched to an HTTP service backed by an on-disk
299+
directory.
287300
- If the designator is a function, requests will be dispatched to your custom
288301
handler. This allows you to access data and functions defined in Node.js
289302
from your Worker.
290-
<!--TODO: other service types, disk, network, external, etc-->
291303

292304
#### Cache
293305

packages/tre/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ export class Miniflare {
478478
const workerBindings: Worker_Binding[] = [];
479479
const additionalModules: Worker_Module[] = [];
480480
for (const [key, plugin] of PLUGIN_ENTRIES) {
481-
const pluginBindings = await plugin.getBindings(workerOpts[key]);
481+
const pluginBindings = await plugin.getBindings(workerOpts[key], i);
482482
if (pluginBindings !== undefined) {
483483
workerBindings.push(...pluginBindings);
484484

packages/tre/src/plugins/core/index.ts

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { readFileSync } from "fs";
22
import fs from "fs/promises";
33
import { TextEncoder } from "util";
44
import { bold } from "kleur/colors";
5-
import { Request, Response } from "undici";
65
import { z } from "zod";
76
import {
87
Service,
@@ -11,13 +10,7 @@ import {
1110
kVoid,
1211
supportedCompatibilityDate,
1312
} from "../../runtime";
14-
import {
15-
Awaitable,
16-
JsonSchema,
17-
Log,
18-
MiniflareCoreError,
19-
zAwaitable,
20-
} from "../../shared";
13+
import { Awaitable, JsonSchema, Log, MiniflareCoreError } from "../../shared";
2114
import { getCacheServiceName } from "../cache";
2215
import {
2316
BINDING_SERVICE_LOOPBACK,
@@ -31,15 +24,11 @@ import {
3124
STRING_SCRIPT_PATH,
3225
convertModuleDefinition,
3326
} from "./modules";
27+
import { ServiceDesignatorSchema } from "./services";
3428

3529
const encoder = new TextEncoder();
3630
const numericCompare = new Intl.Collator(undefined, { numeric: true }).compare;
3731

38-
// (request: Request) => Awaitable<Response>
39-
export const ServiceFetchSchema = z
40-
.function()
41-
.args(z.instanceof(Request))
42-
.returns(zAwaitable(z.instanceof(Response)));
4332
export const CoreOptionsSchema = z.object({
4433
name: z.string().optional(),
4534
script: z.string().optional(),
@@ -62,10 +51,7 @@ export const CoreOptionsSchema = z.object({
6251
wasmBindings: z.record(z.string()).optional(),
6352
textBlobBindings: z.record(z.string()).optional(),
6453
dataBlobBindings: z.record(z.string()).optional(),
65-
// TODO: add support for workerd network/external/disk services here
66-
serviceBindings: z
67-
.record(z.union([z.string(), ServiceFetchSchema]))
68-
.optional(),
54+
serviceBindings: z.record(ServiceDesignatorSchema).optional(),
6955
});
7056

7157
export const CoreSharedOptionsSchema = z.object({
@@ -92,11 +78,19 @@ export const SERVICE_LOOPBACK = `${CORE_PLUGIN_NAME}:loopback`;
9278
export const SERVICE_ENTRY = `${CORE_PLUGIN_NAME}:entry`;
9379
// Service prefix for all regular user workers
9480
const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`;
81+
// Service prefix for `workerd`'s builtin services (network, external, disk)
82+
const SERVICE_BUILTIN_PREFIX = `${CORE_PLUGIN_NAME}:builtin`;
9583
// Service prefix for custom fetch functions defined in `serviceBindings` option
9684
const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`;
9785

98-
export function getUserServiceName(name = "") {
99-
return `${SERVICE_USER_PREFIX}:${name}`;
86+
export function getUserServiceName(workerName = "") {
87+
return `${SERVICE_USER_PREFIX}:${workerName}`;
88+
}
89+
function getBuiltinServiceName(workerIndex: number, bindingName: string) {
90+
return `${SERVICE_BUILTIN_PREFIX}:${workerIndex}:${bindingName}`;
91+
}
92+
function getCustomServiceName(workerIndex: number, bindingName: string) {
93+
return `${SERVICE_CUSTOM_PREFIX}:${workerIndex}:${bindingName}`;
10094
}
10195

10296
export const HEADER_PROBE = "MF-Probe";
@@ -235,7 +229,7 @@ export const CORE_PLUGIN: Plugin<
235229
> = {
236230
options: CoreOptionsSchema,
237231
sharedOptions: CoreSharedOptionsSchema,
238-
getBindings(options) {
232+
getBindings(options, workerIndex) {
239233
const bindings: Awaitable<Worker_Binding>[] = [];
240234

241235
if (options.bindings !== undefined) {
@@ -269,15 +263,23 @@ export const CORE_PLUGIN: Plugin<
269263
}
270264
if (options.serviceBindings !== undefined) {
271265
bindings.push(
272-
...Object.entries(options.serviceBindings).map(([name, service]) => ({
273-
name,
274-
service: {
275-
name:
276-
typeof service === "function"
277-
? `${SERVICE_CUSTOM_PREFIX}:${name}` // Custom `fetch` function
278-
: `${SERVICE_USER_PREFIX}:${service}`, // Regular user worker
279-
},
280-
}))
266+
...Object.entries(options.serviceBindings).map(([name, service]) => {
267+
let serviceName: string;
268+
if (typeof service === "function") {
269+
// Custom `fetch` function
270+
serviceName = getCustomServiceName(workerIndex, name);
271+
} else if (typeof service === "object") {
272+
// Builtin workerd service: network, external, disk
273+
serviceName = getBuiltinServiceName(workerIndex, name);
274+
} else {
275+
// Regular user worker
276+
serviceName = getUserServiceName(service);
277+
}
278+
return {
279+
name: name,
280+
service: { name: serviceName },
281+
};
282+
})
281283
);
282284
}
283285

@@ -372,8 +374,9 @@ export const CORE_PLUGIN: Plugin<
372374
if (options.serviceBindings !== undefined) {
373375
for (const [name, service] of Object.entries(options.serviceBindings)) {
374376
if (typeof service === "function") {
377+
// Custom `fetch` function
375378
services.push({
376-
name: `${SERVICE_CUSTOM_PREFIX}:${name}`,
379+
name: getCustomServiceName(workerIndex, name),
377380
worker: {
378381
serviceWorkerScript: SCRIPT_CUSTOM_SERVICE,
379382
compatibilityDate: "2022-09-01",
@@ -389,6 +392,12 @@ export const CORE_PLUGIN: Plugin<
389392
],
390393
},
391394
});
395+
} else if (typeof service === "object") {
396+
// Builtin workerd service: network, external, disk
397+
services.push({
398+
name: getBuiltinServiceName(workerIndex, name),
399+
...service,
400+
});
392401
}
393402
}
394403
}
@@ -430,3 +439,5 @@ function getWorkerScript(
430439
return { serviceWorkerScript: code };
431440
}
432441
}
442+
443+
export * from "./services";
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Request, Response } from "undici";
2+
import { z } from "zod";
3+
import {
4+
ExternalServer,
5+
HttpOptions_Style,
6+
TlsOptions_Version,
7+
} from "../../runtime";
8+
import { zAwaitable } from "../../shared";
9+
10+
// Zod validators for types in runtime/config/workerd.ts.
11+
// All options should be optional except where specifically stated.
12+
// TODO: autogenerate these with runtime/config/workerd.ts from capnp
13+
14+
export const HttpOptionsHeaderSchema = z.object({
15+
name: z.string(), // name should be required
16+
value: z.ostring(), // If omitted, the header will be removed
17+
});
18+
const HttpOptionsSchema = z.object({
19+
style: z.nativeEnum(HttpOptions_Style).optional(),
20+
forwardedProtoHeader: z.ostring(),
21+
cfBlobHeader: z.ostring(),
22+
injectRequestHeaders: HttpOptionsHeaderSchema.array().optional(),
23+
injectResponseHeaders: HttpOptionsHeaderSchema.array().optional(),
24+
});
25+
26+
const TlsOptionsKeypairSchema = z.object({
27+
privateKey: z.ostring(),
28+
certificateChain: z.ostring(),
29+
});
30+
31+
const TlsOptionsSchema = z.object({
32+
keypair: TlsOptionsKeypairSchema.optional(),
33+
requireClientCerts: z.oboolean(),
34+
trustBrowserCas: z.oboolean(),
35+
trustedCertificates: z.string().array().optional(),
36+
minVersion: z.nativeEnum(TlsOptions_Version).optional(),
37+
cipherList: z.ostring(),
38+
});
39+
40+
const NetworkSchema = z.object({
41+
allow: z.string().array().optional(),
42+
deny: z.string().array().optional(),
43+
tlsOptions: TlsOptionsSchema.optional(),
44+
});
45+
46+
export const ExternalServerSchema = z.intersection(
47+
z.object({ address: z.string() }), // address should be required
48+
z.union([
49+
z.object({ http: z.optional(HttpOptionsSchema) }),
50+
z.object({
51+
https: z.optional(
52+
z.object({
53+
options: HttpOptionsSchema.optional(),
54+
tlsOptions: TlsOptionsSchema.optional(),
55+
certificateHost: z.ostring(),
56+
})
57+
),
58+
}),
59+
])
60+
) as z.ZodType<ExternalServer>;
61+
// This type cast is required for `api-extractor` to produce a `.d.ts` rollup.
62+
// Rather than outputting a `z.ZodIntersection<...>` for this type, it will
63+
// just use `z.ZodType<ExternalServer>`. Without this, the extractor process
64+
// just ends up pinned at 100% CPU. Probably unbounded recursion? I guess this
65+
// type is too complex? Something to investigate... :thinking_face:
66+
67+
const DiskDirectorySchema = z.object({
68+
path: z.string(), // path should be required
69+
writable: z.oboolean(),
70+
});
71+
72+
export const ServiceFetchSchema = z
73+
.function()
74+
.args(z.instanceof(Request))
75+
.returns(zAwaitable(z.instanceof(Response)));
76+
77+
export const ServiceDesignatorSchema = z.union([
78+
z.string(),
79+
z.object({ network: NetworkSchema }),
80+
z.object({ external: ExternalServerSchema }),
81+
z.object({ disk: DiskDirectorySchema }),
82+
ServiceFetchSchema,
83+
]);

packages/tre/src/plugins/shared/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export interface PluginBase<
2626
SharedOptions extends z.ZodType | undefined
2727
> {
2828
options: Options;
29-
getBindings(options: z.infer<Options>): Awaitable<Worker_Binding[] | void>;
29+
getBindings(
30+
options: z.infer<Options>,
31+
workerIndex: number
32+
): Awaitable<Worker_Binding[] | void>;
3033
getServices(
3134
options: PluginServicesOptions<Options, SharedOptions>
3235
): Awaitable<Service[] | void>;

packages/tre/src/runtime/config/workerd.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,10 @@ export type Worker_DurableObjectNamespace = { className?: string } & (
132132
| { ephemeralLocal?: Void }
133133
);
134134

135-
export type ExternalServer =
135+
export type ExternalServer = { address?: string } & (
136136
| { http: HttpOptions }
137-
| { https: ExternalServer_Https };
137+
| { https: ExternalServer_Https }
138+
);
138139

139140
export interface ExternalServer_Https {
140141
options?: HttpOptions;

0 commit comments

Comments
 (0)