diff --git a/.changeset/vpc-networks-binding.md b/.changeset/vpc-networks-binding.md new file mode 100644 index 000000000000..a60d5907ddcb --- /dev/null +++ b/.changeset/vpc-networks-binding.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +"miniflare": minor +"@cloudflare/workers-utils": minor +--- + +Add `vpc_networks` binding support for routing Worker traffic through a Cloudflare Tunnel or network diff --git a/packages/miniflare/src/plugins/index.ts b/packages/miniflare/src/plugins/index.ts index 9cc332befa2f..9d101e2ae4b9 100644 --- a/packages/miniflare/src/plugins/index.ts +++ b/packages/miniflare/src/plugins/index.ts @@ -37,6 +37,7 @@ import { VERSION_METADATA_PLUGIN, VERSION_METADATA_PLUGIN_NAME, } from "./version-metadata"; +import { VPC_NETWORKS_PLUGIN, VPC_NETWORKS_PLUGIN_NAME } from "./vpc-networks"; import { VPC_SERVICES_PLUGIN, VPC_SERVICES_PLUGIN_NAME } from "./vpc-services"; import { WORKER_LOADER_PLUGIN, @@ -66,6 +67,7 @@ export const PLUGINS = { [IMAGES_PLUGIN_NAME]: IMAGES_PLUGIN, [STREAM_PLUGIN_NAME]: STREAM_PLUGIN, [VECTORIZE_PLUGIN_NAME]: VECTORIZE_PLUGIN, + [VPC_NETWORKS_PLUGIN_NAME]: VPC_NETWORKS_PLUGIN, [VPC_SERVICES_PLUGIN_NAME]: VPC_SERVICES_PLUGIN, [MTLS_PLUGIN_NAME]: MTLS_PLUGIN, [HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN, @@ -131,6 +133,7 @@ export type WorkerOptions = z.input & z.input & z.input & z.input & + z.input & z.input & z.input & z.input & @@ -212,6 +215,7 @@ export * from "./dispatch-namespace"; export * from "./images"; export * from "./stream"; export * from "./vectorize"; +export * from "./vpc-networks"; export * from "./vpc-services"; export * from "./mtls"; export * from "./hello-world"; diff --git a/packages/miniflare/src/plugins/vpc-networks/index.ts b/packages/miniflare/src/plugins/vpc-networks/index.ts new file mode 100644 index 000000000000..b4f3c33a7f1d --- /dev/null +++ b/packages/miniflare/src/plugins/vpc-networks/index.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; +import { + getUserBindingServiceName, + Plugin, + ProxyNodeBinding, + remoteProxyClientWorker, + RemoteProxyConnectionString, +} from "../shared"; + +const VpcNetworksSchema = z.object({ + tunnel_id: z.string().optional(), + network_id: z.string().optional(), + remoteProxyConnectionString: z + .custom() + .optional(), +}); + +export const VpcNetworksOptionsSchema = z.object({ + vpcNetworks: z.record(VpcNetworksSchema).optional(), +}); + +export const VPC_NETWORKS_PLUGIN_NAME = "vpc-networks"; + +export const VPC_NETWORKS_PLUGIN: Plugin = { + options: VpcNetworksOptionsSchema, + async getBindings(options) { + if (!options.vpcNetworks) { + return []; + } + + return Object.entries(options.vpcNetworks).map( + ([name, { tunnel_id, network_id, remoteProxyConnectionString }]) => { + const identifier = tunnel_id ?? network_id; + if (!identifier) { + throw new Error( + `vpc_networks binding "${name}" must have either tunnel_id or network_id` + ); + } + return { + name, + + service: { + name: getUserBindingServiceName( + VPC_NETWORKS_PLUGIN_NAME, + identifier, + remoteProxyConnectionString + ), + }, + }; + } + ); + }, + getNodeBindings(options: z.infer) { + if (!options.vpcNetworks) { + return {}; + } + return Object.fromEntries( + Object.keys(options.vpcNetworks).map((name) => [ + name, + new ProxyNodeBinding(), + ]) + ); + }, + async getServices({ options }) { + if (!options.vpcNetworks) { + return []; + } + + return Object.entries(options.vpcNetworks).map( + ([name, { tunnel_id, network_id, remoteProxyConnectionString }]) => { + const identifier = tunnel_id ?? network_id; + if (!identifier) { + throw new Error( + `vpc_networks binding "${name}" must have either tunnel_id or network_id` + ); + } + return { + name: getUserBindingServiceName( + VPC_NETWORKS_PLUGIN_NAME, + identifier, + remoteProxyConnectionString + ), + worker: remoteProxyClientWorker(remoteProxyConnectionString, name), + }; + } + ); + }, +}; diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index 90bbfa23d249..098e45964fb9 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -406,4 +406,5 @@ export const defaultWranglerConfig: Config = { streaming_tail_consumers: undefined, pipelines: [], vpc_services: [], + vpc_networks: [], }; diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 4836d7e9a60c..d385bea12941 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -1352,6 +1352,26 @@ export interface EnvironmentNonInheritable { /** Whether the VPC service is remote or not */ remote?: boolean; }[]; + + /** + * Specifies VPC networks 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 + */ + vpc_networks: { + /** The binding name used to refer to the VPC network in the Worker. */ + binding: string; + /** The tunnel ID of the Cloudflare Tunnel to route traffic through. Mutually exclusive with network_id. */ + tunnel_id?: string; + /** The network ID to route traffic through. Mutually exclusive with tunnel_id. */ + network_id?: string; + /** Whether the VPC network is remote or not */ + remote?: boolean; + }[]; } /** diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 19997250de01..12b21e3f3882 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -103,7 +103,8 @@ export type ConfigBindingFieldName = | "assets" | "unsafe_hello_world" | "worker_loaders" - | "vpc_services"; + | "vpc_services" + | "vpc_networks"; /** * @deprecated new code should use getBindingTypeFriendlyName() instead @@ -141,6 +142,7 @@ export const friendlyBindingNames: Record = { unsafe_hello_world: "Hello World", worker_loaders: "Worker Loader", vpc_services: "VPC Service", + vpc_networks: "VPC Network", } as const; /** @@ -181,6 +183,7 @@ const bindingTypeFriendlyNames: Record = { ratelimit: "Rate Limit", worker_loader: "Worker Loader", vpc_service: "VPC Service", + vpc_network: "VPC Network", media: "Media", assets: "Assets", inherit: "Inherited", @@ -1885,6 +1888,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateVpcServiceBinding), [] ), + vpc_networks: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "vpc_networks", + validateBindingArray(envName, validateVpcNetworkBinding), + [] + ), version_metadata: notInheritable( diagnostics, topLevelEnv, @@ -2930,6 +2943,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "pipeline", "worker_loader", "vpc_service", + "vpc_network", "stream", "media", ]; @@ -3994,6 +4008,65 @@ const validateVpcServiceBinding: ValidatorFn = (diagnostics, field, value) => { return isValid; }; +const validateVpcNetworkBinding: ValidatorFn = (diagnostics, field, value) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"vpc_networks" bindings should be objects, but got ${JSON.stringify( + value + )}` + ); + return false; + } + let isValid = true; + // VPC network bindings must have a binding and exactly one of tunnel_id or network_id. + if (!isRequiredProperty(value, "binding", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "binding" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + const hasTunnelId = hasProperty(value, "tunnel_id"); + const hasNetworkId = hasProperty(value, "network_id"); + if (hasTunnelId && hasNetworkId) { + diagnostics.errors.push( + `"${field}" bindings must have either "tunnel_id" or "network_id", but not both.` + ); + isValid = false; + } else if (!hasTunnelId && !hasNetworkId) { + diagnostics.errors.push( + `"${field}" bindings must have either a "tunnel_id" or "network_id" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } else if (hasTunnelId && typeof value.tunnel_id !== "string") { + diagnostics.errors.push( + `"${field}" bindings must have a string "tunnel_id" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } else if (hasNetworkId && typeof value.network_id !== "string") { + diagnostics.errors.push( + `"${field}" bindings must have a string "network_id" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + "binding", + "tunnel_id", + "network_id", + "remote", + ]); + + return isValid; +}; + /** * Check that bindings whose names might conflict, don't. * diff --git a/packages/workers-utils/src/map-worker-metadata-bindings.ts b/packages/workers-utils/src/map-worker-metadata-bindings.ts index ce3682c728f2..0c9818a84e02 100644 --- a/packages/workers-utils/src/map-worker-metadata-bindings.ts +++ b/packages/workers-utils/src/map-worker-metadata-bindings.ts @@ -350,6 +350,19 @@ export function mapWorkerMetadataBindings( ]; } break; + case "vpc_network": + { + configObj.vpc_networks = [ + ...(configObj.vpc_networks ?? []), + { + binding: binding.name, + ...(binding.tunnel_id + ? { tunnel_id: binding.tunnel_id } + : { network_id: binding.network_id }), + }, + ]; + } + break; default: { configObj.unsafe = { bindings: [...(configObj.unsafe?.bindings ?? []), binding], diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index ec96a43bad03..4ec3ca86b5cf 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -34,6 +34,7 @@ import type { CfUnsafeBinding, CfUserLimits, CfVectorize, + CfVpcNetwork, CfVpcService, CfWorkerLoader, CfWorkflow, @@ -161,6 +162,12 @@ export type WorkerMetadataBinding = simple: { limit: number; period: 10 | 60 }; } | { type: "vpc_service"; name: string; service_id: string } + | { + type: "vpc_network"; + name: string; + tunnel_id?: string; + network_id?: string; + } | { type: "worker_loader"; name: string; @@ -322,6 +329,7 @@ export type Binding = | ({ type: "ratelimit" } & NameOmit) | ({ type: "worker_loader" } & BindingOmit) | ({ type: "vpc_service" } & BindingOmit) + | ({ type: "vpc_network" } & BindingOmit) | ({ type: "media" } & BindingOmit) | ({ type: `unsafe_${string}` } & Omit) | { type: "assets" } diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index e0306908c9d2..a31298d2e7a7 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -276,6 +276,13 @@ export interface CfVpcService { remote?: boolean; } +export interface CfVpcNetwork { + binding: string; + tunnel_id?: string; + network_id?: string; + remote?: boolean; +} + export interface CfAnalyticsEngineDataset { binding: string; dataset?: string; diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index d3d6375953ff..66d754150aa3 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -75,6 +75,7 @@ describe("normalizeAndValidateConfig()", () => { unsafe_hello_world: [], ratelimits: [], vpc_services: [], + vpc_networks: [], services: [], analytics_engine_datasets: [], route: undefined, @@ -4667,6 +4668,120 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[vpc_networks]", () => { + it("should accept valid bindings with tunnel_id", ({ expect }) => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + vpc_networks: [ + { + binding: "MY_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "MY_OTHER_NETWORK", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + } as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(config.vpc_networks).toEqual([ + { + binding: "MY_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "MY_OTHER_NETWORK", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ]); + expect(diagnostics.hasErrors()).toBe(false); + }); + + it("should accept valid bindings with network_id", ({ expect }) => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + vpc_networks: [ + { + binding: "MY_MESH_NETWORK", + network_id: "some-network-id", + }, + ], + } as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(config.vpc_networks).toEqual([ + { + binding: "MY_MESH_NETWORK", + network_id: "some-network-id", + }, + ]); + expect(diagnostics.hasErrors()).toBe(false); + }); + + it("should error if both tunnel_id and network_id are provided", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + vpc_networks: [ + { + binding: "MY_NETWORK", + tunnel_id: "aaa", + network_id: "some-network-id", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "vpc_networks[0]" bindings must have either "tunnel_id" or "network_id", but not both." + `); + }); + + it("should error if vpc_networks bindings are not valid", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + vpc_networks: [ + {}, + { + binding: "VALID", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { binding: null, tunnel_id: 123, invalid: true }, + { binding: "MISSING_ID" }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "vpc_networks[0]" bindings should have a string "binding" field but got {}. + - "vpc_networks[0]" bindings must have either a "tunnel_id" or "network_id" field but got {}. + - "vpc_networks[2]" bindings should have a string "binding" field but got {"binding":null,"tunnel_id":123,"invalid":true}. + - "vpc_networks[2]" bindings must have a string "tunnel_id" field but got {"binding":null,"tunnel_id":123,"invalid":true}. + - "vpc_networks[3]" bindings must have either a "tunnel_id" or "network_id" field but got {"binding":"MISSING_ID"}." + `); + }); + }); + describe("[unsafe.bindings]", () => { it("should error if unsafe is an array", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts index b9d72c49a3c3..3ed77dee18f1 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts @@ -85,6 +85,16 @@ describe("convertConfigBindingsToStartWorkerBindings", () => { service_id: "0199295b-b3ac-7760-8246-bca40877b3e9", }, ], + vpc_networks: [ + { + binding: "MY_VPC_NETWORK", + tunnel_id: "0399295b-b3ac-7760-8246-bca40877b3e1", + }, + { + binding: "MY_MESH_NETWORK", + network_id: "some-network-id", + }, + ], }); expect(result).toEqual({ AI: { @@ -143,6 +153,14 @@ describe("convertConfigBindingsToStartWorkerBindings", () => { service_id: "0199295b-b3ac-7760-8246-bca40877b3e9", type: "vpc_service", }, + MY_VPC_NETWORK: { + tunnel_id: "0399295b-b3ac-7760-8246-bca40877b3e1", + type: "vpc_network", + }, + MY_MESH_NETWORK: { + network_id: "some-network-id", + type: "vpc_network", + }, }); }); diff --git a/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts b/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts index eb3301526667..fb44a09faf7d 100644 --- a/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts +++ b/packages/wrangler/src/__tests__/deploy/durable-objects.test.ts @@ -1213,6 +1213,136 @@ describe("deploy", () => { `); }); }); + describe("vpc_networks", () => { + it("should upload VPC network bindings", async ({ expect }) => { + writeWranglerConfig({ + vpc_networks: [ + { + binding: "VPC_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + ], + }); + await fs.promises.writeFile("index.js", `export default {};`); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "vpc_network", + name: "VPC_NETWORK", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + ], + }); + + await runWrangler("deploy index.js"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your Worker has access to the following bindings: + Binding Resource + env.VPC_NETWORK (0199295b-b3ac-7760-8246-bca40877b3e9) VPC Network + + Uploaded test-name (TIMINGS) + Deployed test-name triggers (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Version ID: Galaxy-Class" + `); + }); + + it("should upload multiple VPC network bindings", async ({ expect }) => { + writeWranglerConfig({ + vpc_networks: [ + { + binding: "VPC_NET_A", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "VPC_NET_B", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + }); + await fs.promises.writeFile("index.js", `export default {};`); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "vpc_network", + name: "VPC_NET_A", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + type: "vpc_network", + name: "VPC_NET_B", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + }); + + await runWrangler("deploy index.js"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your Worker has access to the following bindings: + Binding Resource + env.VPC_NET_A (0199295b-b3ac-7760-8246-bca40877b3e9) VPC Network + env.VPC_NET_B (0299295b-b3ac-7760-8246-bca40877b3e0) VPC Network + + Uploaded test-name (TIMINGS) + Deployed test-name triggers (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Version ID: Galaxy-Class" + `); + }); + + it("should upload VPC network bindings with network_id", async ({ + expect, + }) => { + writeWranglerConfig({ + vpc_networks: [ + { + binding: "VPC_NETWORK", + network_id: "cf1:network", + }, + ], + }); + await fs.promises.writeFile("index.js", `export default {};`); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "vpc_network", + name: "VPC_NETWORK", + network_id: "cf1:network", + }, + ], + }); + + await runWrangler("deploy index.js"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your Worker has access to the following bindings: + Binding Resource + env.VPC_NETWORK (cf1:network) VPC Network + + Uploaded test-name (TIMINGS) + Deployed test-name triggers (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Version ID: Galaxy-Class" + `); + }); + }); describe("mtls_certificates", () => { it("should upload mtls_certificate bindings", async ({ expect }) => { writeWranglerConfig({ diff --git a/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts b/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts index 22867bd4663f..ef9f69b75a13 100644 --- a/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts +++ b/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts @@ -387,6 +387,12 @@ describe("getRemoteConfigsDiff", () => { service_id: "my-vpc", }, ], + vpc_networks: [ + { + binding: "MY_NETWORK", + tunnel_id: "my-tunnel", + }, + ], }, { name: "my-worker-id", @@ -495,6 +501,13 @@ describe("getRemoteConfigsDiff", () => { remote: true, }, ], + vpc_networks: [ + { + binding: "MY_NETWORK", + tunnel_id: "my-tunnel", + remote: true, + }, + ], } as unknown as Config ); expect(diff).toBeNull(); diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index b81b149106cd..d790ff6541c3 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -520,6 +520,7 @@ const bindingsConfigMock: Omit< service_id: "0199295b-b3ac-7760-8246-bca40877b3e9", }, ], + vpc_networks: [], }; describe("generate types", () => { @@ -3301,6 +3302,105 @@ describe("generate types", () => { " `); }); + + it("should generate types for VPC networks bindings", async ({ expect }) => { + fs.writeFileSync( + "./index.ts", + `export default { async fetch(request, env) { return await env.VPC_NET.fetch(request); } };` + ); + fs.writeFileSync( + "./wrangler.json", + JSON.stringify({ + compatibility_date: "2022-01-12", + name: "test-vpc-networks", + main: "./index.ts", + vpc_networks: [ + { + binding: "VPC_NET", + tunnel_id: "0199295b-b3ac-7760-8246-bca40877b3e9", + }, + { + binding: "VPC_NET_B", + tunnel_id: "0299295b-b3ac-7760-8246-bca40877b3e0", + }, + ], + }), + "utf-8" + ); + + await runWrangler("types --include-runtime=false"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env { + VPC_NET: Fetcher; + VPC_NET_B: Fetcher; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.json file. + " + `); + }); + + it("should generate types for VPC networks bindings with network_id", async ({ + expect, + }) => { + fs.writeFileSync( + "./index.ts", + `export default { async fetch(request, env) { return await env.VPC_MESH.fetch(request); } };` + ); + fs.writeFileSync( + "./wrangler.json", + JSON.stringify({ + compatibility_date: "2022-01-12", + name: "test-vpc-networks-mesh", + main: "./index.ts", + vpc_networks: [ + { + binding: "VPC_MESH", + network_id: "cf1:network", + }, + ], + }), + "utf-8" + ); + + await runWrangler("types --include-runtime=false"); + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env { + VPC_MESH: Fetcher; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.json file. + " + `); + }); }); describe("pipeline schema type generation", () => { diff --git a/packages/wrangler/src/api/remoteBindings/index.ts b/packages/wrangler/src/api/remoteBindings/index.ts index a7df42d1a6a9..da0bee651ad8 100644 --- a/packages/wrangler/src/api/remoteBindings/index.ts +++ b/packages/wrangler/src/api/remoteBindings/index.ts @@ -30,6 +30,11 @@ export function pickRemoteBindings( return true; } + if (binding.type === "vpc_network") { + // VPC Network is always remote + return true; + } + return "remote" in binding && binding["remote"]; }) ); diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index 89b5281254cd..1b0731cb61f1 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -361,6 +361,12 @@ export function convertConfigToBindings( } break; } + case "vpc_networks": { + for (const { binding, ...x } of info) { + output[binding] = { type: "vpc_network", ...x }; + } + break; + } case "media": { const { binding, ...x } = info; output[binding] = { type: "media", ...x }; diff --git a/packages/wrangler/src/deploy/check-remote-secrets-override.ts b/packages/wrangler/src/deploy/check-remote-secrets-override.ts index 3ccf8804e3ce..258daf362748 100644 --- a/packages/wrangler/src/deploy/check-remote-secrets-override.ts +++ b/packages/wrangler/src/deploy/check-remote-secrets-override.ts @@ -115,7 +115,8 @@ function extractBindingNames(config: Config): string[] { case "services": case "mtls_certificates": case "dispatch_namespaces": - case "vpc_services": { + case "vpc_services": + case "vpc_networks": { const value: Config[typeof key] = untypedValue; return (value ?? []).map((workflowBinding) => workflowBinding.binding); } diff --git a/packages/wrangler/src/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index c22b6f035227..09144d9b2920 100644 --- a/packages/wrangler/src/deploy/config-diffs.ts +++ b/packages/wrangler/src/deploy/config-diffs.ts @@ -33,6 +33,7 @@ const reorderableBindings = { unsafe_hello_world: true, worker_loaders: true, vpc_services: true, + vpc_networks: true, // Wrapper objects containing binding arrays durable_objects: true, @@ -170,6 +171,12 @@ function removeRemoteConfigFieldFromBindings(normalizedConfig: Config): void { ); } + if (normalizedConfig.vpc_networks?.length) { + normalizedConfig.vpc_networks = normalizedConfig.vpc_networks.map( + ({ remote: _, ...binding }) => binding + ); + } + if (normalizedConfig.workflows?.length) { normalizedConfig.workflows = normalizedConfig.workflows.map( ({ remote: _, ...binding }) => binding diff --git a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts index 3c085e50cb99..032d22559add 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -137,6 +137,7 @@ export function createWorkerUploadForm( ); const ratelimits = extractBindingsOfType("ratelimit", bindings); const vpc_services = extractBindingsOfType("vpc_service", bindings); + const vpc_networks = extractBindingsOfType("vpc_network", bindings); const services = extractBindingsOfType("service", bindings); const analytics_engine_datasets = extractBindingsOfType( "analytics_engine", @@ -365,6 +366,14 @@ export function createWorkerUploadForm( }); }); + vpc_networks.forEach(({ binding, tunnel_id, network_id }) => { + metadataBindings.push({ + name: binding, + type: "vpc_network", + ...(tunnel_id ? { tunnel_id } : { network_id }), + }); + }); + services.forEach( ({ binding, diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 0223e7908175..23cf4211f203 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -415,6 +415,7 @@ type WorkerOptionsBindings = Pick< | "browserRendering" | "vectorize" | "vpcServices" + | "vpcNetworks" | "dispatchNamespaces" | "mtlsCertificates" | "helloWorld" @@ -481,6 +482,7 @@ export function buildMiniflareBindingOptions( const mtlsCertificates = extractBindingsOfType("mtls_certificate", bindings); const vectorizeBindings = extractBindingsOfType("vectorize", bindings); const vpcServices = extractBindingsOfType("vpc_service", bindings); + const vpcNetworks = extractBindingsOfType("vpc_network", bindings); const secretsStoreSecrets = extractBindingsOfType( "secrets_store_secret", bindings @@ -827,6 +829,22 @@ export function buildMiniflareBindingOptions( ]; }) ), + vpcNetworks: Object.fromEntries( + vpcNetworks.map((vpc) => { + warnOrError("vpc_network", vpc.remote, "always-remote"); + return [ + vpc.binding, + { + tunnel_id: vpc.tunnel_id, + network_id: vpc.network_id, + remoteProxyConnectionString: + vpc.remote && remoteProxyConnectionString + ? remoteProxyConnectionString + : undefined, + }, + ]; + }) + ), dispatchNamespaces: Object.fromEntries( dispatchNamespaces.map((dispatchNamespace) => { diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 77874f43fc3a..abca566b8919 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -1853,6 +1853,21 @@ function collectCoreBindings( addBinding(vpcService.binding, "Fetcher", "vpc_services", envName); } + for (const [index, vpcNetwork] of (env.vpc_networks ?? []).entries()) { + if (!vpcNetwork.binding) { + throwMissingBindingError({ + binding: vpcNetwork, + bindingType: "vpc_networks", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + addBinding(vpcNetwork.binding, "Fetcher", "vpc_networks", envName); + } + // Pipelines handled separately for async schema fetching if (env.logfwdr?.bindings?.length) { @@ -2771,6 +2786,25 @@ function collectCoreBindingsPerEnvironment( }); } + for (const [index, vpcNetwork] of (env.vpc_networks ?? []).entries()) { + if (!vpcNetwork.binding) { + throwMissingBindingError({ + binding: vpcNetwork, + bindingType: "vpc_networks", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + bindings.push({ + bindingCategory: "vpc_networks", + name: vpcNetwork.binding, + type: "Fetcher", + }); + } + // Pipelines handled separately for async schema fetching if (env.logfwdr?.bindings?.length) { diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 5df301da18e3..518cdde3477d 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -92,6 +92,7 @@ export function printBindings( ); const services = extractBindingsOfType("service", bindings); const vpc_services = extractBindingsOfType("vpc_service", bindings); + const vpc_networks = extractBindingsOfType("vpc_network", bindings); const analytics_engine_datasets = extractBindingsOfType( "analytics_engine", bindings @@ -353,6 +354,22 @@ export function printBindings( ); } + if (vpc_networks.length > 0) { + output.push( + ...vpc_networks.map(({ binding, tunnel_id, network_id, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("vpc_network"), + value: network_id ?? tunnel_id, + mode: getMode({ + isSimulatedLocally: + remote && !context.remoteBindingsDisabled ? false : undefined, + }), + }; + }) + ); + } + if (r2_buckets.length > 0) { output.push( ...r2_buckets.map(({ binding, bucket_name, jurisdiction, remote }) => {