Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for VPC direct connect (network interfaces) (#1823)
55 changes: 54 additions & 1 deletion spec/v2/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)", () => {
Expand All @@ -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);
});
});
9 changes: 8 additions & 1 deletion src/runtime/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,15 @@ export interface ManifestEndpoint {
timeoutSeconds?: number | Expression<number> | ResetValue;
vpc?:
| {
connector: string | Expression<string>;
connector?: string | Expression<string>;
egressSettings?: string | Expression<string> | ResetValue;
networkInterfaces?:
| Array<{
network?: string | Expression<string> | ResetValue;
subnetwork?: string | Expression<string> | ResetValue;
tags?: string | string[] | Expression<string> | Expression<string[]> | ResetValue;
}>
| ResetValue;
}
| ResetValue;
serviceAccountEmail?: string | Expression<string> | ResetValue;
Expand Down
86 changes: 80 additions & 6 deletions src/v2/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> | 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<string> | ResetValue;

/**
* Tags for VPC traffic.
* An optional field for VPC direct connect
* mutually exclusive with vpcConnector.
*/
tags?: string | string[] | Expression<string> | Expression<string[]> | ResetValue;
}

/**
* `GlobalOptions` are options that can be set across an entire project.
* These options are common to HTTPS and event handling functions.
Expand Down Expand Up @@ -179,6 +208,7 @@ export interface GlobalOptions {

/**
* Connect a function to a specified VPC connector.
* Mutually exclusive with networkInterface
*/
vpcConnector?: string | Expression<string> | ResetValue;

Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that in manifest.ts, we support an array of network interfaces. But here we only expose a singular object. Is it something that was intentional or do we plan on something like networkInterfaces?: NetworkInterface | NetworkInterface[] in the future?

Copy link
Member Author

Choose a reason for hiding this comment

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

Great question, and yes. The Google API is a plural but only supports arrays of size one. To support ergonomics, we will currently only accept a singular in our API and eventually support a plural if GCF ever supports one, but our wire format will always be plural to avoid mistakes.

This is part of the predictability principle that you should be forgiving in what you accept (e.g. singular or plural) but strict in what you produce (plural)


/**
* Specific service account for the function to run as.
*/
Expand Down Expand Up @@ -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];
});
Expand All @@ -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) => {
Expand Down Expand Up @@ -365,12 +414,37 @@ export function optionsToEndpoint(
"cpu"
);
convertIfPresent(endpoint, opts, "serviceAccountEmail", "serviceAccount");
if (opts.vpcConnector !== undefined) {
if (opts.vpcConnector === null || opts.vpcConnector instanceof ResetValue) {
const vpcEgress = opts.vpcEgress ?? opts.vpcConnectorEgressSettings;

Choose a reason for hiding this comment

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

iiuc, it looks like we are silently prioritizing vpcEgress if both that and vpcConnectorEgressSettings are provided. Was curious if we want to log a warning if a user accidentally supplies both, or is the alias behavior intended to be a silent override?

Copy link
Member Author

Choose a reason for hiding this comment

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

That seems pretty reasonable.

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;
}
}
Expand Down
Loading