Ask for confirmation before deleting functions during 'firebase deploy' (#287)

This commit is contained in:
Lauren Long
2018-07-19 16:31:02 -07:00
parent 46511c52d4
commit 5fd8ae036d
2 changed files with 144 additions and 81 deletions

View File

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

View File

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