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 @@ -244,6 +244,24 @@ function parseEndpoints(
"environmentVariables",
"cpu"
);
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,
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
6 changes: 3 additions & 3 deletions src/deploy/functions/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,17 @@ export async function secretsAreValid(projectId: string, wantBackend: backend.Ba
await validateSecretVersions(projectId, endpoints);
}

const secretsSupportedPlatforms = ["gcfv1", "gcfv2"];
/**
* Ensures that all endpoints specifying secret environment variables target platform that supports the feature.
*/
function validatePlatformTargets(endpoints: backend.Endpoint[]) {
const supportedPlatforms = ["gcfv1"];
const unsupported = endpoints.filter((e) => !supportedPlatforms.includes(e.platform));
const unsupported = endpoints.filter((e) => !secretsSupportedPlatforms.includes(e.platform));
if (unsupported.length > 0) {
const errs = unsupported.map((e) => `${e.id}[platform=${e.platform}]`);
throw new FirebaseError(
`Tried to set secret environment variables on ${errs.join(", ")}. ` +
`Only ${supportedPlatforms.join(", ")} support secret environments.`
`Only ${secretsSupportedPlatforms.join(", ")} support secret environments.`
);
}
}
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
22 changes: 20 additions & 2 deletions src/gcp/cloudfunctionsv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ export interface EventFilter {
value: string;
}

/**
* Configurations for secret environment variables attached to a cloud functions resource.
*/
export interface SecretEnvVar {
taeold marked this conversation as resolved.
Show resolved Hide resolved
/* Name of the environment variable. */
key: string;
/* Project identifier (or project number) of the project that contains the secret. */
projectId: string;
/* Name of the secret in secret manager. e.g. MY_SECRET, NOT projects/abc/secrets/MY_SECRET */
secret: string;
/* Version of the secret (version number or the string 'latest') */
version?: string;
}

/** The Cloud Run service that underlies a Cloud Function. */
export interface ServiceConfig {
// Output only
Expand All @@ -104,6 +118,7 @@ export interface ServiceConfig {
timeoutSeconds?: number;
availableMemory?: string;
environmentVariables?: Record<string, string>;
secretEnvironmentVariables?: SecretEnvVar[];
maxInstanceCount?: number;
minInstanceCount?: number;
vpcConnector?: string;
Expand Down Expand Up @@ -351,12 +366,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 +438,7 @@ export function functionFromEndpoint(endpoint: backend.Endpoint, source: Storage
gcfFunction.serviceConfig,
endpoint,
"environmentVariables",
"secretEnvironmentVariables",
"serviceAccountEmail",
"ingressSettings",
"timeoutSeconds"
Expand Down Expand Up @@ -586,6 +603,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 @@ -420,10 +420,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 @@ -432,8 +434,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 @@ -450,7 +454,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 @@ -471,7 +478,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 @@ -481,20 +491,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