From 5fd8ae036dbceeca2eedf94c46db688839fdbaad Mon Sep 17 00:00:00 2001 From: Lauren Long Date: Thu, 19 Jul 2018 16:31:02 -0700 Subject: [PATCH] Ask for confirmation before deleting functions during 'firebase deploy' (#287) --- commands/functions-delete.js | 13 +- lib/deploy/functions/release.js | 212 ++++++++++++++++++++------------ 2 files changed, 144 insertions(+), 81 deletions(-) diff --git a/commands/functions-delete.js b/commands/functions-delete.js index c0ff18a7..37e2a695 100644 --- a/commands/functions-delete.js +++ b/commands/functions-delete.js @@ -15,6 +15,11 @@ var utils = require("../lib/utils"); module.exports = new Command("functions:delete [filters...]") .description("delete one or more Cloud Functions by name or group name.") + .option( + "--region ", + "Specify region of the function to be deleted. " + + "If omitted, functions from all regions whose names match the filters will be deleted. " + ) .option("-f, --force", "No confirmation. Otherwise, a confirmation prompt will appear.") .before(requireAccess, [scopes.CLOUD_PLATFORM]) .action(function(filters, options) { @@ -34,12 +39,14 @@ module.exports = new Command("functions:delete [filters...]") .listAll(projectId) .then(function(result) { var allFunctions = _.map(result, "name"); - return _.filter(allFunctions, function(functionName) { - return _.some( + return _.filter(allFunctions, function(name) { + var regionMatches = options.region ? helper.getRegion(name) === options.region : true; + var nameMatches = _.some( _.map(filterChunks, function(chunk) { - return helper.functionMatchesGroup(functionName, chunk); + return helper.functionMatchesGroup(name, chunk); }) ); + return regionMatches && nameMatches; }); }) .then(function(result) { diff --git a/lib/deploy/functions/release.js b/lib/deploy/functions/release.js index 102419de..822a282e 100644 --- a/lib/deploy/functions/release.js +++ b/lib/deploy/functions/release.js @@ -9,6 +9,7 @@ var logger = require("../../logger"); var track = require("../../track"); var utils = require("../../utils"); var helper = require("../../functionsDeployHelper"); +var prompt = require("../../prompt"); var CLI_DEPLOYMENT_TOOL = "cli-firebase"; var CLI_DEPLOYMENT_LABELS = { @@ -57,6 +58,55 @@ function _fetchTriggerUrls(projectId, ops, sourceUrl) { }); } +var printSuccess = function(op) { + _endTimer(op.func); + utils.logSuccess( + chalk.bold.green("functions[" + helper.getFunctionLabel(op.func) + "]: ") + + "Successful " + + op.type + + " operation. " + ); + if (op.triggerUrl && op.type !== "delete") { + logger.info( + chalk.bold("Function URL"), + "(" + helper.getFunctionName(op.func) + "):", + op.triggerUrl + ); + } +}; +var printFail = function(op) { + _endTimer(op.func); + failedDeployments += 1; + utils.logWarning( + chalk.bold.yellow("functions[" + helper.getFunctionLabel(op.func) + "]: ") + "Deployment error." + ); + if (op.error.code === 8) { + logger.debug(op.error.message); + logger.info( + "You have exceeded your deployment quota, please deploy your functions in batches by using the --only flag, " + + "and wait a few minutes before deploying again. Go to " + + chalk.underline("https://firebase.google.com/docs/cli/#deploy_specific_functions") + + " to learn more." + ); + } else { + logger.info(op.error.message); + } +}; + +var printTooManyOps = function() { + utils.logWarning( + chalk.bold.yellow("functions:") + " too many functions are being deployed, cannot poll status." + ); + logger.info( + "In a few minutes, you can check status at " + + utils.consoleUrl(options.project, "/functions/logs") + ); + logger.info( + "You can use the --only flag to deploy only a portion of your functions in the future." + ); + deployments = []; // prevents analytics tracking of deployments +}; + module.exports = function(context, options, payload) { if (!options.config.has("functions")) { return Promise.resolve(); @@ -170,12 +220,12 @@ module.exports = function(context, options, payload) { throw new FirebaseError( "Function " + chalk.bold(functionName) + - " was deployed using a legacy " + - "trigger type and cannot be updated with the new SDK. To proceed with this deployment, you must first delete the " + - "function by visiting the Cloud Console at: https://console.cloud.google.com/functions/list?project=" + - projectId + - "\n\nTo avoid service interruption, you may wish to create an identical function with a different name before " + - "deleting this function.\n" + " was deployed using a legacy trigger type and cannot be updated without deleting " + + "the previous function. Follow the instructions on " + + chalk.underline( + "https://firebase.google.com/docs/functions/manage-functions#modify-trigger" + ) + + " for how to change the trigger without losing events.\n" ); } else { _startTimer(name, "update"); @@ -200,7 +250,7 @@ module.exports = function(context, options, payload) { .value(); // Delete functions - _.chain(existingFunctions) + var functionsToDelete = _.chain(existingFunctions) .filter(function(functionInfo) { if (typeof functionInfo.labels === "undefined") { return ( @@ -213,30 +263,85 @@ module.exports = function(context, options, payload) { .map(pluckName) .difference(uploadedNames) .intersection(deleteReleaseNames) - .map(function(name) { - var functionName = helper.getFunctionName(name); - var region = helper.getRegion(name); - - utils.logBullet( - chalk.bold.cyan("functions: ") + - "deleting function " + - chalk.bold(helper.getFunctionLabel(name)) + - "..." - ); - _startTimer(name, "delete"); - deployments.push({ - name: name, - retryFunction: function() { - return gcp.cloudfunctions.delete({ - projectId: projectId, - region: region, - functionName: functionName, - }); - }, - }); - }) .value(); + if (functionsToDelete.length === 0) { + return Promise.resolve(); + } + var deleteList = _.map(functionsToDelete, function(func) { + return "\t" + helper.getFunctionLabel(func); + }).join("\n"); + + if (options.nonInteractive) { + var deleteCommands = _.map(functionsToDelete, function(func) { + return ( + "\tfirebase functions:delete " + + helper.getFunctionName(func) + + " --region " + + helper.getRegion(func) + ); + }).join("\n"); + + throw new FirebaseError( + "The following functions are found in your project but do not exist in your local source code:\n" + + deleteList + + "\n\nAborting because deletion cannot proceed in non-interactive mode. To fix, manually delete the functions by running:\n" + + chalk.bold(deleteCommands) + ); + } + + logger.info( + "\nThe following functions are found in your project but do not exist in your local source code:\n" + + deleteList + + "\n\nIf you are renaming a function or changing its region, it is recommended that you create the new " + + "function first before deleting the old one to prevent event loss. For more info, visit " + + chalk.underline( + "https://firebase.google.com/docs/functions/manage-functions#modify" + "\n" + ) + ); + + return prompt + .once({ + type: "confirm", + name: "confirm", + default: false, + message: + "Would you like to proceed with deletion? Selecting no will continue the rest of the deployments.", + }) + .then(function(proceed) { + if (!proceed) { + if (deployments.length !== 0) { + utils.logBullet( + chalk.bold.cyan("functions: ") + "continuing with other deployments." + ); + } + return; + } + functionsToDelete.forEach(function(name) { + var functionName = helper.getFunctionName(name); + var region = helper.getRegion(name); + + utils.logBullet( + chalk.bold.cyan("functions: ") + + "deleting function " + + chalk.bold(helper.getFunctionLabel(name)) + + "..." + ); + _startTimer(name, "delete"); + deployments.push({ + name: name, + retryFunction: function() { + return gcp.cloudfunctions.delete({ + projectId: projectId, + region: region, + functionName: functionName, + }); + }, + }); + }); + }); + }) + .then(function() { return utils.promiseAllSettled( _.map(deployments, function(op) { return op.retryFunction().then(function(res) { @@ -256,55 +361,6 @@ module.exports = function(context, options, payload) { .value(); failedDeployments += failedCalls.length; - var printSuccess = function(op) { - _endTimer(op.func); - utils.logSuccess( - chalk.bold.green("functions[" + helper.getFunctionLabel(op.func) + "]: ") + - "Successful " + - op.type + - " operation. " - ); - if (op.triggerUrl && op.type !== "delete") { - logger.info( - chalk.bold("Function URL"), - "(" + helper.getFunctionName(op.func) + "):", - op.triggerUrl - ); - } - }; - var printFail = function(op) { - _endTimer(op.func); - failedDeployments += 1; - utils.logWarning( - chalk.bold.yellow("functions[" + helper.getFunctionLabel(op.func) + "]: ") + - "Deployment error." - ); - if (op.error.code === 8) { - logger.debug(op.error.message); - logger.info( - "You have exceeded your deployment quota, please deploy your functions in batches by using the --only flag, " + - "and wait a few minutes before deploying again. Go to https://firebase.google.com/docs/cli/#partial_deploys to learn more." - ); - } else { - logger.info(op.error.message); - } - }; - - var printTooManyOps = function() { - utils.logWarning( - chalk.bold.yellow("functions:") + - " too many functions are being deployed, cannot poll status." - ); - logger.info( - "In a few minutes, you can check status at " + - utils.consoleUrl(options.project, "/functions/logs") - ); - logger.info( - "You can use the --only flag to deploy only a portion of your functions in the future." - ); - deployments = []; // prevents analytics tracking of deployments - }; - return _fetchTriggerUrls(projectId, successfulCalls, sourceUrl) .then(function() { return helper.pollDeploys(successfulCalls, printSuccess, printFail, printTooManyOps);