UNPKG

firebase-tools

Version:
267 lines (266 loc) 12.2 kB
"use strict"; 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; }