Skip to content

functions:kits:uninstall but not terrible - #10981

Open
Berlioz wants to merge 10 commits into
mainfrom
vsfan_kits_uninstall
Open

functions:kits:uninstall but not terrible#10981
Berlioz wants to merge 10 commits into
mainfrom
vsfan_kits_uninstall

Conversation

@Berlioz

@Berlioz Berlioz commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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:

vsfan-mac:kits-test vsfan$ firebaselocal functions:kits:uninstall --kit example-firebase-kit-hello-world
✔ Uninstall will delete all active instances of this kit. You have the following active instances that will be deleted:
asdf,jkl
Do you want to continue with uninstall (y/N)? Yes
✔ You are about to delete the following Cloud Functions:
    asdf:kit-asdf-helloWorld(us-central1)
    asdf:kit-asdf-helloWorldCallable(us-central1)
  Are you sure? Yes
i  functions: deleting Node.js 24 (2nd Gen) function asdf:kit-asdf-helloWorldCallable(us-central1)...
i  functions: deleting Node.js 24 (2nd Gen) function asdf:kit-asdf-helloWorld(us-central1)...
✔  functions[asdf:kit-asdf-helloWorldCallable(us-central1)] Successful delete operation.
✔  functions[asdf:kit-asdf-helloWorld(us-central1)] Successful delete operation.
⚠  functions: Kits instance config directory function-kits/example-firebase-kit-hello-world/config-asdf still contains files (bar,bar.json,baz,foo,foo.txt, ...4 more); not deleting automatically
✔  Wrote configuration info to firebase.json
✔ You are about to delete the following Cloud Functions:
    jkl:kit-jkl-helloWorld(us-east1)
    jkl:kit-jkl-helloWorldCallable(us-east1)
  Are you sure? Yes
i  functions: deleting Node.js 24 (2nd Gen) function jkl:kit-jkl-helloWorldCallable(us-east1)...
i  functions: deleting Node.js 24 (2nd Gen) function jkl:kit-jkl-helloWorld(us-east1)...
✔  functions[jkl:kit-jkl-helloWorld(us-east1)] Successful delete operation.
✔  functions[jkl:kit-jkl-helloWorldCallable(us-east1)] Successful delete operation.
✔  Wrote configuration info to firebase.json
✔  Wrote configuration info to firebase.json

Stuff currently slated for fast-follow unless someone makes me do it in this PR:

  • What about .env.alias
  • Moving shared code between this and functions:delete into a shared helper
  • What about kits/instances that were never deployed, or whose cloud resources are in a bad state

@wiz-9635d3485b

wiz-9635d3485b Bot commented Aug 24, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 2 Medium 5 Low
Software Management Finding Software Management Findings -
Total 2 Medium 5 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/commands/functions-kits-uninstall.ts
Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts
Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts
Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts Outdated

@ajperel ajperel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts Outdated

/*
* 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/commands/functions-kits-uninstall.ts
Comment thread src/commands/functions-kits-uninstall.ts Outdated
const projectId = fileName.replace(new RegExp("^.env."), "");
await uninstallProjectInstance(options, config, projectId, instanceId, kitInstancePath);
}
config.deleteProjectDir(kitInstancePath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/commands/functions-kits-uninstall.ts Outdated

@ajperel ajperel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More comments from tools repo review skill

Comment thread src/config.ts
fs.removeSync(this.path(p));
}

deleteProjectDir(p: string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [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 method

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread src/config.ts
Comment thread src/config.ts
Comment thread src/commands/functions-kits-uninstall.ts
Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts Outdated
if (instanceConfigDirPath === "") {
throw new FirebaseError(`Instance ID ${instanceId} not found in firebase.json`);
}
kitForInstance = kitForInstance!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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`);
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .find() idea was, frankly, a lot better than this.

Comment thread src/commands/functions-kits-uninstall.ts Outdated
Comment thread src/commands/functions-kits-uninstall.ts Outdated
import { Config } from "../config";
import { listKitConfigs } from "../functions/kits/config";
import { Options } from "../options";
import { join } from "path";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 ajperel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took me a sec to realize "5" is the char count for .env., can you write a short comment about why that value?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants