Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for deliberately unset secrets (such as optional secrets in Extensions) during deployment and export. It updates secret parsing, environment binding, and parameter handling to skip prompting and prevent deployment errors for these unset secrets. The review feedback highlights an issue in applyEnvSecretBindings where a non-array secretEnvironmentVariables could bypass the unset check and trigger a spurious warning; restructuring the condition to always perform an early continue when unset is true is recommended.
| force?: boolean, | ||
| nonInteractive: boolean, | ||
| force: boolean, | ||
| recursive = false, |
There was a problem hiding this comment.
So this is deranged, and probably a sign that this function needs some serious refactoring, but I don't see an immediately better way to implement the new prompting UX we agreed on.
The issue is that we recursively call this function in the case where the resourceId is blank and we prompt the user to enter one (currently that's only when secretEnvParams experiment is on, but presumably that will be the default soon). We do this because if the user enters the name of a secret that already exists, we want to take advantage of all the does-the-secret-exist/does-the-version-exist/is-the-secret-in-a-bad-state logic this function gives us.
This means that the the value for this secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing)... string will be printed twice in that case, because of the way we reordered the messages being printed.
| if (secretParam.unset) { | ||
| return ""; | ||
| } | ||
| secretParam.optional = true; |
There was a problem hiding this comment.
Is this intentional? the bot thinks its a bug:
Accidental forced optionality in ensureSecret() (src/deploy/functions/params.ts:294):
secretParam.optional = true; //
There was a problem hiding this comment.
Impact: Line 294 unconditionally mutates secretParam.optional = true for all secrets passing through ensureSecret(). This causes every standard required secret to display (Optional) and ; enter nothing to skip, allowing developers to inadvertently skip required production secrets during firebase deploy.
There was a problem hiding this comment.
It's a (communicated to me at least) hack for local testing until Victor makes the change to firebase-functions that enables us to set real optional values. Definitely a bad bug if it was something we were checking in, but it will be removed before we merge.
The real order will be update Functions SDK. Revert this hack, and instead use that to have real optional secrets while doing manual testing for this PR. And then land (after addressing any other substantive comments)
There was a problem hiding this comment.
Yeah sorry I mentioned I was doing this in the CF3 chat room, should have added a comment or something to the code
ajperel
left a comment
There was a problem hiding this comment.
AI had a point that if "secretEnvParams" experiment is off optional secrets will break cause we'll never write REF= to the .env file. We should probably force all secrets to be required if secretEnvParams is off and like log a warning to turn on the experiment if we encounter an optional param while it's off.
That said we'll turn the experiment on by default in the next release and probably not turn it off... but we can't control what users do so as long as we have the experiment we should do that defensively. Otherwise folks could get broken. I could see someone turning it off if they just want the old behavior and not to be prompted for secret resource locations.
I will do another review tomorrow morning to try to understand this more deeply myself, this pass was heavily aided by AI.
| if (secretParam.format === "json") { | ||
| validateJsonSecret(secretParam.name, secretValue); | ||
| } | ||
| if (secretValue === "" && secretParam.optional) { |
There was a problem hiding this comment.
We should probably do this before we validateJsonSecret right? Because if it's empty it won't be valid JSON.
And we don't need to handle undefined here for JSON cause in this instance prompting returns the empty string and we write and empty ref... it's in functions getter that we do undefined?
| resolvedSecretRefs: Record<string, string>, | ||
| ) { | ||
| const missingSecrets: string[] = []; | ||
| for (const [secretName, secretBinding] of Object.entries(resolvedSecretRefs)) { |
There was a problem hiding this comment.
AI says secretName might be lowercase but secretEnvVar might be uppercase and we could fail to unbind below if cases differ. should we normalize everything to uppcase case just to be safe.
but in at least one other place this didn't seem to be a real thing. Can we hit a problem if users do:
defineSecret("my_secret") instead of defineSecret("MY_SECRET") or something?
| * Removes any Secret Params with a resolved reference of "", corresponding to an Optional secret that the user has declined to create, | ||
| * from the SecretEnvironmentVariables that the Functions backend will be provided. | ||
| */ | ||
| export function unbindMissingOptionalSecrets( |
There was a problem hiding this comment.
Please fix the warning for missing return type since this is a new function.
| } | ||
| let label = secretParam.label || secretParam.name; | ||
| if (!recursive) { | ||
| if (secretParam.optional) { |
There was a problem hiding this comment.
AI says there is a small bug in and extra (optional) in the value prompt in case A.
Case A: Default (secretEnvParams is OFF)
GOOGLE_API_KEY_FOO (Optional): The value for this secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as GOOGLE_API_KEY_FOO.
? Enter a value for GOOGLE_API_KEY_FOO (Optional); enter nothing to skip:
Case B: Experimental (secretEnvParams is ON)
GOOGLE_API_KEY_FOO (Optional): The value for this secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as GOOGLE_API_KEY_FOO.
✔ What resource ID do you want to use for the backing Secret resource for secret param GOOGLE_API_KEY_FOO? GOOGLE_API_KEY_FOO
? Enter a value for GOOGLE_API_KEY_FOO; enter nothing to skip:
And that the fix is to do:
if (!recursive) {
const headerLabel = secretParam.optional ? `${label} (Optional)` : label;
const notice = `The value for this secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${resourceId}.`;
const desc = secretParam.description ? `${secretParam.description} ${notice}` : notice;
logger.info(`\n${clc.bold(headerLabel)}: ${(await marked(desc)).trim()}`);
}
This is tiny, And we'll probably tweak this again if we pull out prompting so I don't feel strongly about it.
Support the
optionalfield on SecretParams, indicating that the user can decline to bind the SecretParam to a Cloud Secret Manager resource. In this case, the runtime value of the Secret is undefined. This is backed by a new binding type in .env files: it is now legal to bind optional param SECRET_REFs to the empty string.src/extensions/export.ts: Writes "" to .env for secrets defined in the spec but not present in live. Also skips the unejected secret checks for thos esecrets.
src/deploy/functions/build.ts: Introduces a new
unsetfield on ParsedSecretRef and modifies secret/param binding behavior if this field is true. I experimented with using typescript unions to ensure thatunsetandsecretIdcan't both be set at the same time and it worked but honestly it was really ugly.src/deploy/functions/params.ts: Skips the ensureSecret() flow entirely if the
unsetfield has propagated from ParsedSecretRef. Allows users to choose not to create a Secret resource foroptionalparams, which results in the empty string being written to .env.src/deploy/functions/prepare.ts and src/deploy/functions/backend.ts: If a secret came back from param resolution with an empty string binding, redact it from the Backend's SecretEnvironmentVariables, which control which Secret resources are attached to the function's backend.
UX example: