Skip to content

Commit b6cb593

Browse files
committed
Improve secret parameter support for firebase functions.
Key changes: - Added --format flag to functions:secrets:set command - Auto-detect JSON format from .json file extensions - Added format field to SecretParam interface for deploy-time handling - Use visible input() prompt for JSON secrets vs password() for regular secrets - Validate JSON format before storing in Secret Manager - Improved error messages with actionable commands for developers - Added non-interactive mode check with helpful error for missing secrets Example usage: firebase functions:secrets:set STRIPE_CONFIG --format=json --data-file config.json cat config.json | firebase functions:secrets:set STRIPE_CONFIG --format=json
1 parent 33c310e commit b6cb593

File tree

3 files changed

+81
-11
lines changed

3 files changed

+81
-11
lines changed

src/commands/functions-secrets-set.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as clc from "colorette";
22

33
import { logger } from "../logger";
4+
import { FirebaseError } from "../error";
45
import { ensureValidKey, ensureSecret } from "../functions/secrets";
56
import { Command } from "../command";
67
import { requirePermissions } from "../requirePermissions";
@@ -36,15 +37,41 @@ export const command = new Command("functions:secrets:set <KEY>")
3637
"--data-file <dataFile>",
3738
'file path from which to read secret data. Set to "-" to read the secret data from stdin',
3839
)
40+
.option("--format <format>", "format of the secret value. 'string' (default) or 'json'")
3941
.action(async (unvalidatedKey: string, options: Options) => {
4042
const projectId = needProjectId(options);
4143
const projectNumber = await needProjectNumber(options);
4244
const key = await ensureValidKey(unvalidatedKey, options);
4345
const secret = await ensureSecret(projectId, key, options);
46+
47+
// Determine format using priority order: explicit flag > file extension > default to string
48+
let format = options.format as string | undefined;
49+
const dataFile = options.dataFile as string | undefined;
50+
if (!format && dataFile && dataFile !== "-") {
51+
// Auto-detect from file extension
52+
if (dataFile.endsWith(".json")) {
53+
format = "json";
54+
}
55+
}
56+
4457
const secretValue = await readSecretValue(
4558
`Enter a value for ${key}`,
46-
options.dataFile as string | undefined,
59+
dataFile,
4760
);
61+
62+
if (format === "json") {
63+
try {
64+
JSON.parse(secretValue);
65+
} catch (e: any) {
66+
throw new FirebaseError(
67+
`Provided value for ${key} is not valid JSON.\n\n` +
68+
`For complex JSON values, use:\n` +
69+
` firebase functions:secrets:set ${key} --format=json --data-file <file.json>\n` +
70+
`Or pipe from stdin:\n` +
71+
` cat config.json | firebase functions:secrets:set ${key} --format=json`,
72+
);
73+
}
74+
}
4875
const secretVersion = await addVersion(projectId, key, secretValue);
4976
logSuccess(`Created a new secret version ${toSecretVersionResourceName(secretVersion)}`);
5077

src/deploy/functions/params.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ export interface ListParam extends ParamBase<string[]> {
169169
delimiter?: string;
170170
}
171171

172-
export interface TextInput<T> { // eslint-disable-line
172+
export interface TextInput<T> {
173+
// eslint-disable-line
173174
text: {
174175
example?: string;
175176

@@ -224,6 +225,9 @@ interface SecretParam {
224225
// A long description of the parameter's purpose and allowed values. If omitted, UX will not
225226
// provide a description of the parameter
226227
description?: string;
228+
229+
// The format of the secret, e.g. "json"
230+
format?: string;
227231
}
228232

229233
export type Param = StringParam | IntParam | BooleanParam | ListParam | SecretParam;
@@ -390,6 +394,23 @@ export async function resolveParams(
390394
}
391395

392396
const [needSecret, needPrompt] = partition(outstanding, (param) => param.type === "secret");
397+
398+
// Check for missing secrets in non-interactive mode
399+
if (nonInteractive && needSecret.length > 0) {
400+
const secretNames = needSecret.map((p) => p.name).join(", ");
401+
const commands = needSecret
402+
.map(
403+
(p) =>
404+
`\tfirebase functions:secrets:set ${p.name}${(p as SecretParam).format === "json" ? " --format=json --data-file <file.json>" : ""}`,
405+
)
406+
.join("\n");
407+
throw new FirebaseError(
408+
`In non-interactive mode but have no value for the following secrets: ${secretNames}\n\n` +
409+
"Set these secrets before deploying:\n" +
410+
commands,
411+
);
412+
}
413+
393414
// The functions emulator will handle secrets
394415
if (!isEmulator) {
395416
for (const param of needSecret) {
@@ -398,7 +419,7 @@ export async function resolveParams(
398419
}
399420

400421
if (nonInteractive && needPrompt.length > 0) {
401-
const envNames = outstanding.map((p) => p.name).join(", ");
422+
const envNames = needPrompt.map((p) => p.name).join(", ");
402423
throw new FirebaseError(
403424
`In non-interactive mode but have no value for the following environment variables: ${envNames}\n` +
404425
"To continue, either run `firebase deploy` with an interactive terminal, or add values to a dotenv file. " +
@@ -460,17 +481,39 @@ function populateDefaultParams(config: FirebaseConfig): Record<string, ParamValu
460481
* to read its environment variables. They are instead provided through GCF's own
461482
* Secret Manager integration.
462483
*/
463-
async function handleSecret(secretParam: SecretParam, projectId: string) {
484+
async function handleSecret(secretParam: SecretParam, projectId: string): Promise<void> {
464485
const metadata = await secretManager.getSecretMetadata(projectId, secretParam.name, "latest");
465486
if (!metadata.secret) {
466-
const secretValue = await password({
467-
message: `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${
468-
secretParam.name
469-
}. Enter a value for ${secretParam.label || secretParam.name}:`,
470-
});
487+
let secretValue: string;
488+
const promptMessage = `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${
489+
secretParam.name
490+
}. Enter ${secretParam.format === "json" ? "a JSON value" : "a value"} for ${
491+
secretParam.label || secretParam.name
492+
}:`;
493+
494+
if (secretParam.format === "json") {
495+
secretValue = await input({
496+
message: promptMessage,
497+
});
498+
try {
499+
JSON.parse(secretValue);
500+
} catch (e: any) {
501+
throw new FirebaseError(
502+
`Provided value for ${secretParam.name} is not valid JSON.\n\n` +
503+
`For complex JSON values, use:\n` +
504+
` firebase functions:secrets:set ${secretParam.name} --format=json --data-file <file.json>\n` +
505+
`Or pipe from stdin:\n` +
506+
` cat config.json | firebase functions:secrets:set ${secretParam.name} --format=json`,
507+
);
508+
}
509+
} else {
510+
secretValue = await password({
511+
message: promptMessage,
512+
});
513+
}
471514
await secretManager.createSecret(projectId, secretParam.name, secretLabels());
472515
await secretManager.addVersion(projectId, secretParam.name, secretValue);
473-
return secretValue;
516+
return;
474517
} else if (!metadata.secretVersion) {
475518
throw new FirebaseError(
476519
`Cloud Secret Manager has no latest version of the secret defined by param ${

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@ export function generatePassword(n = 20): string {
890890

891891
/**
892892
* Reads a secret value from either a file or a prompt.
893-
* If dataFile is falsy and this is a tty, uses prompty. Otherwise reads from dataFile.
893+
* If dataFile is falsy and this is a tty, uses prompt. Otherwise reads from dataFile.
894894
* If dataFile is - or falsy, this means reading from file descriptor 0 (e.g. pipe in)
895895
*/
896896
export function readSecretValue(prompt: string, dataFile?: string): Promise<string> {

0 commit comments

Comments
 (0)