firebase-tools
Version:
Command-Line Interface for Firebase
267 lines (266 loc) • 12.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.command = void 0;
const requireConfig_1 = require("../requireConfig");
const command_1 = require("../command");
const config_1 = require("../functions/kits/config");
const path_1 = require("path");
const error_1 = require("../error");
const prompt_1 = require("../prompt");
const functional_1 = require("../functional");
const posix_1 = require("path/posix");
const projectUtils_1 = require("../projectUtils");
const utils_1 = require("../utils");
const backend = require("../deploy/functions/backend");
const planner = require("../deploy/functions/release/planner");
const executor = require("../deploy/functions/release/executor");
const fabricator = require("../deploy/functions/release/fabricator");
const functionsDeployHelper_1 = require("../deploy/functions/functionsDeployHelper");
const functionsConfig = require("../functionsConfig");
const getProjectNumber_1 = require("../getProjectNumber");
const reporter = require("../deploy/functions/release/reporter");
const logger_1 = require("../logger");
exports.command = new command_1.Command("functions:kits:uninstall")
.description("uninstall a function kit or kit instance from your project")
.before(requireConfig_1.requireConfig)
.option("--kit <kitId>", "")
.option("--instance <instanceId>", "")
.action(async (options) => {
const firebaseConfig = options.config;
if (options.instance && options.kit) {
throw new error_1.FirebaseError("Cannot specify both --kit and --instance. Please specify only one.");
}
if (!options.instance && !options.kit) {
throw new error_1.FirebaseError("Must specify either --kit or --instance.");
}
if (typeof options.instance === "string") {
await handleInstance(options, firebaseConfig);
}
if (typeof options.kit === "string") {
await handleKit(options, firebaseConfig);
}
});
async function handleKit(options, config) {
const kitId = options.kit;
const kits = (0, config_1.listKitConfigs)(config.src);
const kitConfig = kits.find((k) => k.kit === kitId);
if (!kitConfig) {
throw new error_1.FirebaseError(`Kit ${kitId} not found in firebase.json. Use functions:kits:list to get all existing kit IDs.`);
}
const conservativeDeletion = nonstandardKitLayout(kitConfig);
logger_1.logger.debug(`Kit ${kitConfig.kit} uninstall mode: conservativeDeletion=${conservativeDeletion}`);
if (Object.keys(kitConfig.instances).length > 0) {
const willBlindDeleteMsg = conservativeDeletion
? ""
: " and permanently remove their source code and configuration";
if (!(await (0, prompt_1.confirm)({
message: `Uninstall will delete all active instances of this kit${willBlindDeleteMsg}. You have the following active instances that will be deleted:
${Object.keys(kitConfig.instances).join(",")}
Do you want to continue with uninstall (y/N)?`,
default: false,
nonInteractive: options.nonInteractive,
force: options.force,
}))) {
return;
}
}
await uninstallKit(options, config, kitConfig);
return;
}
async function handleInstance(options, config) {
const instanceId = options.instance;
const projectId = (0, projectUtils_1.needProjectId)(options);
const kits = (0, config_1.listKitConfigs)(config.src);
const kitForInstance = kits.find((k) => instanceId in k.instances);
if (typeof kitForInstance === "undefined") {
throw new error_1.FirebaseError(`Instance ID ${instanceId} not found in firebase.json`);
}
const instanceConfigDirPath = kitForInstance.instances[instanceId];
const conservativeDeletion = nonstandardKitLayout(kitForInstance);
logger_1.logger.debug(`Kit ${kitForInstance.kit} uninstall mode: conservativeDeletion=${conservativeDeletion}`);
const projectsWithConfigs = [];
const configDirContents = config.lsProjectDir(instanceConfigDirPath);
const fileNames = configDirContents.filter((f) => f.isFile()).map((f) => f.name);
for (const fileName of fileNames) {
if (!fileName.startsWith(".env.")) {
continue;
}
projectsWithConfigs.push(fileName.slice(".env.".length));
}
if (projectsWithConfigs.length > 0 && !projectsWithConfigs.includes(projectId)) {
throw new error_1.FirebaseError(`Instance at ${instanceConfigDirPath} contains no .env file for current project`);
}
if (projectsWithConfigs.length === 0 || projectsWithConfigs.length === 1) {
if (Object.keys(kitForInstance.instances).length > 1) {
await uninstallInstance(options, config, instanceId, instanceConfigDirPath, conservativeDeletion);
return;
}
const confirmMsg = `This is the last remaining instance for kit ${kitForInstance.kit}. This will delete all source and configuration code. Proceed? (y/N)`;
if (!(await (0, prompt_1.confirm)({
message: confirmMsg,
default: false,
nonInteractive: options.nonInteractive,
force: options.force,
}))) {
return;
}
await uninstallKit(options, config, kitForInstance);
return;
}
await uninstallProjectInstance(options, config, projectId, instanceId, instanceConfigDirPath);
}
async function uninstallInstance(options, config, instanceId, instanceConfigDirPath, onlyDeleteEmpty) {
const configDirContents = config.lsProjectDir(instanceConfigDirPath);
const fileNames = configDirContents.filter((f) => f.isFile()).map((f) => f.name);
for (const fileName of fileNames) {
if (!fileName.startsWith(".env.")) {
continue;
}
const projectId = fileName.replace(new RegExp("^.env."), "");
await uninstallProjectInstance(options, config, projectId, instanceId, instanceConfigDirPath);
}
if (!onlyDeleteEmpty || configDirEmpty(config, instanceConfigDirPath)) {
config.deleteProjectDir(instanceConfigDirPath);
}
let functionsConfig = config.src.functions ?? [];
if (!Array.isArray(functionsConfig)) {
functionsConfig = [functionsConfig];
}
for (const stanza of functionsConfig) {
if (typeof stanza.kit === "undefined") {
continue;
}
const instances = stanza.instances ?? {};
for (const configInstanceId of Object.keys(instances)) {
if (configInstanceId === instanceId) {
delete stanza.instances[instanceId];
}
}
}
config.set("functions", functionsConfig);
config.writeProjectFile("firebase.json", config.src);
}
async function uninstallProjectInstance(options, config, projectId, instanceId, kitInstancePath) {
const envFilePath = (0, path_1.join)(kitInstancePath, `.env.${projectId}`);
const context = {
projectId: projectId,
filters: [{ codebase: instanceId }],
};
const haveBackend = await backend.existingBackend(context);
const plan = await planner.createDeploymentPlan({
wantBackend: backend.empty(),
haveBackend: haveBackend,
codebase: "",
projectId: context.projectId,
filters: context.filters,
deleteAll: true,
});
const allEpToDelete = Object.values(plan.regionalChangesets)
.map((changes) => changes.endpointsToDelete)
.reduce(functional_1.reduceFlat, [])
.sort(backend.compareFunctions);
if (allEpToDelete.length > 0) {
const deleteList = allEpToDelete.map((func) => `\t${(0, functionsDeployHelper_1.getFunctionLabel)(func)}`).join("\n");
const confirmDeletion = await (0, prompt_1.confirm)({
message: "You are about to delete the following Cloud Functions:\n" +
deleteList +
"\n Are you sure?",
default: false,
force: options.force,
nonInteractive: options.nonInteractive,
});
if (!confirmDeletion) {
throw new error_1.FirebaseError("Command aborted.");
}
const functionExecutor = new executor.QueueExecutor({
retries: 30,
backoff: 20000,
concurrency: 40,
maxBackoff: 40000,
});
const appEngineLocation = functionsConfig.getAppEngineLocation(config);
try {
const fab = new fabricator.Fabricator({
functionExecutor,
runFunctionExecutor: functionExecutor,
appEngineLocation,
executor: new executor.QueueExecutor({}),
sources: {},
projectNumber: await (0, getProjectNumber_1.getProjectNumber)({ projectId: context.projectId }),
projectId: context.projectId,
});
const summary = await fab.applyPlan({ default: plan });
await reporter.logAndTrackDeployStats(summary);
reporter.printErrors(summary);
if (summary.results.some((r) => r.error)) {
throw new error_1.FirebaseError("Some functions deletions failed. Not modifying firebase.json.");
}
}
catch (err) {
throw new error_1.FirebaseError("Failed to delete functions", {
original: err,
exit: 1,
});
}
}
else {
logger_1.logger.info(`No deployed functions found for instance ${instanceId}. This is normal if firebase deploy was never run.`);
}
config.deleteProjectFile(envFilePath);
}
async function uninstallKit(options, config, kit) {
const conservativeDeletion = nonstandardKitLayout(kit);
for (const [instanceId, instanceConfigDir] of Object.entries(kit.instances)) {
await uninstallInstance(options, config, instanceId, instanceConfigDir, conservativeDeletion);
}
if (conservativeDeletion) {
const instanceConfigDirParents = Object.values(kit.instances).map((p) => (0, posix_1.dirname)(p));
if (instanceConfigDirParents.length > 0 &&
instanceConfigDirParents.every((p) => p === instanceConfigDirParents[0]) &&
configDirEmpty(config, instanceConfigDirParents[0])) {
config.deleteProjectDir(instanceConfigDirParents[0]);
}
}
else {
const kitRootDirs = [
(0, posix_1.dirname)(kit.source),
...Object.values(kit.instances).map((p) => (0, posix_1.dirname)(p)),
];
if (!kitRootDirs.every((p) => p === kitRootDirs[0])) {
throw new error_1.FirebaseError(`aborting kit uninstall: couldn't infer one kit root directory (${[...new Set(kitRootDirs)].join(", ")} in firebase.json)`);
}
config.deleteProjectDir(kitRootDirs[0]);
}
let functionsConfig = config.src.functions ?? [];
if (!Array.isArray(functionsConfig)) {
functionsConfig = [functionsConfig];
}
functionsConfig = functionsConfig.filter((fc) => fc.kit !== kit.kit);
config.set("functions", functionsConfig);
config.writeProjectFile("firebase.json", config.src);
}
function nonstandardKitLayout(kitConfig) {
if (kitConfig.sourcePackage) {
if (kitConfig.source !== `function-kits/${kitConfig.kit}/source`) {
return true;
}
for (const [instanceId, instanceConfigDirPath] of Object.entries(kitConfig.instances)) {
if (instanceConfigDirPath !== `function-kits/${kitConfig.kit}/config-${instanceId}`) {
return true;
}
}
return false;
}
return true;
}
function configDirEmpty(config, projectRelativePath) {
const contents = config.lsProjectDir(projectRelativePath).map((dirent) => dirent.name);
if (contents.length > 0) {
const digest = contents.length > 5
? `${contents.slice(0, 5).join(",")}, ...${contents.length - 5} more`
: contents.join(",");
(0, utils_1.logLabeledWarning)("functions", `Kits instance config directory ${projectRelativePath} still contains files (${digest}); not deleting automatically`);
return false;
}
return true;
}