Skip to content

Commit 48d83ca

Browse files
RiscadoARicardo Antunes
andauthored
[wrangler] Add vpc_networks binding support (#12992)
Co-authored-by: Ricardo Antunes <rantunes@cloudflare.com>
1 parent da39573 commit 48d83ca

File tree

27 files changed

+784
-15
lines changed

27 files changed

+784
-15
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Fix remote proxy worker not catching errors thrown by bindings during `wrangler dev`

.changeset/vpc-networks-binding.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"wrangler": minor
3+
"miniflare": minor
4+
"@cloudflare/workers-utils": minor
5+
---
6+
7+
Add `vpc_networks` binding support for routing Worker traffic through a Cloudflare Tunnel or network.
8+
9+
```jsonc
10+
{
11+
"vpc_networks": [
12+
// Route through a specific Cloudflare Tunnel
13+
{ "binding": "MY_FIRST_VPC", "tunnel_id": "<tunnel-id>" },
14+
// Route through the Cloudflare One mesh network
15+
{ "binding": "MY_SECOND_VPC", "network_id": "cf1:network" },
16+
],
17+
}
18+
```

packages/miniflare/src/plugins/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
VERSION_METADATA_PLUGIN,
3939
VERSION_METADATA_PLUGIN_NAME,
4040
} from "./version-metadata";
41+
import { VPC_NETWORKS_PLUGIN, VPC_NETWORKS_PLUGIN_NAME } from "./vpc-networks";
4142
import { VPC_SERVICES_PLUGIN, VPC_SERVICES_PLUGIN_NAME } from "./vpc-services";
4243
import {
4344
WORKER_LOADER_PLUGIN,
@@ -68,6 +69,7 @@ export const PLUGINS = {
6869
[IMAGES_PLUGIN_NAME]: IMAGES_PLUGIN,
6970
[STREAM_PLUGIN_NAME]: STREAM_PLUGIN,
7071
[VECTORIZE_PLUGIN_NAME]: VECTORIZE_PLUGIN,
72+
[VPC_NETWORKS_PLUGIN_NAME]: VPC_NETWORKS_PLUGIN,
7173
[VPC_SERVICES_PLUGIN_NAME]: VPC_SERVICES_PLUGIN,
7274
[MTLS_PLUGIN_NAME]: MTLS_PLUGIN,
7375
[HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN,
@@ -134,6 +136,7 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
134136
z.input<typeof IMAGES_PLUGIN.options> &
135137
z.input<typeof STREAM_PLUGIN.options> &
136138
z.input<typeof VECTORIZE_PLUGIN.options> &
139+
z.input<typeof VPC_NETWORKS_PLUGIN.options> &
137140
z.input<typeof VPC_SERVICES_PLUGIN.options> &
138141
z.input<typeof MTLS_PLUGIN.options> &
139142
z.input<typeof HELLO_WORLD_PLUGIN.options> &
@@ -214,6 +217,7 @@ export * from "./dispatch-namespace";
214217
export * from "./images";
215218
export * from "./stream";
216219
export * from "./vectorize";
220+
export * from "./vpc-networks";
217221
export * from "./vpc-services";
218222
export * from "./mtls";
219223
export * from "./hello-world";
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { z } from "zod";
2+
import {
3+
getUserBindingServiceName,
4+
Plugin,
5+
ProxyNodeBinding,
6+
remoteProxyClientWorker,
7+
RemoteProxyConnectionString,
8+
} from "../shared";
9+
10+
const VpcNetworksSchema = z.union([
11+
z.object({
12+
tunnel_id: z.string(),
13+
remoteProxyConnectionString: z
14+
.custom<RemoteProxyConnectionString>()
15+
.optional(),
16+
}),
17+
z.object({
18+
network_id: z.string(),
19+
remoteProxyConnectionString: z
20+
.custom<RemoteProxyConnectionString>()
21+
.optional(),
22+
}),
23+
]);
24+
25+
export const VpcNetworksOptionsSchema = z.object({
26+
vpcNetworks: z.record(VpcNetworksSchema).optional(),
27+
});
28+
29+
export const VPC_NETWORKS_PLUGIN_NAME = "vpc-networks";
30+
31+
export const VPC_NETWORKS_PLUGIN: Plugin<typeof VpcNetworksOptionsSchema> = {
32+
options: VpcNetworksOptionsSchema,
33+
async getBindings(options) {
34+
if (!options.vpcNetworks) {
35+
return [];
36+
}
37+
38+
return Object.entries(options.vpcNetworks).map(([name, binding]) => {
39+
const identifier =
40+
"tunnel_id" in binding ? binding.tunnel_id : binding.network_id;
41+
return {
42+
name,
43+
44+
service: {
45+
name: getUserBindingServiceName(
46+
VPC_NETWORKS_PLUGIN_NAME,
47+
identifier,
48+
binding.remoteProxyConnectionString
49+
),
50+
},
51+
};
52+
});
53+
},
54+
getNodeBindings(options: z.infer<typeof VpcNetworksOptionsSchema>) {
55+
if (!options.vpcNetworks) {
56+
return {};
57+
}
58+
return Object.fromEntries(
59+
Object.keys(options.vpcNetworks).map((name) => [
60+
name,
61+
new ProxyNodeBinding(),
62+
])
63+
);
64+
},
65+
async getServices({ options }) {
66+
if (!options.vpcNetworks) {
67+
return [];
68+
}
69+
70+
return Object.entries(options.vpcNetworks).map(([name, binding]) => {
71+
const identifier =
72+
"tunnel_id" in binding ? binding.tunnel_id : binding.network_id;
73+
return {
74+
name: getUserBindingServiceName(
75+
VPC_NETWORKS_PLUGIN_NAME,
76+
identifier,
77+
binding.remoteProxyConnectionString
78+
),
79+
worker: remoteProxyClientWorker(
80+
binding.remoteProxyConnectionString,
81+
name
82+
),
83+
};
84+
});
85+
},
86+
};

packages/workers-utils/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,5 @@ export const defaultWranglerConfig: Config = {
408408
streaming_tail_consumers: undefined,
409409
pipelines: [],
410410
vpc_services: [],
411+
vpc_networks: [],
411412
};

packages/workers-utils/src/config/environment.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,34 @@ export interface EnvironmentNonInheritable {
13901390
/** Whether the VPC service is remote or not */
13911391
remote?: boolean;
13921392
}[];
1393+
1394+
/**
1395+
* Specifies VPC networks that are bound to this Worker environment.
1396+
*
1397+
* NOTE: This field is not automatically inherited from the top level environment,
1398+
* and so must be specified in every named environment.
1399+
*
1400+
* @default []
1401+
* @nonInheritable
1402+
*/
1403+
vpc_networks: (
1404+
| {
1405+
/** The binding name used to refer to the VPC network in the Worker. */
1406+
binding: string;
1407+
/** The tunnel ID of the Cloudflare Tunnel to route traffic through. Mutually exclusive with network_id. */
1408+
tunnel_id: string;
1409+
/** Whether the VPC network is remote or not */
1410+
remote?: boolean;
1411+
}
1412+
| {
1413+
/** The binding name used to refer to the VPC network in the Worker. */
1414+
binding: string;
1415+
/** The network ID to route traffic through. Mutually exclusive with tunnel_id. */
1416+
network_id: string;
1417+
/** Whether the VPC network is remote or not */
1418+
remote?: boolean;
1419+
}
1420+
)[];
13931421
}
13941422

13951423
/**

packages/workers-utils/src/config/validation.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ export type ConfigBindingFieldName =
105105
| "assets"
106106
| "unsafe_hello_world"
107107
| "worker_loaders"
108-
| "vpc_services";
108+
| "vpc_services"
109+
| "vpc_networks";
109110

110111
/**
111112
* @deprecated new code should use getBindingTypeFriendlyName() instead
@@ -145,6 +146,7 @@ export const friendlyBindingNames: Record<ConfigBindingFieldName, string> = {
145146
unsafe_hello_world: "Hello World",
146147
worker_loaders: "Worker Loader",
147148
vpc_services: "VPC Service",
149+
vpc_networks: "VPC Network",
148150
} as const;
149151

150152
/**
@@ -187,6 +189,7 @@ const bindingTypeFriendlyNames: Record<Binding["type"], string> = {
187189
ratelimit: "Rate Limit",
188190
worker_loader: "Worker Loader",
189191
vpc_service: "VPC Service",
192+
vpc_network: "VPC Network",
190193
media: "Media",
191194
assets: "Assets",
192195
inherit: "Inherited",
@@ -1911,6 +1914,16 @@ function normalizeAndValidateEnvironment(
19111914
validateBindingArray(envName, validateVpcServiceBinding),
19121915
[]
19131916
),
1917+
vpc_networks: notInheritable(
1918+
diagnostics,
1919+
topLevelEnv,
1920+
rawConfig,
1921+
rawEnv,
1922+
envName,
1923+
"vpc_networks",
1924+
validateBindingArray(envName, validateVpcNetworkBinding),
1925+
[]
1926+
),
19141927
version_metadata: notInheritable(
19151928
diagnostics,
19161929
topLevelEnv,
@@ -2958,6 +2971,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
29582971
"pipeline",
29592972
"worker_loader",
29602973
"vpc_service",
2974+
"vpc_network",
29612975
"stream",
29622976
"media",
29632977
];
@@ -4093,6 +4107,65 @@ const validateVpcServiceBinding: ValidatorFn = (diagnostics, field, value) => {
40934107
return isValid;
40944108
};
40954109

4110+
const validateVpcNetworkBinding: ValidatorFn = (diagnostics, field, value) => {
4111+
if (typeof value !== "object" || value === null) {
4112+
diagnostics.errors.push(
4113+
`"vpc_networks" bindings should be objects, but got ${JSON.stringify(
4114+
value
4115+
)}`
4116+
);
4117+
return false;
4118+
}
4119+
let isValid = true;
4120+
// VPC network bindings must have a binding and exactly one of tunnel_id or network_id.
4121+
if (!isRequiredProperty(value, "binding", "string")) {
4122+
diagnostics.errors.push(
4123+
`"${field}" bindings should have a string "binding" field but got ${JSON.stringify(
4124+
value
4125+
)}.`
4126+
);
4127+
isValid = false;
4128+
}
4129+
const hasTunnelId = hasProperty(value, "tunnel_id");
4130+
const hasNetworkId = hasProperty(value, "network_id");
4131+
if (hasTunnelId && hasNetworkId) {
4132+
diagnostics.errors.push(
4133+
`"${field}" bindings must have either a "tunnel_id" or "network_id", but not both.`
4134+
);
4135+
isValid = false;
4136+
} else if (!hasTunnelId && !hasNetworkId) {
4137+
diagnostics.errors.push(
4138+
`"${field}" bindings must have either a "tunnel_id" or "network_id" field but got ${JSON.stringify(
4139+
value
4140+
)}.`
4141+
);
4142+
isValid = false;
4143+
} else if (hasTunnelId && typeof value.tunnel_id !== "string") {
4144+
diagnostics.errors.push(
4145+
`"${field}" bindings must have a string "tunnel_id" field but got ${JSON.stringify(
4146+
value
4147+
)}.`
4148+
);
4149+
isValid = false;
4150+
} else if (hasNetworkId && typeof value.network_id !== "string") {
4151+
diagnostics.errors.push(
4152+
`"${field}" bindings must have a string "network_id" field but got ${JSON.stringify(
4153+
value
4154+
)}.`
4155+
);
4156+
isValid = false;
4157+
}
4158+
4159+
validateAdditionalProperties(diagnostics, field, Object.keys(value), [
4160+
"binding",
4161+
"tunnel_id",
4162+
"network_id",
4163+
"remote",
4164+
]);
4165+
4166+
return isValid;
4167+
};
4168+
40964169
/**
40974170
* Check that bindings whose names might conflict, don't.
40984171
*

packages/workers-utils/src/map-worker-metadata-bindings.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,21 @@ export function mapWorkerMetadataBindings(
368368
];
369369
}
370370
break;
371+
case "vpc_network":
372+
{
373+
if (binding.tunnel_id !== undefined) {
374+
configObj.vpc_networks = [
375+
...(configObj.vpc_networks ?? []),
376+
{ binding: binding.name, tunnel_id: binding.tunnel_id },
377+
];
378+
} else if (binding.network_id !== undefined) {
379+
configObj.vpc_networks = [
380+
...(configObj.vpc_networks ?? []),
381+
{ binding: binding.name, network_id: binding.network_id },
382+
];
383+
}
384+
}
385+
break;
371386
default: {
372387
configObj.unsafe = {
373388
bindings: [...(configObj.unsafe?.bindings ?? []), binding],

packages/workers-utils/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type {
3636
CfUnsafeBinding,
3737
CfUserLimits,
3838
CfVectorize,
39+
CfVpcNetwork,
3940
CfVpcService,
4041
CfWorkerLoader,
4142
CfWorkflow,
@@ -165,6 +166,12 @@ export type WorkerMetadataBinding =
165166
simple: { limit: number; period: 10 | 60 };
166167
}
167168
| { type: "vpc_service"; name: string; service_id: string }
169+
| {
170+
type: "vpc_network";
171+
name: string;
172+
tunnel_id?: string;
173+
network_id?: string;
174+
}
168175
| {
169176
type: "worker_loader";
170177
name: string;
@@ -328,6 +335,7 @@ export type Binding =
328335
| ({ type: "ratelimit" } & NameOmit<CfRateLimit>)
329336
| ({ type: "worker_loader" } & BindingOmit<CfWorkerLoader>)
330337
| ({ type: "vpc_service" } & BindingOmit<CfVpcService>)
338+
| ({ type: "vpc_network" } & BindingOmit<CfVpcNetwork>)
331339
| ({ type: "media" } & BindingOmit<CfMediaBinding>)
332340
| ({ type: `unsafe_${string}` } & Omit<CfUnsafeBinding, "name" | "type">)
333341
| { type: "assets" }

packages/workers-utils/src/worker.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ export interface CfVpcService {
288288
remote?: boolean;
289289
}
290290

291+
export interface CfVpcNetwork {
292+
binding: string;
293+
tunnel_id?: string;
294+
network_id?: string;
295+
remote?: boolean;
296+
}
297+
291298
export interface CfAnalyticsEngineDataset {
292299
binding: string;
293300
dataset?: string;

0 commit comments

Comments
 (0)