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 19 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
71 changes: 1 addition & 70 deletions scripts/emulator-tests/fixtures.ts
Expand Up @@ -6,17 +6,6 @@ export const TIMEOUT_MED = 5000;
export const MODULE_ROOT = findModuleRoot("firebase-tools", __dirname);
export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = {
onCreate: {
adminSdkConfig: {
databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
storageBucket: "fake-project-id.appspot.com",
},
emulators: {
firestore: {
host: "localhost",
port: 8080,
},
},
cwd: MODULE_ROOT,
proto: {
data: {
value: {
Expand All @@ -41,22 +30,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } =
},
},
},
triggerId: "us-central1-function_id",
targetName: "function_id",
projectId: "fake-project-id",
},
onWrite: {
adminSdkConfig: {
databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
storageBucket: "fake-project-id.appspot.com",
},
emulators: {
firestore: {
host: "localhost",
port: 8080,
},
},
cwd: MODULE_ROOT,
proto: {
data: {
value: {
Expand All @@ -81,22 +56,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } =
},
},
},
triggerId: "us-central1-function_id",
targetName: "function_id",
projectId: "fake-project-id",
},
onDelete: {
adminSdkConfig: {
databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
storageBucket: "fake-project-id.appspot.com",
},
emulators: {
firestore: {
host: "localhost",
port: 8080,
},
},
cwd: MODULE_ROOT,
proto: {
data: {
oldValue: {
Expand All @@ -121,22 +82,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } =
},
},
},
triggerId: "us-central1-function_id",
targetName: "function_id",
projectId: "fake-project-id",
},
onUpdate: {
adminSdkConfig: {
databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
storageBucket: "fake-project-id.appspot.com",
},
emulators: {
firestore: {
host: "localhost",
port: 8080,
},
},
cwd: MODULE_ROOT,
proto: {
data: {
oldValue: {
Expand Down Expand Up @@ -173,25 +120,9 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } =
timestamp: "2019-05-15T16:21:15.148831Z",
},
},
triggerId: "us-central1-function_id",
targetName: "function_id",
projectId: "fake-project-id",
},
onRequest: {
adminSdkConfig: {
databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
storageBucket: "fake-project-id.appspot.com",
},
emulators: {
firestore: {
host: "localhost",
port: 8080,
},
},
cwd: MODULE_ROOT,
triggerId: "us-central1-function_id",
targetName: "function_id",
projectId: "fake-project-id",
proto: {},
},
};

Expand Down
9 changes: 4 additions & 5 deletions scripts/emulator-tests/functionsEmulator.spec.ts
Expand Up @@ -3,7 +3,7 @@ import * as express from "express";
import * as sinon from "sinon";
import * as supertest from "supertest";

import { SignatureType } from "../../src/emulator/functionsEmulatorShared";
import { EmulatedTriggerDefinition } from "../../src/emulator/functionsEmulatorShared";
import {
EmulatableBackend,
FunctionsEmulator,
Expand Down Expand Up @@ -32,6 +32,7 @@ if ((process.env.DEBUG || "").toLowerCase().includes("spec")) {

const functionsEmulator = new FunctionsEmulator({
projectId: "fake-project-id",
projectDir: MODULE_ROOT,
emulatableBackends: [
{
functionsDir: MODULE_ROOT,
Expand Down Expand Up @@ -108,13 +109,11 @@ function useFunctions(triggers: () => {}): void {
// eslint-disable-next-line @typescript-eslint/unbound-method
functionsEmulator.startFunctionRuntime = (
backend: EmulatableBackend,
triggerId: string,
targetName: string,
triggerType: SignatureType,
trigger: EmulatedTriggerDefinition,
proto?: any,
runtimeOpts?: InvokeRuntimeOpts
): RuntimeWorker => {
return startFunctionRuntime(testBackend, triggerId, targetName, triggerType, proto, {
return startFunctionRuntime(testBackend, trigger, proto, {
nodeBinary: process.execPath,
serializedTriggers,
});
Expand Down
26 changes: 21 additions & 5 deletions scripts/emulator-tests/functionsEmulatorRuntime.spec.ts
Expand Up @@ -26,10 +26,15 @@ const testBackend = {
};

const functionsEmulator = new FunctionsEmulator({
projectDir: MODULE_ROOT,
projectId: "fake-project-id",
emulatableBackends: [testBackend],
adminSdkConfig: {
projectId: "fake-project-id",
databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com",
storageBucket: "fake-project-id.appspot.com",
},
});
(functionsEmulator as any).adminSdkConfig = FunctionRuntimeBundles.onRequest.adminSdkConfig;

async function countLogEntries(worker: RuntimeWorker): Promise<{ [key: string]: number }> {
const runtime = worker.runtime;
Expand All @@ -55,11 +60,22 @@ function startRuntimeWithFunctions(
opts.ignore_warnings = true;
opts.serializedTriggers = serializedTriggers;

const dummyTriggerDef = {
name: "function_id",
region: "region",
id: "region-function_id",
entryPoint: "function_id",
platform: "gcfv1" as const,
};
return functionsEmulator.startFunctionRuntime(
testBackend,
frb.triggerId!,
frb.targetName!,
signatureType,
{
...dummyTriggerDef,
// Fill in with dummy trigger info based on given signature type.
...(signatureType === "http"
? { httpsTrigger: {} }
: { eventTrigger: { eventType: "", resource: "" } }),
},
frb.proto,
opts
);
Expand Down Expand Up @@ -450,7 +466,7 @@ describe("FunctionsEmulator-Runtime", () => {

const data = await callHTTPSFunction(worker, frb);
const info = JSON.parse(data);
expect(info.databaseURL).to.eql(frb.adminSdkConfig.databaseURL!);
expect(info.databaseURL).to.eql("https://fake-project-id-default-rtdb.firebaseio.com");
}).timeout(TIMEOUT_MED);
});
});
Expand Down
19 changes: 19 additions & 0 deletions src/commands/functions-secrets-access.ts
@@ -0,0 +1,19 @@
import { Command } from "../command";
import { logger } from "../logger";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { accessSecretVersion } from "../gcp/secretManager";

export default new Command("functions:secrets:access <KEY>[@version]")
.description(
"Access secret value given secret and its version. Defaults to accessing the latest version."
)
.action(async (key: string, options: Options) => {
const projectId = needProjectId(options);
let [name, version] = key.split("@");
if (!version) {
version = "latest";
}
const value = await accessSecretVersion(projectId, name, version);
logger.info(value);
});
78 changes: 78 additions & 0 deletions src/commands/functions-secrets-destroy.ts
@@ -0,0 +1,78 @@
import { Command } from "../command";
import { Options } from "../options";
import { needProjectId, needProjectNumber } from "../projectUtils";
import {
deleteSecret,
destroySecretVersion,
getSecret,
getSecretVersion,
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(
{
name: "destroy",
type: "confirm",
default: true,
message: `Are you sure you want to destroy ${sv.secret.name}@${sv.versionId}`,
},
options
);
if (!confirm) {
return;
}
}
await destroySecretVersion(projectId, name, version);
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) {
logBullet(`No active secret versions left. Destroying secret ${name}`);
// No active secret version. Remove secret resource.
await deleteSecret(projectId, name);
}
}
});
23 changes: 23 additions & 0 deletions src/commands/functions-secrets-get.ts
@@ -0,0 +1,23 @@
import Table = require("cli-table");

import { Command } from "../command";
import { logger } from "../logger";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { listSecretVersions } from "../gcp/secretManager";

export default new Command("functions:secrets:get <KEY>")
.description("Get metadata for secret and its versions")
.action(async (key: string, options: Options) => {
const projectId = needProjectId(options);
const versions = await listSecretVersions(projectId, key);

const table = new Table({
head: ["Version", "State"],
style: { head: ["yellow"] },
});
for (const version of versions) {
table.push([version.versionId, version.state]);
}
logger.info(table.toString());
});
68 changes: 68 additions & 0 deletions src/commands/functions-secrets-prune.ts
@@ -0,0 +1,68 @@
import * as args from "../deploy/functions/args";
import * as backend from "../deploy/functions/backend";
import { Command } from "../command";
import { Options } from "../options";
import { needProjectId, needProjectNumber } from "../projectUtils";
import { pruneSecrets } from "../functions/secrets";
import { requirePermissions } from "../requirePermissions";
import { isFirebaseManaged } from "../deploymentTool";
import { logBullet, logSuccess } from "../utils";
import { promptOnce } from "../prompt";
import { destroySecretVersion } from "../gcp/secretManager";

export default new Command("functions:secrets:prune")
.description("Destroys unused secrets")
.before(requirePermissions, [
"cloudfunctions.functions.list",
"secretmanager.secrets.list",
"secretmanager.versions.list",
"secretmanager.versions.destroy",
])
.action(async (options: Options) => {
const projectNumber = await needProjectNumber(options);
const projectId = needProjectId(options);

logBullet("Loading secrets...");

const haveBackend = await backend.existingBackend({ projectId } as args.Context);
const haveEndpoints = backend
.allEndpoints(haveBackend)
.filter((e) => isFirebaseManaged(e.labels || []));

const pruned = await pruneSecrets({ projectNumber, projectId }, haveEndpoints);

if (pruned.length === 0) {
logBullet("All secrets are in use. Nothing to prune today.");
return;
}

// prompt to get them all deleted
logBullet(
`Found ${pruned.length} unused active secret versions:\n\t` +
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")
);
return;
}

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

logSuccess("Destroyed all unused secrets!");
});