Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for secrets in gcfv2 endpoints #4451

Merged
merged 11 commits into from
May 3, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add support for secrets to v2 functions (#4451).
5 changes: 4 additions & 1 deletion src/deploy/functions/release/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ export async function release(
const projectId = needProjectId(options);
const projectNumber = await needProjectNumber(options);
// Re-load backend with all endpoints, not just the ones deployed.
const reloadedBackend = await backend.existingBackend({ projectId } as args.Context);
const reloadedBackend = await backend.existingBackend(
{ projectId } as args.Context,
/* forceRefresh= */ true
);
const prunedResult = await secrets.pruneAndDestroySecrets(
{ projectId, projectNumber },
backend.allEndpoints(reloadedBackend)
Expand Down
18 changes: 18 additions & 0 deletions src/deploy/functions/runtimes/discovery/v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,24 @@ function parseEndpoints(
"ingressSettings",
"environmentVariables"
);
renameIfPresent(
parsed,
ep,
"secretEnvironmentVariables",
"secretEnvironmentVariables",
(senvs: ManifestEndpoint["secretEnvironmentVariables"]) => {
if (senvs && senvs.length > 0) {
ep.secretEnvironmentVariables = [];
for (const { key, secret } of senvs) {
ep.secretEnvironmentVariables.push({
key: key,
taeold marked this conversation as resolved.
Show resolved Hide resolved
secret: secret || key, // if secret is undefined, assume env var key == secret name
projectId: project,
taeold marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
}
);
allParsed.push(parsed);
}

Expand Down
2 changes: 1 addition & 1 deletion src/deploy/functions/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export async function secretsAreValid(projectId: string, wantBackend: backend.Ba
* Ensures that all endpoints specifying secret environment variables target platform that supports the feature.
*/
function validatePlatformTargets(endpoints: backend.Endpoint[]) {
const supportedPlatforms = ["gcfv1"];
const supportedPlatforms = ["gcfv1", "gcfv2"];
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit:

const supportedPlatforms = new Set(["gcfv1", "gcfv2"]);
const unsupported = endpoints.filter((e) => !supportedPlatforms.has(e.platform));

Nit x2: I don't know how often this is called, but maybe it makes more sense to have this defined as a const up top?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You and @inlined should have a talk! @inlined does not like Sets, especially in our code where N is almost always < 50 where using Set (which has every so slightly different API from arrays) doesn't give us much!

Copy link
Contributor

Choose a reason for hiding this comment

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

Team convention is king. :)

That said, my thoughts are its less about efficiency and more about intention/code clarity.

Set limits the purpose of the variable/code to just checking for existence.

Array leaves the implementation open ended. Maybe the array going to be used with has, reduce, or map. Someone else won't know unless they read through the code to find how/why the array is being used.

const unsupported = endpoints.filter((e) => !supportedPlatforms.includes(e.platform));
if (unsupported.length > 0) {
const errs = unsupported.map((e) => `${e.id}[platform=${e.platform}]`);
Expand Down
5 changes: 3 additions & 2 deletions src/gcp/cloudfunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,12 +370,13 @@ export async function updateFunction(
cloudFunction: Omit<CloudFunction, OutputOnlyFields>
): Promise<Operation> {
const endpoint = `/${cloudFunction.name}`;
// Keys in labels and environmentVariables are user defined, so we don't recurse
// Keys in labels and environmentVariables and secretEnvironmentVariables are user defined, so we don't recurse
// for field masks.
const fieldMasks = proto.fieldMasks(
cloudFunction,
/* doNotRecurseIn...=*/ "labels",
"environmentVariables"
"environmentVariables",
"secretEnvironmentVariables"
);

// Failure policy is always an explicit policy and is only signified by the presence or absence of
Expand Down
15 changes: 13 additions & 2 deletions src/gcp/cloudfunctionsv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ export interface EventFilter {
value: string;
}

export interface SecretEnvVar {
taeold marked this conversation as resolved.
Show resolved Hide resolved
key: string;
projectId: string;
secret: string;
version?: string;
}

/** The Cloud Run service that underlies a Cloud Function. */
export interface ServiceConfig {
// Output only
Expand All @@ -104,6 +111,7 @@ export interface ServiceConfig {
timeoutSeconds?: number;
availableMemory?: string;
environmentVariables?: Record<string, string>;
secretEnvironmentVariables?: Array<SecretEnvVar>;
taeold marked this conversation as resolved.
Show resolved Hide resolved
maxInstanceCount?: number;
minInstanceCount?: number;
vpcConnector?: string;
Expand Down Expand Up @@ -351,12 +359,13 @@ async function listFunctionsInternal(
export async function updateFunction(
cloudFunction: Omit<CloudFunction, OutputOnlyFields>
): Promise<Operation> {
// Keys in labels and environmentVariables are user defined, so we don't recurse
// Keys in labels and environmentVariables and secretEnvironmentVariables are user defined, so we don't recurse
// for field masks.
const fieldMasks = proto.fieldMasks(
cloudFunction,
/* doNotRecurseIn...=*/ "labels",
"serviceConfig.environmentVariables"
"serviceConfig.environmentVariables",
"serviceConfig.secretEnvironmentVariables"
);
try {
const queryParams = {
Expand Down Expand Up @@ -422,6 +431,7 @@ export function functionFromEndpoint(endpoint: backend.Endpoint, source: Storage
gcfFunction.serviceConfig,
endpoint,
"environmentVariables",
"secretEnvironmentVariables",
"serviceAccountEmail",
"ingressSettings",
"timeoutSeconds"
Expand Down Expand Up @@ -586,6 +596,7 @@ export function endpointFromFunction(gcfFunction: CloudFunction): backend.Endpoi
"serviceAccountEmail",
"ingressSettings",
"environmentVariables",
"secretEnvironmentVariables",
"timeoutSeconds"
);
proto.renameIfPresent(
Expand Down
52 changes: 32 additions & 20 deletions src/test/deploy/functions/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,12 @@ describe("validate", () => {
expect(validate.secretsAreValid(project, b)).to.not.be.rejected;
});

it("fails validation given endpoint with secrets targeting unsupported platform", () => {
it("fails validation given non-existent secret version", () => {
secretVersionStub.rejects({ reason: "Secret version does not exist" });

const b = backend.of({
...ENDPOINT,
platform: "gcfv2",
platform: "gcfv1",
secretEnvironmentVariables: [
{
projectId: project,
Expand All @@ -411,8 +413,10 @@ describe("validate", () => {
},
],
});

expect(validate.secretsAreValid(project, b)).to.be.rejectedWith(FirebaseError);
expect(validate.secretsAreValid(project, b)).to.be.rejectedWith(
FirebaseError,
/Failed to validate secret version/
);
});

it("fails validation given non-existent secret version", () => {
Expand All @@ -429,7 +433,10 @@ describe("validate", () => {
},
],
});
expect(validate.secretsAreValid(project, b)).to.be.rejectedWith(FirebaseError);
expect(validate.secretsAreValid(project, b)).to.be.rejectedWith(
FirebaseError,
/Failed to validate secret versions/
);
});

it("fails validation given disabled secret version", () => {
Expand All @@ -450,7 +457,10 @@ describe("validate", () => {
},
],
});
expect(validate.secretsAreValid(project, b)).to.be.rejected;
expect(validate.secretsAreValid(project, b)).to.be.rejectedWith(
FirebaseError,
/Failed to validate secret versions/
);
});

it("passes validation and resolves latest version given valid secret config", async () => {
Expand All @@ -460,20 +470,22 @@ describe("validate", () => {
state: "ENABLED",
});

const b = backend.of({
...ENDPOINT,
platform: "gcfv1",
secretEnvironmentVariables: [
{
projectId: project,
secret: "MY_SECRET",
key: "MY_SECRET",
},
],
});

await validate.secretsAreValid(project, b);
expect(backend.allEndpoints(b)[0].secretEnvironmentVariables![0].version).to.equal("2");
for (const platform of ["gcfv1" as const, "gcfv2" as const]) {
const b = backend.of({
...ENDPOINT,
platform,
secretEnvironmentVariables: [
{
projectId: project,
secret: "MY_SECRET",
key: "MY_SECRET",
},
],
});

await validate.secretsAreValid(project, b);
expect(backend.allEndpoints(b)[0].secretEnvironmentVariables![0].version).to.equal("2");
}
});
});
});
14 changes: 14 additions & 0 deletions src/test/gcp/cloudfunctionsv2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ describe("cloudfunctionsv2", () => {
environmentVariables: {
FOO: "bar",
},
secretEnvironmentVariables: [
{
secret: "MY_SECRET",
key: "MY_SECRET",
projectId: "project",
},
],
};

const fullGcfFunction: Omit<
Expand All @@ -227,6 +234,13 @@ describe("cloudfunctionsv2", () => {
environmentVariables: {
FOO: "bar",
},
secretEnvironmentVariables: [
{
secret: "MY_SECRET",
key: "MY_SECRET",
projectId: "project",
},
],
vpcConnector: "connector",
vpcConnectorEgressSettings: "ALL_TRAFFIC",
ingressSettings: "ALLOW_ALL",
Expand Down