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 1 commit
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add functions:secrets:{access, destroy, get} commands. (#4026)
Follow up #4021 to add other management commands for CF3 secrets.

Note that `destroy` commands can be improved by making sure we don't accidentally delete secrets versions currently in use (which would immediately break the function!). I'll add these feature in a follow up PR when we finish reviewing the PR w/ `prune` command.
  • Loading branch information
taeold committed Feb 3, 2022
commit 08f22361c1084219df84274f9529dd5767df68b1
19 changes: 19 additions & 0 deletions src/commands/functions-secrets-access.ts
Original file line number Diff line number Diff line change
@@ -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);
});
51 changes: 51 additions & 0 deletions src/commands/functions-secrets-destroy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Command } from "../command";
import { logger } from "../logger";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import {
deleteSecret,
destroySecretVersion,
getSecret,
getSecretVersion,
listSecretVersions,
} from "../gcp/secretManager";
import { promptOnce } from "../prompt";
import * as secrets from "../functions/secrets";

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);
let [name, version] = key.split("@");
if (!version) {
version = "latest";
}
const sv = await getSecretVersion(projectId, name, version);
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);
logger.info(`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}`);
// No active secret version. Remove secret resource.
await deleteSecret(projectId, name);
}
}
});
23 changes: 23 additions & 0 deletions src/commands/functions-secrets-get.ts
Original file line number Diff line number Diff line change
@@ -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());
});
3 changes: 3 additions & 0 deletions src/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ module.exports = function (client) {
client.functions.deletegcfartifacts = loadCommand("functions-deletegcfartifacts");
}
client.functions.secrets = {};
client.functions.secrets.access = loadCommand("functions-secrets-access");
client.functions.secrets.destroy = loadCommand("functions-secrets-destroy");
client.functions.secrets.get = loadCommand("functions-secrets-get");
client.functions.secrets.set = loadCommand("functions-secrets-set");
client.help = loadCommand("help");
client.hosting = {};
Expand Down
2 changes: 1 addition & 1 deletion src/functions/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const FIREBASE_MANGED = "firebase-managed";
/**
* Returns true if secret is managed by Firebase.
*/
function isFirebaseManaged(secret: Secret): boolean {
export function isFirebaseManaged(secret: Secret): boolean {
return Object.keys(secret.labels || []).includes(FIREBASE_MANGED);
}

Expand Down
82 changes: 81 additions & 1 deletion src/gcp/secretManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ interface AddVersionRequest {
payload: { data: string };
}

interface SecretVersionResponse {
name: string;
state: SecretVersionState;
}

interface AccessSecretVersionResponse {
name: string;
payload: {
data: string;
};
}

const API_VERSION = "v1beta1";

const client = new Client({ urlPrefix: secretManagerOrigin, apiVersion: API_VERSION });
Expand All @@ -70,6 +82,37 @@ export async function getSecret(projectId: string, name: string): Promise<Secret
return secret;
}

/**
* List all secret versions associated with a secret.
*/
export async function listSecretVersions(
projectId: string,
name: string
): Promise<Required<SecretVersion[]>> {
type Response = { versions: SecretVersionResponse[]; nextPageToken?: string };
const secrets: Required<SecretVersion[]> = [];
const path = `projects/${projectId}/secrets/${name}/versions`;

let pageToken = "";
while (true) {
const opts = pageToken == "" ? {} : { queryParams: { pageToken } };
const res = await client.get<Response>(path, opts);

for (const s of res.body.versions) {
secrets.push({
...parseSecretVersionResourceName(s.name),
state: s.state,
});
}

if (!res.body.nextPageToken) {
break;
}
pageToken = res.body.nextPageToken;
}
return secrets;
}

/**
* Returns secret version resource of given name and version in the project.
*/
Expand All @@ -78,7 +121,7 @@ export async function getSecretVersion(
name: string,
version: string
): Promise<Required<SecretVersion>> {
const getRes = await client.get<{ name: string; state: SecretVersionState }>(
const getRes = await client.get<SecretVersionResponse>(
`projects/${projectId}/secrets/${name}/versions/${version}`
);
return {
Expand All @@ -87,6 +130,35 @@ export async function getSecretVersion(
};
}

/**
* Access secret value of a given secret version.
*/
export async function accessSecretVersion(
projectId: string,
name: string,
version: string
): Promise<string> {
const res = await client.get<AccessSecretVersionResponse>(
`projects/${projectId}/secrets/${name}/versions/${version}:access`
);
return Buffer.from(res.body.payload.data, "base64").toString();
}

/**
* Change state of secret version to destroyed.
*/
export async function destroySecretVersion(
projectId: string,
name: string,
version: string
): Promise<void> {
if (version === "latest") {
const sv = await getSecretVersion(projectId, name, "latest");
version = sv.versionId;
}
await client.post(`projects/${projectId}/secrets/${name}/versions/${version}:destroy`);
}

/**
* Returns true if secret resource of given name exists on the project.
*/
Expand Down Expand Up @@ -179,6 +251,14 @@ export async function patchSecret(
return parseSecretResourceName(res.body.name);
}

/**
* Delete secret resource.
*/
export async function deleteSecret(projectId: string, name: string): Promise<void> {
const path = `projects/${projectId}/secrets/${name}`;
await client.delete(path);
}

/**
* Add new version the payload as value on the given secret.
*/
Expand Down