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

Improve function secrets ergonomics #4130

Merged
merged 65 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
41696b3
Use runtime delegate to parse function triggers in the Functions Emul…
taeold Jan 27, 2022
ac72bf8
Slim down Functions Emulator Runtime (i.e. args sent over to emulated…
taeold Jan 31, 2022
dd8cef8
Merge remote-tracking branch 'origin/master' into cf3-secrets
taeold Jan 31, 2022
8b1a590
Merge branch 'cf3-secrets' of https://github.com/firebase/firebase-to…
taeold Jan 31, 2022
39f2ac7
CF3 Secrets Support (#3959)
taeold Feb 3, 2022
3293186
Add new command (functions:secrets:set) for creating secrets to be us…
taeold Feb 3, 2022
08f2236
Add functions:secrets:{access, destroy, get} commands. (#4026)
taeold Feb 3, 2022
3310fdc
Add command to prune unused secrets (#4108)
taeold Feb 4, 2022
5731022
Guard destroy command by checking to see if version is in use.
taeold Feb 4, 2022
ea194e1
Prune secrets on deploy, redeploy on new secret versions.
taeold Feb 4, 2022
ca8a09e
Update log messages.
taeold Feb 4, 2022
87534f7
Use utility fn instead.
taeold Feb 4, 2022
8144038
Only prune secrets on successful deploy.
taeold Feb 4, 2022
3b45d53
Improve log messages.
taeold Feb 4, 2022
7906346
Improve prune to find all non-destroyed secret versions.
taeold Feb 4, 2022
1853e7f
Better types, better docs.
taeold Feb 7, 2022
5bb029e
Eslints.
taeold Feb 7, 2022
bb51612
Add support for secrets in the Functions Emulator (#4106)
taeold Feb 7, 2022
7e1d70d
Exit early if secret is not in use.
taeold Feb 7, 2022
b784fb5
Add test cases.
taeold Feb 7, 2022
4c1dc1f
Make better code comments.
taeold Feb 7, 2022
c459b23
Fix bug where label wasn't included in the returned resource.
taeold Feb 8, 2022
108beea
Merge branch 'master' of https://github.com/firebase/firebase-tools i…
taeold Feb 8, 2022
ef86cce
Merge branch 'cf3-secrets' of https://github.com/firebase/firebase-to…
taeold Feb 8, 2022
e799549
Remove preview flag, add option to disable dotenv support. (#4022)
taeold Feb 8, 2022
ad8be43
Reload all endpoints when pruning secrets post deploy.
taeold Feb 8, 2022
f271e69
Guard destroy command by checking to see if version is in use.
taeold Feb 4, 2022
21ae5fb
Prune secrets on deploy, redeploy on new secret versions.
taeold Feb 4, 2022
c9e7a2e
Update log messages.
taeold Feb 4, 2022
09670c1
Use utility fn instead.
taeold Feb 4, 2022
704003e
Only prune secrets on successful deploy.
taeold Feb 4, 2022
38d5b1a
Improve log messages.
taeold Feb 4, 2022
91a358d
Improve prune to find all non-destroyed secret versions.
taeold Feb 4, 2022
64bb335
Better types, better docs.
taeold Feb 7, 2022
8cd70c6
Eslints.
taeold Feb 7, 2022
3c6ddb4
Exit early if secret is not in use.
taeold Feb 7, 2022
a342ba3
Add test cases.
taeold Feb 7, 2022
f6a52d0
Make better code comments.
taeold Feb 7, 2022
432159a
Fix bug where label wasn't included in the returned resource.
taeold Feb 8, 2022
2ada4f3
Reload all endpoints when pruning secrets post deploy.
taeold Feb 8, 2022
fd16fb8
Merge branch 'dl-cf3-secret-cmds-ergonomics' of https://github.com/fi…
taeold Feb 17, 2022
fc0a888
Add -f option to prune.
taeold Feb 17, 2022
31d0fe6
Only prune secrets if the deploy contained function w/ secrets.
taeold Feb 18, 2022
6cd5c16
Merge branch 'master' of https://github.com/firebase/firebase-tools i…
taeold Feb 23, 2022
31b9c9c
Nits.
taeold Feb 23, 2022
2cfbcce
Add changelog.
taeold Feb 23, 2022
d0ca620
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 7, 2022
207f202
Use === instead of ==.
taeold Mar 7, 2022
50020b6
Remove unnecessary null check.
taeold Mar 9, 2022
502af3d
Reuse existing haveEndpoints variable.
taeold Mar 9, 2022
a6cf8c7
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 9, 2022
f8a3b01
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 11, 2022
af43869
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 15, 2022
2a66e7d
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 16, 2022
cf14e46
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 17, 2022
7bf6214
Update CHANGELOG.md
taeold Mar 17, 2022
09d93ae
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 21, 2022
8f2f309
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 22, 2022
28897a4
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 23, 2022
74067b9
Prettier.
taeold Mar 24, 2022
8c557c0
Merge branch 'master' into dl-cf3-secret-cmds-ergonomics
taeold Mar 30, 2022
02fbd31
Merge conflict.
taeold Mar 30, 2022
05068df
Fix merge gone wrong.
taeold Mar 30, 2022
6abe03a
Pretty.
taeold Mar 31, 2022
d679d0d
Pretty.
taeold Mar 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Fixes bug where functions' memory configurations weren't preserved in batched function deploys (#4253).
- `ext:export` now uses stable ordering for params in .env files (#4256).
- Adds alerting event provider (#4258).
- Adds ergonomic improvements to CF3 secret commands to automatically redeploy functions and delete unused secrets (#4130).
35 changes: 31 additions & 4 deletions src/commands/functions-secrets-destroy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Command } from "../command";
import { logger } from "../logger";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { needProjectId, needProjectNumber } from "../projectUtils";
import {
deleteSecret,
destroySecretVersion,
Expand All @@ -10,18 +9,46 @@ import {
listSecretVersions,
} from "../gcp/secretManager";
import { promptOnce } from "../prompt";
import { logBullet, logWarning } from "../utils";
import * as secrets from "../functions/secrets";
import * as backend from "../deploy/functions/backend";
import * as args from "../deploy/functions/args";

export default new Command("functions:secrets:destroy <KEY>[@version]")
.description("Destroy a secret. Defaults to destroying the latest version.")
.withForce("Destroys a secret without confirmation.")
.action(async (key: string, options: Options) => {
const projectId = needProjectId(options);
const projectNumber = await needProjectNumber(options);
const haveBackend = await backend.existingBackend({ projectId } as args.Context);

let [name, version] = key.split("@");
if (!version) {
version = "latest";
}
const sv = await getSecretVersion(projectId, name, version);

if (sv.state === "DESTROYED") {
logBullet(`Secret ${sv.secret.name}@${version} is already destroyed. Nothing to do.`);
return;
}

const boundEndpoints = backend
.allEndpoints(haveBackend)
.filter((e) => secrets.inUse({ projectId, projectNumber }, sv.secret, e));
if (boundEndpoints.length > 0) {
const endpointsMsg = boundEndpoints
.map((e) => `${e.id}[${e.platform}](${e.region})`)
Copy link
Member

Choose a reason for hiding this comment

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

A function can only be gcf v1 or v2, so we can probably drop this disambiguation. It'll also help us avoid leaking gcfv2's existence/support when we release secrets support.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, old comment. Disregard.

.join("\t\n");
logWarning(
`Secret ${name}@${version} is currently in use by following functions:\n\t${endpointsMsg}`
);
if (!options.force) {
logWarning("Refusing to destroy secret in use. Use -f to destroy the secret anyway.");
return;
}
}

if (!options.force) {
const confirm = await promptOnce(
{
Expand All @@ -37,13 +64,13 @@ export default new Command("functions:secrets:destroy <KEY>[@version]")
}
}
await destroySecretVersion(projectId, name, version);
logger.info(`Destroyed secret version ${name}@${sv.versionId}`);
logBullet(`Destroyed secret version ${name}@${sv.versionId}`);

const secret = await getSecret(projectId, name);
if (secrets.isFirebaseManaged(secret)) {
const versions = await listSecretVersions(projectId, name);
if (versions.filter((v) => v.state === "ENABLED").length === 0) {
logger.info(`No active secret versions left. Destroying secret ${name}`);
logBullet(`No active secret versions left. Destroying secret ${name}`);
// No active secret version. Remove secret resource.
await deleteSecret(projectId, name);
}
Expand Down
38 changes: 19 additions & 19 deletions src/commands/functions-secrets-prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { promptOnce } from "../prompt";
import { destroySecretVersion } from "../gcp/secretManager";

export default new Command("functions:secrets:prune")
.withForce("Destroys unused secrets without prompt")
.description("Destroys unused secrets")
.before(requirePermissions, [
"cloudfunctions.functions.list",
Expand Down Expand Up @@ -42,27 +43,26 @@ export default new Command("functions:secrets:prune")
pruned.map((sv) => `${sv.secret}@${sv.version}`).join("\n\t")
);

const confirm = await promptOnce(
{
name: "destroy",
type: "confirm",
default: true,
message: `Do you want to destroy unused secret versions?`,
},
options
);

if (!confirm) {
logBullet(
"Run the following commands to destroy each unused secret version:\n\t" +
pruned
.map((sv) => `firebase functions:secrets:destroy ${sv.secret}@${sv.version}`)
.join("\n\t")
if (!options.force) {
const confirm = await promptOnce(
{
name: "destroy",
type: "confirm",
default: true,
message: `Do you want to destroy unused secret versions?`,
},
options
);
return;
if (!confirm) {
logBullet(
"Run the following commands to destroy each unused secret version:\n\t" +
pruned
.map((sv) => `firebase functions:secrets:destroy ${sv.secret}@${sv.version}`)
.join("\n\t")
);
return;
}
}

await Promise.all(pruned.map((sv) => destroySecretVersion(projectId, sv.secret, sv.version)));

logSuccess("Destroyed all unused secrets!");
});
89 changes: 80 additions & 9 deletions src/commands/functions-secrets-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ import * as fs from "fs";

import * as clc from "cli-color";

import { ensureValidKey, ensureSecret } from "../functions/secrets";
import { ensureValidKey, ensureSecret, pruneAndDestroySecrets } from "../functions/secrets";
import { Command } from "../command";
import { requirePermissions } from "../requirePermissions";
import { Options } from "../options";
import { promptOnce } from "../prompt";
import { logBullet, logSuccess } from "../utils";
import { needProjectId } from "../projectUtils";
import { logBullet, logSuccess, logWarning } from "../utils";
import { needProjectId, needProjectNumber } from "../projectUtils";
import { addVersion, toSecretVersionResourceName } from "../gcp/secretManager";
import * as secrets from "../functions/secrets";
import * as backend from "../deploy/functions/backend";
import * as args from "../deploy/functions/args";

export default new Command("functions:secrets:set <KEY>")
.description("Create or update a secret for use in Cloud Functions for Firebase")
.withForce(
"Does not ensure input keys are valid or upgrade existing secrets to have Firebase manage them."
)
.description("Create or update a secret for use in Cloud Functions for Firebase.")
.withForce("Automatically updates functions to use the new secret.")
.before(requirePermissions, [
"secretmanager.secrets.create",
"secretmanager.secrets.get",
Expand All @@ -29,6 +30,7 @@ export default new Command("functions:secrets:set <KEY>")
)
.action(async (unvalidatedKey: string, options: Options) => {
const projectId = needProjectId(options);
const projectNumber = await needProjectNumber(options);
const key = await ensureValidKey(unvalidatedKey, options);
const secret = await ensureSecret(projectId, key, options);
let secretValue;
Expand All @@ -49,8 +51,77 @@ export default new Command("functions:secrets:set <KEY>")

const secretVersion = await addVersion(projectId, key, secretValue);
logSuccess(`Created a new secret version ${toSecretVersionResourceName(secretVersion)}`);

if (!secrets.isFirebaseManaged(secret)) {
logBullet(
"Please deploy your functions for the change to take effect by running:\n\t" +
clc.bold("firebase deploy --only functions")
);
return;
}

const haveBackend = await backend.existingBackend({ projectId } as args.Context);
const endpointsToUpdate = backend
.allEndpoints(haveBackend)
.filter((e) => secrets.inUse({ projectId, projectNumber }, secret, e));

if (endpointsToUpdate.length === 0) {
return;
}

logBullet(
"Please deploy your functions for the change to take effect by running:\n\t" +
clc.bold("firebase deploy --only functions")
`${endpointsToUpdate.length} functions are using stale version of secret ${secret.name}:\n\t` +
endpointsToUpdate.map((e) => `${e.id}(${e.region})`).join("\n\t")
);

if (!options.force) {
const confirm = await promptOnce(
{
name: "redeploy",
type: "confirm",
default: true,
message: `Do you want to re-deploy the functions and destroy the stale version of secret ${secret.name}?`,
},
options
);
if (!confirm) {
logBullet(
"Please deploy your functions for the change to take effect by running:\n\t" +
clc.bold("firebase deploy --only functions")
);
return;
}
}

const updateOps = endpointsToUpdate.map(async (e) => {
logBullet(`Updating function ${e.id}(${e.region})...`);
const updated = await secrets.updateEndpointSecret(
{ projectId, projectNumber },
secretVersion,
e
);
logBullet(`Updated function ${e.id}(${e.region}).`);
return updated;
});
const updatedEndpoints = await Promise.all(updateOps);

logBullet(`Pruning stale secrets...`);
const prunedResult = await pruneAndDestroySecrets(
{ projectId, projectNumber },
updatedEndpoints
);
if (prunedResult.destroyed.length > 0) {
logBullet(
`Detroyed unused secret versions: ${prunedResult.destroyed
.map((s) => `${s.secret}@${s.version}`)
.join(", ")}`
);
}
if (prunedResult.erred.length > 0) {
logWarning(
`Failed to destroy unused secret versions:\n\t${prunedResult.erred
.map((err) => err.message)
.join("\n\t")}`
);
}
});
4 changes: 2 additions & 2 deletions src/deploy/functions/release/fabricator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import * as scheduler from "../../../gcp/cloudscheduler";
import * as utils from "../../../utils";

// TODO: Tune this for better performance.
const gcfV1PollerOptions = {
const gcfV1PollerOptions: Omit<poller.OperationPollerOptions, "operationResourceName"> = {
apiOrigin: functionsOrigin,
apiVersion: gcf.API_VERSION,
masterTimeout: 25 * 60 * 1_000, // 25 minutes is the maximum build time for a function
maxBackoff: 10_000,
};

const gcfV2PollerOptions = {
const gcfV2PollerOptions: Omit<poller.OperationPollerOptions, "operationResourceName"> = {
apiOrigin: functionsV2Origin,
apiVersion: gcfV2.API_VERSION,
masterTimeout: 25 * 60 * 1_000, // 25 minutes is the maximum build time for a function
Expand Down
30 changes: 30 additions & 0 deletions src/deploy/functions/release/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import * as fabricator from "./fabricator";
import * as reporter from "./reporter";
import * as executor from "./executor";
import * as prompts from "../prompts";
import * as secrets from "../../../functions/secrets";
import { getAppEngineLocation } from "../../../functionsConfig";
import { getFunctionLabel } from "../functionsDeployHelper";
import { FirebaseError } from "../../../error";
import { needProjectId, needProjectNumber } from "../../../projectUtils";
import { logLabeledBullet, logLabeledWarning } from "../../../utils";

/** Releases new versions of functions to prod. */
export async function release(
Expand Down Expand Up @@ -85,6 +88,33 @@ export async function release(
if (allErrors.length) {
const opts = allErrors.length === 1 ? { original: allErrors[0] } : { children: allErrors };
throw new FirebaseError("There was an error deploying functions", { ...opts, exit: 2 });
} else {
if (secrets.of(haveEndpoints).length > 0) {
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);
Copy link
Member

Choose a reason for hiding this comment

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

Isn't there a "force" option you can use to keep context?

const prunedResult = await secrets.pruneAndDestroySecrets(
{ projectId, projectNumber },
backend.allEndpoints(reloadedBackend)
);
if (prunedResult.destroyed.length > 0) {
logLabeledBullet(
"functions",
`Destroyed unused secret versions: ${prunedResult.destroyed
.map((s) => `${s.secret}@${s.version}`)
.join(", ")}`
);
}
if (prunedResult.erred.length > 0) {
logLabeledWarning(
"functions",
`Failed to destroy unused secret versions:\n\t${prunedResult.erred
.map((err) => err.message)
.join("\n\t")}`
);
}
}
}
}

Expand Down