diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..83ea74663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add support for VPC direct connect (network interfaces) (#1823) diff --git a/spec/v2/options.spec.ts b/spec/v2/options.spec.ts index 4105f9a1f..d83b16894 100644 --- a/spec/v2/options.spec.ts +++ b/spec/v2/options.spec.ts @@ -22,7 +22,7 @@ import { expect } from "chai"; import { defineJsonSecret, defineSecret } from "../../src/params"; -import { GlobalOptions } from "../../src/v2/options"; +import { GlobalOptions, optionsToEndpoint, RESET_VALUE } from "../../src/v2/options"; describe("GlobalOptions", () => { it("should accept all valid secret types in secrets array (type test)", () => { @@ -39,3 +39,56 @@ describe("GlobalOptions", () => { expect(opts.secrets).to.have.length(3); }); }); + +describe("optionsToEndpoint", () => { + it("should return an empty vpc if none provided", () => { + const endpoint = optionsToEndpoint({}); + expect(endpoint.vpc).to.be.undefined; + }); + + it("should set vpcConnector correctly", () => { + const endpoint = optionsToEndpoint({ vpcConnector: "my-connector", vpcEgress: "ALL_TRAFFIC" }); + expect(endpoint.vpc).to.deep.equal({ + connector: "my-connector", + egressSettings: "ALL_TRAFFIC", + }); + }); + + it("should set networkInterface correctly", () => { + const endpoint = optionsToEndpoint({ + networkInterface: { network: "my-network" }, + vpcEgress: "PRIVATE_RANGES_ONLY", + }); + expect(endpoint.vpc).to.deep.equal({ + networkInterfaces: [{ network: "my-network" }], + egressSettings: "PRIVATE_RANGES_ONLY", + }); + }); + + it("should throw an error if both vpcConnector and networkInterface are provided", () => { + expect(() => { + optionsToEndpoint({ + vpcConnector: "my-connector", + networkInterface: { network: "my-network" }, + }); + }).to.throw("Cannot set both vpcConnector and networkInterface"); + }); + + it("should throw an error if networkInterface has no network or subnetwork", () => { + expect(() => { + optionsToEndpoint({ + networkInterface: {}, + }); + }).to.throw("At least one of network or subnetwork must be specified in networkInterface."); + }); + + it("should reset vpc when vpcConnector is RESET_VALUE", () => { + const endpoint = optionsToEndpoint({ vpcConnector: RESET_VALUE }); + expect(endpoint.vpc).to.equal(RESET_VALUE); + }); + + it("should reset vpc when networkInterface is RESET_VALUE", () => { + const endpoint = optionsToEndpoint({ networkInterface: RESET_VALUE }); + expect(endpoint.vpc).to.equal(RESET_VALUE); + }); +}); diff --git a/src/runtime/manifest.ts b/src/runtime/manifest.ts index aee28ccce..32e754c16 100644 --- a/src/runtime/manifest.ts +++ b/src/runtime/manifest.ts @@ -52,8 +52,15 @@ export interface ManifestEndpoint { timeoutSeconds?: number | Expression | ResetValue; vpc?: | { - connector: string | Expression; + connector?: string | Expression; egressSettings?: string | Expression | ResetValue; + networkInterfaces?: + | Array<{ + network?: string | Expression | ResetValue; + subnetwork?: string | Expression | ResetValue; + tags?: string | string[] | Expression | Expression | ResetValue; + }> + | ResetValue; } | ResetValue; serviceAccountEmail?: string | Expression | ResetValue; diff --git a/src/v2/options.ts b/src/v2/options.ts index 5b7e12aa9..c5f8f1ba9 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -107,6 +107,35 @@ export type VpcEgressSetting = "PRIVATE_RANGES_ONLY" | "ALL_TRAFFIC"; */ export type IngressSetting = "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB"; +/** + * Interface for a direct VPC network connection. + * At least one of network or subnetwork must be specified. + */ +export interface NetworkInterface { + /** + * Network to use for VPC direct connect. + * network or subnetwork must be specified to use VPC direct connect, though + * both can be specified as well. "default" is an acceptable value. + * Mutually exclusive with vpcConnector. + */ + network?: string | Expression | ResetValue; + + /** + * Subnetwork to use for VPC direct connect. + * network or subnetwork must be specified to use VPC direct connect, though + * both can be specified as well. "default" is an acceptable value. + * Mutually exclusive with vpcConnector. + */ + subnetwork?: string | Expression | ResetValue; + + /** + * Tags for VPC traffic. + * An optional field for VPC direct connect + * mutually exclusive with vpcConnector. + */ + tags?: string | string[] | Expression | Expression | ResetValue; +} + /** * `GlobalOptions` are options that can be set across an entire project. * These options are common to HTTPS and event handling functions. @@ -179,6 +208,7 @@ export interface GlobalOptions { /** * Connect a function to a specified VPC connector. + * Mutually exclusive with networkInterface */ vpcConnector?: string | Expression | ResetValue; @@ -187,6 +217,16 @@ export interface GlobalOptions { */ vpcConnectorEgressSettings?: VpcEgressSetting | ResetValue; + /** + * An alias for vpcConnectorEgressSettings. + */ + vpcEgress?: VpcEgressSetting | ResetValue; + + /** + * Network Interface to use with VPC Direct Connect + */ + networkInterface?: NetworkInterface | ResetValue; + /** * Specific service account for the function to run as. */ @@ -311,9 +351,18 @@ export function optionsToTriggerAnnotations( "ingressSettings", "labels", "vpcConnector", - "vpcConnectorEgressSettings", "secrets" ); + + const vpcEgress = opts.vpcEgress ?? opts.vpcConnectorEgressSettings; + if (vpcEgress !== undefined) { + if (vpcEgress === null || vpcEgress instanceof ResetValue) { + annotation.vpcConnectorEgressSettings = null; + } else { + annotation.vpcConnectorEgressSettings = vpcEgress; + } + } + convertIfPresent(annotation, opts, "availableMemoryMb", "memory", (mem: MemoryOption) => { return MemoryOptionToMB[mem]; }); @@ -333,7 +382,7 @@ export function optionsToTriggerAnnotations( convertIfPresent(annotation, opts, "timeout", "timeoutSeconds", durationFromSeconds); convertIfPresent( annotation, - opts as any as EventHandlerOptions, + opts as EventHandlerOptions, "failurePolicy", "retry", (retry: boolean) => { @@ -365,12 +414,40 @@ export function optionsToEndpoint( "cpu" ); convertIfPresent(endpoint, opts, "serviceAccountEmail", "serviceAccount"); - if (opts.vpcConnector !== undefined) { - if (opts.vpcConnector === null || opts.vpcConnector instanceof ResetValue) { + if (opts.vpcEgress && opts.vpcConnectorEgressSettings) { + logger.warn("vpcEgress and vpcConnectorEgressSettings are both set. Using vpcEgress"); + } + const vpcEgress = opts.vpcEgress ?? opts.vpcConnectorEgressSettings; + const connector = opts.vpcConnector; + const networkInterface = opts.networkInterface; + + if (connector !== undefined || vpcEgress !== undefined || networkInterface !== undefined) { + const resetConnector = connector === null || connector instanceof ResetValue; + const hasConnector = !!connector; + const resetNetwork = networkInterface === null || networkInterface instanceof ResetValue; + const hasNetwork = !!networkInterface && !resetNetwork; + + if (hasNetwork) { + if (!networkInterface.network && !networkInterface.subnetwork) { + throw new Error( + "At least one of network or subnetwork must be specified in networkInterface." + ); + } + } + + // It's OK to reset one and set the other, that's just being pedantic while switching types. + // But if you only use a reset value, that means you don't want VPC at all. + if (hasNetwork && hasConnector) { + throw new Error("Cannot set both vpcConnector and networkInterface"); + } else if ((resetConnector && !hasNetwork) || (resetNetwork && !hasConnector)) { endpoint.vpc = RESET_VALUE; } else { - const vpc: ManifestEndpoint["vpc"] = { connector: opts.vpcConnector }; - convertIfPresent(vpc, opts, "egressSettings", "vpcConnectorEgressSettings"); + const vpc: ManifestEndpoint["vpc"] = {}; + convertIfPresent(vpc, opts, "connector", "vpcConnector"); + if (vpcEgress !== undefined) { + vpc.egressSettings = vpcEgress; + } + convertIfPresent(vpc, opts, "networkInterfaces", "networkInterface", (a) => [a]); endpoint.vpc = vpc; } }