Integrate .env secret bindings with param prompting#10783
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for writing backing resource bindings for Functions secret parameters to .env files under the secretEnvParams experiment. The changes include updating resolveBackend and resolveParams to return resolved secret references, adding new fields to SecretParam, and implementing writeResolvedSecretRefs to persist these bindings. The review feedback highlights critical issues regarding case-sensitivity mismatches between camelCase secret names and uppercase .env keys in both applyEnvSecretBindings and writeResolvedSecretRefs, which could lead to deployment failures or duplicate keys. Additionally, the feedback suggests querying the specific pinned secret version instead of hardcoding 'latest' when retrieving secret metadata.
| for (const secret of build.params.filter((param) => param.type === "secret")) { | ||
| if (secret.name === key) { | ||
| secret.resourceId = secretId; | ||
| secret.version = version; | ||
| secret.inLocalEnvironment = true; | ||
| } | ||
| } |
There was a problem hiding this comment.
Using .filter() here loses the TypeScript type narrowing, meaning secret is still typed as the union Param rather than SecretParam. This will cause a compilation error when attempting to assign secret.resourceId, secret.version, and secret.inLocalEnvironment under strict null/type checks.
Additionally, comparing secret.name === key is case-sensitive. Since .env keys are validated to be uppercase (e.g., APIKEY), but secret parameter names can be camelCase (e.g., apiKey), this comparison will fail to match.
We can resolve both issues and optimize performance by using a simple for...of loop with case-insensitive comparison and type narrowing.
| for (const secret of build.params.filter((param) => param.type === "secret")) { | |
| if (secret.name === key) { | |
| secret.resourceId = secretId; | |
| secret.version = version; | |
| secret.inLocalEnvironment = true; | |
| } | |
| } | |
| for (const param of build.params) { | |
| if (param.type === "secret" && param.name.toUpperCase() === key.toUpperCase()) { | |
| param.resourceId = secretId; | |
| param.version = version; | |
| param.inLocalEnvironment = true; | |
| } | |
| } |
References
- TypeScript: Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. Use strict null checks and handleundefined/nullexplicitly. (link)
There was a problem hiding this comment.
Is that...true? I mean the case thing definitely is true, fixed.
| const version = secretParam.version || "latest"; | ||
| let secretAlreadyExisted = false; | ||
|
|
||
| const metadata = await secretManager.getSecretMetadata(projectId, resourceId, "latest"); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| export function writeResolvedSecretRefs( | ||
| resolvedSecretRefs: Readonly<Record<string, string>>, | ||
| haveSecretRefs: Readonly<Record<string, string>>, | ||
| userEnvOpt: UserEnvsOpts, | ||
| ): void { | ||
| const toWrite: Record<string, string> = {}; | ||
|
|
||
| for (const secretName of Object.keys(resolvedSecretRefs)) { | ||
| const resolvedRef = resolvedSecretRefs[secretName]; | ||
| if (!Object.prototype.hasOwnProperty.call(haveSecretRefs, secretName)) { | ||
| const reservedKey = "FIREBASE_SECRET_REF_" + secretName.toUpperCase(); | ||
| toWrite[reservedKey] = resolvedRef; | ||
| } | ||
| } | ||
|
|
||
| writeUserEnvs(toWrite, userEnvOpt); | ||
| } |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
| } | ||
|
|
||
| const secretRefString = | ||
| typeof version === "undefined" ? "resourceId" : `${resourceId}:${version}`; |
There was a problem hiding this comment.
I don't think you mean resourceId to be quoted here
There was a problem hiding this comment.
sad trombone noises
| }. Enter ${secretParam.format === "json" ? "a JSON value" : "a value"} for ${ | ||
| if (experiments.isEnabled("secretEnvParams")) { | ||
| // TODO: Move the explanation and link to Cloud Secret Manager in the next prompt here once this makes it out of experimental. | ||
| resourceId = await input({ |
There was a problem hiding this comment.
- Did we want to default to the existing behavior if the secret doesn't already exist (YOLO secret = key)?
- I thought we wanted to confirm loop before reusing a secret
- If reusing a secret we shouldn't needt o create & add a version.
There was a problem hiding this comment.
e:
- I don't think so--we talked it over in chat and this would break flows where the user had multiple environments deployed in the same project. Added --force support though.
- I thought so too but I was working out the logic in my head and right now, if the secret already exists and was created manually or with secrets:set or whatever, we would just pull it in without asking, right? This doesn't actually change the current behavior of secrets?
- Ack. Recursing like this seems like the most elegant way of handling the input-a-reused-secret case.
| } is in illegal state ${metadata.secretVersion.state}`, | ||
| ); | ||
| } else { | ||
| secretAlreadyExisted = true; |
There was a problem hiding this comment.
Am I reading diffs wrong? It looks like this is set if the experiment is false?
There was a problem hiding this comment.
Staring at my monitor for a bit and trusting vs code's brace matching, I'm pretty sure this is the fall-through for the big "what's the state of the referenced secret" if block that begins if (!metadata.secret)
| } | ||
| } | ||
|
|
||
| writeUserEnvs(toWrite, userEnvOpt); |
There was a problem hiding this comment.
is it safe to call writeUserEnvs repeatedly in different places? Does this append? If so, maybe wee should rename it appendUserEnvs
There was a problem hiding this comment.
It appends yeah. I did want to maintain some sense that this is a filesystem touch when skimming the code though, maybe writeNewUserEnvs()?
41c5985 to
5a993d6
Compare
This PR centers around changes made to the handleSecret()/now ensureSecret() method invoked during param resolution. Formerly, handleSecret() only made sure that a Cloud Secret with a resource name matching the name of the Secret Param existed with a readable latest version.
Now:
Intended behavior after merging this PR on deploy: