functions:kits:uninstall but not terrible - #10981
Conversation
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
There was a problem hiding this comment.
Code Review
This pull request introduces the functions:kits:uninstall command to allow users to uninstall a function kit or kit instance from their Firebase project, along with adding directory-handling helper methods to the Config class. The review feedback is highly constructive and identifies several critical areas for improvement: preventing potential ENOENT crashes by checking directory existence before file operations, handling non-interactive environments when prompting for multiple projects, making the --kit and --instance options mutually exclusive, and ensuring local configurations are preserved if cloud deletion fails. Additionally, the feedback highlights style guide violations regarding strict null checks, unsafe non-null assertions, and the use of the any type.
ajperel
left a comment
There was a problem hiding this comment.
You've brought up a bunch of interesting edge cases around what to delete on disk. This probably maybe should have started as a more detailed doc or discussion but we can hash it out. It was more complicated that I realized.
|
|
||
| /* | ||
| * For each .env.<projectId> file present in a Kit instance config folder, destroy the | ||
| * Function (project = input, region = env.FIREBASE_FUNCTION_KIT_REGION, id = kitInstanceId) |
There was a problem hiding this comment.
Do we need region? Is it possible to deploy a kit instance to two different regions in the same project? I'm honestly not sure.
There was a problem hiding this comment.
Not anymore. That's a relic from when I thought I had to implement all the deletion logic myself instead of taking advantage of the codebase.
| const projectId = fileName.replace(new RegExp("^.env."), ""); | ||
| await uninstallProjectInstance(options, config, projectId, instanceId, kitInstancePath); | ||
| } | ||
| config.deleteProjectDir(kitInstancePath); |
There was a problem hiding this comment.
You're careful to filter out files that aren't .env files ...honestly hadn't thought about anyone creating those. If they have do you think we should still delete this directory? warn? error?
| instanceId: string, | ||
| kitInstancePath: string, | ||
| ): Promise<void> { | ||
| const envFilePath = join(kitInstancePath, `.env.${projectId}`); |
There was a problem hiding this comment.
What if folks are using an alias?
I am OK if we say handling aliases is a follow up PR FWIW
| projectId: projectId, | ||
| filters: [{ codebase: instanceId } as EndpointFilter], | ||
| }; | ||
| const haveBackend = await backend.existingBackend(context); |
There was a problem hiding this comment.
You are duplicating what functions:delete does here. Is there a reason not to refactor that command so you can re-use it here?
In the interests of time I could accept this, but I'd like us to unify and reduce tech debt in the near future.
There was a problem hiding this comment.
Not really needing the codebase disambiguation logic, I guess, and slightly different semantics around what messages we want to print to the user and what happens if some but not all of the deletion operations fail. Could definitely work around it, if time is available.
ajperel
left a comment
There was a problem hiding this comment.
More comments from tools repo review skill
| fs.removeSync(this.path(p)); | ||
| } | ||
|
|
||
| deleteProjectDir(p: string) { |
There was a problem hiding this comment.
🔴 [Critical Safety] Insufficient Directory Deletion Protection
Rationale: deleteProjectDir checks for /.. but does not prevent deleting the project directory itself (if passed . or resolving to project root) or arbitrary directories if passed absolute paths.
Suggested Fix:
deleteProjectDir(p: string) {
if (p.includes("/..")) {
throw new FirebaseError("sanity: refusing to delete project-relative dir containing '/..'");
}
const resolvedPath = path.resolve(this.path(p));
const resolvedProjectDir = path.resolve(this.projectDir);
if (!resolvedPath.startsWith(resolvedProjectDir + path.sep)) {
throw new FirebaseError("sanity: refusing to delete directory outside of project directory");
}
// ... rest of the methodThere was a problem hiding this comment.
I don't think this correct because of the guards in the config.path() helper itself, but the method should definitely reject any absolute paths.
...Also, is it necessarily wrong that this might be called on project root?
| if (instanceConfigDirPath === "") { | ||
| throw new FirebaseError(`Instance ID ${instanceId} not found in firebase.json`); | ||
| } | ||
| kitForInstance = kitForInstance!; |
There was a problem hiding this comment.
🟡 Nit: Avoid Non-Null Assertion
Rationale: kitForInstance = kitForInstance! can be avoided by checking if it is undefined after the loop.
Suggested Fix:
let kitForInstance: ValidatedKitSingle | undefined;
for (const kitConfig of kits) {
if (kitConfig.instances[instanceId]) {
kitForInstance = kitConfig;
instanceConfigDirPath = kitConfig.instances[instanceId];
break;
}
}
if (!kitForInstance) {
throw new FirebaseError(`Instance ID ${instanceId} not found in firebase.json`);
}There was a problem hiding this comment.
The .find() idea was, frankly, a lot better than this.
| import { Config } from "../config"; | ||
| import { listKitConfigs } from "../functions/kits/config"; | ||
| import { Options } from "../options"; | ||
| import { join } from "path"; |
There was a problem hiding this comment.
🟡 Nit: Inconsistent Path Imports
Rationale: Mixing path and path/posix imports can be confusing.
Suggested Fix: Use namespace import import * as path from "path" and access path.join and path.posix.dirname explicitly.
ajperel
left a comment
There was a problem hiding this comment.
Also good to follow the contribution guidelines on PR descriptions, etc.
| // If this is the case, blind deletions of directories associate with this kit could delete things the user did not intend to. | ||
| function nonstandardKitLayout(kitConfig: ValidatedKitSingle): boolean { | ||
| if (kitConfig.sourcePackage) { | ||
| if (kitConfig.source !== `function-kits/${kitConfig.kit}/source`) { |
There was a problem hiding this comment.
nit: could leverage FUNCTION_KITS_DIR https://github.com/firebase/firebase-tools/blob/main/src/functions/kits/install.ts#L40 to keep the kits directory consistent if it changes. maybe we could move that constant to a common.ts file. Makes me wonder if we should put these common file pattern paths in a constant as well. Ditto for configDir.
| if (!fileName.startsWith(".env.")) { | ||
| continue; | ||
| } | ||
| projectsWithConfigs.push(fileName.slice(5)); |
There was a problem hiding this comment.
took me a sec to realize "5" is the char count for .env., can you write a short comment about why that value?
Files changed:
src/config.ts: Extends some existing helper functions for filesystem operations with project root relative paths to also have options for directory-level operations
src/commands/functions-kits-uninstall.ts: Implements the actual command
src/commands/index.ts: Registers the command with the CLI, gated by the kits experiment
Scenarios tested:
Instance deletion (not last instance), instance deletion (last instance triggering kit deletion), direct kit deletion.
Conservative filesystem case tested by manually modifying nonstandardKitLayout() since directory kits don't exist yet
UX example:
Stuff currently slated for fast-follow unless someone makes me do it in this PR: