mirror of
https://github.com/zhigang1992/firebase-tools.git
synced 2026-01-12 22:47:24 +08:00
Support configuration of function region, memory, and timeout (#285)
This commit is contained in:
@@ -9,13 +9,145 @@ var logger = require("../../logger");
|
||||
var track = require("../../track");
|
||||
var utils = require("../../utils");
|
||||
var pollOperation = require("../../pollOperations");
|
||||
var helper = require("../../functionsDeployHelper");
|
||||
|
||||
var CLI_DEPLOYMENT_TOOL = "cli-firebase";
|
||||
var CLI_DEPLOYMENT_LABELS = {
|
||||
"deployment-tool": CLI_DEPLOYMENT_TOOL,
|
||||
};
|
||||
var timings = {};
|
||||
var deployments = [];
|
||||
var failedDeployments = 0;
|
||||
|
||||
function _pollAndManageOperations(operations) {
|
||||
var interval;
|
||||
// Poll less frequently when there are many operations to avoid hitting read quota.
|
||||
// See "Read requests" quota at https://cloud.google.com/console/apis/api/cloudfunctions/quotas
|
||||
if (_.size(operations) > 90) {
|
||||
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 Promise.resolve();
|
||||
} else if (_.size(operations) > 40) {
|
||||
interval = 10 * 1000;
|
||||
} else if (_.size(operations) > 15) {
|
||||
interval = 5 * 1000;
|
||||
} else {
|
||||
interval = 2 * 1000;
|
||||
}
|
||||
var pollFunction = gcp.cloudfunctions.check;
|
||||
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 retryCondition = function(result) {
|
||||
// The error codes from a Google.LongRunning operation follow google.rpc.Code format.
|
||||
|
||||
var retryableCodes = [
|
||||
1, // cancelled by client
|
||||
4, // deadline exceeded
|
||||
10, // aborted (typically due to concurrency issue)
|
||||
14, // unavailable
|
||||
];
|
||||
|
||||
if (_.includes(retryableCodes, result.error.code)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return pollOperation.pollAndRetry(
|
||||
operations,
|
||||
pollFunction,
|
||||
interval,
|
||||
printSuccess,
|
||||
printFail,
|
||||
retryCondition
|
||||
);
|
||||
}
|
||||
|
||||
function _startTimer(name, type) {
|
||||
timings[name] = { type: type, t0: process.hrtime() };
|
||||
}
|
||||
|
||||
function _endTimer(name) {
|
||||
if (!timings[name]) {
|
||||
logger.debug("[functions] no timer initialized for", name);
|
||||
return;
|
||||
}
|
||||
|
||||
// hrtime returns a duration as an array of [seconds, nanos]
|
||||
var duration = process.hrtime(timings[name].t0);
|
||||
track(
|
||||
"Functions Deploy (Duration)",
|
||||
timings[name].type,
|
||||
duration[0] * 1000 + Math.round(duration[1] * 1e-6)
|
||||
);
|
||||
}
|
||||
|
||||
function _fetchTriggerUrls(projectId, ops, sourceUrl) {
|
||||
if (!_.find(ops, ["trigger.httpsTrigger", {}])) {
|
||||
// No HTTPS functions being deployed
|
||||
return Promise.resolve();
|
||||
}
|
||||
// TODO make multi region
|
||||
return gcp.cloudfunctions.listAll(projectId).then(function(functions) {
|
||||
var httpFunctions = _.chain(functions)
|
||||
.filter({ sourceUploadUrl: sourceUrl })
|
||||
.filter("httpsTrigger")
|
||||
.value();
|
||||
_.forEach(httpFunctions, function(httpFunc) {
|
||||
_.chain(ops)
|
||||
.find({ func: httpFunc.name })
|
||||
.assign({ triggerUrl: httpFunc.httpsTrigger.url })
|
||||
.value();
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(context, options, payload) {
|
||||
if (!options.config.has("functions")) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var GCP_REGION = gcp.cloudfunctions.DEFAULT_REGION;
|
||||
var projectId = context.projectId;
|
||||
var sourceUrl = context.uploadUrl;
|
||||
// Used in CLI releases v3.4.0 to v3.17.6
|
||||
@@ -23,270 +155,60 @@ module.exports = function(context, options, payload) {
|
||||
"gs://" + "staging." + context.firebaseConfig.storageBucket + "/firebase-functions-source";
|
||||
// Used in CLI releases v3.3.0 and prior
|
||||
var legacySourceUrlOne = "gs://" + projectId + "-gcf/" + projectId;
|
||||
var CLI_DEPLOYMENT_TOOL = "cli-firebase";
|
||||
var CLI_DEPLOYMENT_LABELS = {
|
||||
"deployment-tool": CLI_DEPLOYMENT_TOOL,
|
||||
};
|
||||
|
||||
var functionsInfo = payload.functions.triggers;
|
||||
var functionsInfo = helper.getFunctionsInfo(payload.functions.triggers, projectId);
|
||||
var uploadedNames = _.map(functionsInfo, "name");
|
||||
var timings = {};
|
||||
var failedDeployments = 0;
|
||||
var deployments = [];
|
||||
|
||||
function _startTimer(name, type) {
|
||||
timings[name] = { type: type, t0: process.hrtime() };
|
||||
}
|
||||
|
||||
function _endTimer(name) {
|
||||
if (!timings[name]) {
|
||||
logger.debug("[functions] no timer initialized for", name);
|
||||
return;
|
||||
}
|
||||
|
||||
// hrtime returns a duration as an array of [seconds, nanos]
|
||||
var duration = process.hrtime(timings[name].t0);
|
||||
track(
|
||||
"Functions Deploy (Duration)",
|
||||
timings[name].type,
|
||||
duration[0] * 1000 + Math.round(duration[1] * 1e-6)
|
||||
);
|
||||
}
|
||||
|
||||
function _fetchTriggerUrls(ops) {
|
||||
if (!_.find(ops, ["trigger.httpsTrigger", {}])) {
|
||||
// No HTTPS functions being deployed
|
||||
return Promise.resolve();
|
||||
}
|
||||
return gcp.cloudfunctions.list(projectId, GCP_REGION).then(function(functions) {
|
||||
var httpFunctions = _.chain(functions)
|
||||
.filter({ sourceUploadUrl: sourceUrl })
|
||||
.filter("httpsTrigger")
|
||||
.value();
|
||||
_.forEach(httpFunctions, function(httpFunc) {
|
||||
_.chain(ops)
|
||||
.find({ func: httpFunc.name })
|
||||
.assign({ triggerUrl: httpFunc.httpsTrigger.url })
|
||||
.value();
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function _functionMatchesGroup(functionName, groupChunks) {
|
||||
return _.isEqual(groupChunks, functionName.split("-").slice(0, groupChunks.length));
|
||||
}
|
||||
|
||||
function _getFilterGroups() {
|
||||
if (!options.only) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var opts;
|
||||
return _.chain(options.only.split(","))
|
||||
.filter(function(filter) {
|
||||
opts = filter.split(":");
|
||||
return opts[0] === "functions" && opts[1];
|
||||
})
|
||||
.map(function(filter) {
|
||||
return filter.split(":")[1].split(".");
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
function _getReleaseNames(uploadNames, existingNames, functionFilterGroups) {
|
||||
if (functionFilterGroups.length === 0) {
|
||||
return uploadNames;
|
||||
}
|
||||
|
||||
var allFunctions = _.union(uploadNames, existingNames);
|
||||
return _.filter(allFunctions, function(functionName) {
|
||||
return _.some(
|
||||
_.map(functionFilterGroups, function(groupChunks) {
|
||||
return _functionMatchesGroup(functionName, groupChunks);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function _logFilters(existingNames, releaseNames, functionFilterGroups) {
|
||||
if (functionFilterGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("> [functions] filtering triggers to: " + JSON.stringify(releaseNames, null, 2));
|
||||
track("Functions Deploy with Filter", "", releaseNames.length);
|
||||
|
||||
if (existingNames.length > 0) {
|
||||
utils.logBullet(
|
||||
chalk.bold.cyan("functions: ") + "current functions in project: " + existingNames.join(", ")
|
||||
);
|
||||
}
|
||||
if (releaseNames.length > 0) {
|
||||
utils.logBullet(
|
||||
chalk.bold.cyan("functions: ") +
|
||||
"uploading functions in project: " +
|
||||
releaseNames.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
var allFunctions = _.union(releaseNames, existingNames);
|
||||
var unmatchedFilters = _.chain(functionFilterGroups)
|
||||
.filter(function(filterGroup) {
|
||||
return !_.some(
|
||||
_.map(allFunctions, function(functionName) {
|
||||
return _functionMatchesGroup(functionName, filterGroup);
|
||||
})
|
||||
);
|
||||
})
|
||||
.map(function(group) {
|
||||
return group.join("-");
|
||||
})
|
||||
.value();
|
||||
if (unmatchedFilters.length > 0) {
|
||||
utils.logWarning(
|
||||
chalk.bold.yellow("functions: ") +
|
||||
"the following filters were specified but do not match any functions in the project: " +
|
||||
unmatchedFilters.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _pollAndManageOperations(operations) {
|
||||
var interval;
|
||||
// Poll less frequently when there are many operations to avoid hitting read quota.
|
||||
// See "Read requests" quota at https://cloud.google.com/console/apis/api/cloudfunctions/quotas
|
||||
if (_.size(operations) > 90) {
|
||||
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 Promise.resolve();
|
||||
} else if (_.size(operations) > 40) {
|
||||
interval = 10 * 1000;
|
||||
} else if (_.size(operations) > 15) {
|
||||
interval = 5 * 1000;
|
||||
} else {
|
||||
interval = 2 * 1000;
|
||||
}
|
||||
var pollFunction = gcp.cloudfunctions.check;
|
||||
var printSuccess = function(op) {
|
||||
_endTimer(op.functionName);
|
||||
utils.logSuccess(
|
||||
chalk.bold.green("functions[" + op.functionName + "]: ") +
|
||||
"Successful " +
|
||||
op.type +
|
||||
" operation. "
|
||||
);
|
||||
if (op.triggerUrl && op.type !== "delete") {
|
||||
logger.info(chalk.bold("Function URL"), "(" + op.functionName + "):", op.triggerUrl);
|
||||
}
|
||||
};
|
||||
var printFail = function(op) {
|
||||
_endTimer(op.functionName);
|
||||
failedDeployments += 1;
|
||||
utils.logWarning(
|
||||
chalk.bold.yellow("functions[" + op.functionName + "]: ") + "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 retryCondition = function(result) {
|
||||
// The error codes from a Google.LongRunning operation follow google.rpc.Code format.
|
||||
|
||||
var retryableCodes = [
|
||||
1, // cancelled by client
|
||||
4, // deadline exceeded
|
||||
10, // aborted (typically due to concurrency issue)
|
||||
14, // unavailable
|
||||
];
|
||||
|
||||
if (_.includes(retryableCodes, result.error.code)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return pollOperation.pollAndRetry(
|
||||
operations,
|
||||
pollFunction,
|
||||
interval,
|
||||
printSuccess,
|
||||
printFail,
|
||||
retryCondition
|
||||
);
|
||||
}
|
||||
|
||||
function _getFunctionTrigger(functionInfo) {
|
||||
if (functionInfo.httpsTrigger) {
|
||||
return _.pick(functionInfo, "httpsTrigger");
|
||||
} else if (functionInfo.eventTrigger) {
|
||||
var trigger = functionInfo.eventTrigger;
|
||||
return { eventTrigger: trigger };
|
||||
}
|
||||
logger.debug("Unknown trigger type found in:", functionInfo);
|
||||
return new FirebaseError("Could not parse function trigger, unknown trigger type.");
|
||||
}
|
||||
|
||||
delete payload.functions;
|
||||
return gcp.cloudfunctions
|
||||
.list(projectId, GCP_REGION)
|
||||
.listAll(projectId)
|
||||
.then(function(existingFunctions) {
|
||||
var pluckName = function(functionObject) {
|
||||
var fullName = _.get(functionObject, "name"); // e.g.'projects/proj1/locations/us-central1/functions/func'
|
||||
return _.last(fullName.split("/"));
|
||||
return _.get(functionObject, "name"); // e.g.'projects/proj1/locations/us-central1/functions/func'
|
||||
};
|
||||
|
||||
var existingNames = _.map(existingFunctions, pluckName);
|
||||
var functionFilterGroups = _getFilterGroups();
|
||||
var releaseNames = _getReleaseNames(uploadedNames, existingNames, functionFilterGroups);
|
||||
var functionFilterGroups = helper.getFilterGroups(options);
|
||||
var releaseNames = helper.getReleaseNames(uploadedNames, existingNames, functionFilterGroups);
|
||||
// If not using function filters, then `deleteReleaseNames` should be equivalent to existingNames so that intersection is a noop
|
||||
var deleteReleaseNames = functionFilterGroups.length > 0 ? releaseNames : existingNames;
|
||||
|
||||
_logFilters(existingNames, releaseNames, functionFilterGroups);
|
||||
helper.logFilters(existingNames, releaseNames, functionFilterGroups);
|
||||
|
||||
// Create functions
|
||||
_.chain(uploadedNames)
|
||||
.difference(existingNames)
|
||||
.intersection(releaseNames)
|
||||
.forEach(function(functionName) {
|
||||
var functionInfo = _.find(functionsInfo, { name: functionName });
|
||||
var functionTrigger = _getFunctionTrigger(functionInfo);
|
||||
.forEach(function(name) {
|
||||
var functionInfo = _.find(functionsInfo, { name: name });
|
||||
var functionTrigger = helper.getFunctionTrigger(functionInfo);
|
||||
var functionName = helper.getFunctionName(name);
|
||||
var region = helper.getRegion(name);
|
||||
utils.logBullet(
|
||||
chalk.bold.cyan("functions: ") + "creating function " + chalk.bold(functionName) + "..."
|
||||
chalk.bold.cyan("functions: ") +
|
||||
"creating function " +
|
||||
chalk.bold(helper.getFunctionLabel(name)) +
|
||||
"..."
|
||||
);
|
||||
logger.debug("Trigger is: ", JSON.stringify(functionTrigger));
|
||||
var eventType = functionTrigger.eventTrigger
|
||||
? functionTrigger.eventTrigger.eventType
|
||||
: "https";
|
||||
_startTimer(functionName, "create");
|
||||
_startTimer(name, "create");
|
||||
|
||||
deployments.push({
|
||||
functionName: functionName,
|
||||
name: name,
|
||||
retryFunction: function() {
|
||||
return gcp.cloudfunctions.create({
|
||||
projectId: projectId,
|
||||
region: GCP_REGION,
|
||||
region: region,
|
||||
eventType: eventType,
|
||||
functionName: functionName,
|
||||
entryPoint: functionInfo.entryPoint,
|
||||
trigger: functionTrigger,
|
||||
labels: CLI_DEPLOYMENT_LABELS,
|
||||
labels: _.assign({}, CLI_DEPLOYMENT_LABELS, functionsInfo.labels),
|
||||
sourceUploadUrl: sourceUrl,
|
||||
availableMemoryMb: functionInfo.availableMemoryMb,
|
||||
timeout: functionInfo.timeout,
|
||||
});
|
||||
},
|
||||
trigger: functionTrigger,
|
||||
@@ -298,11 +220,17 @@ module.exports = function(context, options, payload) {
|
||||
_.chain(uploadedNames)
|
||||
.intersection(existingNames)
|
||||
.intersection(releaseNames)
|
||||
.forEach(function(functionName) {
|
||||
var functionInfo = _.find(functionsInfo, { name: functionName });
|
||||
var functionTrigger = _getFunctionTrigger(functionInfo);
|
||||
.forEach(function(name) {
|
||||
var functionInfo = _.find(functionsInfo, { name: name });
|
||||
var functionTrigger = helper.getFunctionTrigger(functionInfo);
|
||||
var functionName = helper.getFunctionName(name);
|
||||
var region = helper.getRegion(name);
|
||||
|
||||
utils.logBullet(
|
||||
chalk.bold.cyan("functions: ") + "updating function " + chalk.bold(functionName) + "..."
|
||||
chalk.bold.cyan("functions: ") +
|
||||
"updating function " +
|
||||
chalk.bold(helper.getFunctionLabel(name)) +
|
||||
"..."
|
||||
);
|
||||
logger.debug("Trigger is: ", JSON.stringify(functionTrigger));
|
||||
var eventType = functionTrigger.eventTrigger
|
||||
@@ -336,17 +264,19 @@ module.exports = function(context, options, payload) {
|
||||
"deleting this function.\n"
|
||||
);
|
||||
} else {
|
||||
_startTimer(functionName, "update");
|
||||
_startTimer(name, "update");
|
||||
deployments.push({
|
||||
functionName: functionName,
|
||||
name: name,
|
||||
retryFunction: function() {
|
||||
return gcp.cloudfunctions.update({
|
||||
projectId: projectId,
|
||||
region: GCP_REGION,
|
||||
region: region,
|
||||
functionName: functionName,
|
||||
trigger: functionTrigger,
|
||||
sourceUploadUrl: sourceUrl,
|
||||
labels: CLI_DEPLOYMENT_LABELS,
|
||||
labels: _.assign({}, CLI_DEPLOYMENT_LABELS, functionsInfo.labels),
|
||||
availableMemoryMb: functionInfo.availableMemoryMb,
|
||||
timeout: functionInfo.timeout,
|
||||
});
|
||||
},
|
||||
trigger: functionTrigger,
|
||||
@@ -369,17 +299,23 @@ module.exports = function(context, options, payload) {
|
||||
.map(pluckName)
|
||||
.difference(uploadedNames)
|
||||
.intersection(deleteReleaseNames)
|
||||
.map(function(functionName) {
|
||||
.map(function(name) {
|
||||
var functionName = helper.getFunctionName(name);
|
||||
var region = helper.getRegion(name);
|
||||
|
||||
utils.logBullet(
|
||||
chalk.bold.cyan("functions: ") + "deleting function " + chalk.bold(functionName) + "..."
|
||||
chalk.bold.cyan("functions: ") +
|
||||
"deleting function " +
|
||||
chalk.bold(helper.getFunctionLabel(name)) +
|
||||
"..."
|
||||
);
|
||||
_startTimer(functionName, "delete");
|
||||
_startTimer(name, "delete");
|
||||
deployments.push({
|
||||
functionName: functionName,
|
||||
name: name,
|
||||
retryFunction: function() {
|
||||
return gcp.cloudfunctions.delete({
|
||||
projectId: projectId,
|
||||
region: GCP_REGION,
|
||||
region: region,
|
||||
functionName: functionName,
|
||||
});
|
||||
},
|
||||
@@ -406,7 +342,7 @@ module.exports = function(context, options, payload) {
|
||||
.value();
|
||||
failedDeployments += failedCalls.length;
|
||||
|
||||
return _fetchTriggerUrls(successfulCalls)
|
||||
return _fetchTriggerUrls(projectId, successfulCalls, sourceUrl)
|
||||
.then(function() {
|
||||
return _pollAndManageOperations(successfulCalls).catch(function() {
|
||||
utils.logWarning(
|
||||
|
||||
149
lib/functionsDeployHelper.js
Normal file
149
lib/functionsDeployHelper.js
Normal file
@@ -0,0 +1,149 @@
|
||||
"use strict";
|
||||
|
||||
var _ = require("lodash");
|
||||
var chalk = require("chalk");
|
||||
|
||||
var FirebaseError = require("./error");
|
||||
var logger = require("./logger");
|
||||
var track = require("./track");
|
||||
var utils = require("./utils");
|
||||
|
||||
function _functionMatchesGroup(functionName, groupChunks) {
|
||||
return _.isEqual(
|
||||
groupChunks,
|
||||
_.last(functionName.split("/"))
|
||||
.split("-")
|
||||
.slice(0, groupChunks.length)
|
||||
);
|
||||
}
|
||||
|
||||
function getFilterGroups(options) {
|
||||
if (!options.only) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var opts;
|
||||
return _.chain(options.only.split(","))
|
||||
.filter(function(filter) {
|
||||
opts = filter.split(":");
|
||||
return opts[0] === "functions" && opts[1];
|
||||
})
|
||||
.map(function(filter) {
|
||||
return filter.split(":")[1].split(".");
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
function getReleaseNames(uploadNames, existingNames, functionFilterGroups) {
|
||||
if (functionFilterGroups.length === 0) {
|
||||
return uploadNames;
|
||||
}
|
||||
|
||||
var allFunctions = _.union(uploadNames, existingNames);
|
||||
return _.filter(allFunctions, function(functionName) {
|
||||
return _.some(
|
||||
_.map(functionFilterGroups, function(groupChunks) {
|
||||
return _functionMatchesGroup(functionName, groupChunks);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function logFilters(existingNames, releaseNames, functionFilterGroups) {
|
||||
if (functionFilterGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("> [functions] filtering triggers to: " + JSON.stringify(releaseNames, null, 2));
|
||||
track("Functions Deploy with Filter", "", releaseNames.length);
|
||||
|
||||
if (existingNames.length > 0) {
|
||||
var list = _.map(existingNames, function(name) {
|
||||
return getFunctionName(name) + "(" + getRegion(name) + ")";
|
||||
}).join(", ");
|
||||
utils.logBullet(chalk.bold.cyan("functions: ") + "current functions in project: " + list);
|
||||
}
|
||||
if (releaseNames.length > 0) {
|
||||
var list = _.map(releaseNames, function(name) {
|
||||
return getFunctionName(name) + "(" + getRegion(name) + ")";
|
||||
}).join(", ");
|
||||
utils.logBullet(chalk.bold.cyan("functions: ") + "uploading functions in project: " + list);
|
||||
}
|
||||
|
||||
var allFunctions = _.union(releaseNames, existingNames);
|
||||
var unmatchedFilters = _.chain(functionFilterGroups)
|
||||
.filter(function(filterGroup) {
|
||||
return !_.some(
|
||||
_.map(allFunctions, function(functionName) {
|
||||
return _functionMatchesGroup(functionName, filterGroup);
|
||||
})
|
||||
);
|
||||
})
|
||||
.map(function(group) {
|
||||
return group.join("-");
|
||||
})
|
||||
.value();
|
||||
if (unmatchedFilters.length > 0) {
|
||||
utils.logWarning(
|
||||
chalk.bold.yellow("functions: ") +
|
||||
"the following filters were specified but do not match any functions in the project: " +
|
||||
unmatchedFilters.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getFunctionsInfo(parsedTriggers, projectId) {
|
||||
var functionsInfo = [];
|
||||
_.forEach(parsedTriggers, function(trigger) {
|
||||
if (!trigger.regions) {
|
||||
trigger.regions = ["us-central1"];
|
||||
}
|
||||
// SDK exports list of regions for each function to be deployed to, need to add a new entry
|
||||
// to functionsInfo for each region.
|
||||
_.forEach(trigger.regions, function(region) {
|
||||
functionsInfo.push(
|
||||
_.chain(trigger)
|
||||
.omit("regions")
|
||||
.assign({
|
||||
name: ["projects", projectId, "locations", region, "functions", trigger.name].join("/"),
|
||||
})
|
||||
.value()
|
||||
);
|
||||
});
|
||||
});
|
||||
return functionsInfo;
|
||||
}
|
||||
|
||||
function getFunctionTrigger(functionInfo) {
|
||||
if (functionInfo.httpsTrigger) {
|
||||
return _.pick(functionInfo, "httpsTrigger");
|
||||
} else if (functionInfo.eventTrigger) {
|
||||
var trigger = functionInfo.eventTrigger;
|
||||
return { eventTrigger: trigger };
|
||||
}
|
||||
logger.debug("Unknown trigger type found in:", functionInfo);
|
||||
return new FirebaseError("Could not parse function trigger, unknown trigger type.");
|
||||
}
|
||||
|
||||
function getFunctionName(fullName) {
|
||||
return fullName.split("/")[5];
|
||||
}
|
||||
|
||||
function getRegion(fullName) {
|
||||
return fullName.split("/")[3];
|
||||
}
|
||||
|
||||
function getFunctionLabel(fullName) {
|
||||
return getFunctionName(fullName) + "(" + getRegion(fullName) + ")";
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFilterGroups: getFilterGroups,
|
||||
getReleaseNames: getReleaseNames,
|
||||
logFilters: logFilters,
|
||||
getFunctionsInfo: getFunctionsInfo,
|
||||
getFunctionTrigger: getFunctionTrigger,
|
||||
getFunctionName: getFunctionName,
|
||||
getRegion: getRegion,
|
||||
getFunctionLabel: getFunctionLabel,
|
||||
};
|
||||
@@ -58,11 +58,11 @@ function _createFunction(options) {
|
||||
entryPoint: options.entryPoint,
|
||||
labels: options.labels,
|
||||
};
|
||||
if (options.availableMemory) {
|
||||
data.availableMemoryMb = options.availableMemory;
|
||||
if (options.availableMemoryMb) {
|
||||
data.availableMemoryMb = options.availableMemoryMb;
|
||||
}
|
||||
if (options.functionTimeout) {
|
||||
data.timeout = options.functionTimeout;
|
||||
if (options.timeout) {
|
||||
data.timeout = options.timeout;
|
||||
}
|
||||
return api
|
||||
.request("POST", endpoint, {
|
||||
@@ -100,6 +100,14 @@ function _updateFunction(options) {
|
||||
);
|
||||
|
||||
var masks = ["sourceUploadUrl", "name", "labels"];
|
||||
if (options.availableMemoryMb) {
|
||||
data.availableMemoryMb = options.availableMemoryMb;
|
||||
masks.push("availableMemoryMb");
|
||||
}
|
||||
if (options.timeout) {
|
||||
data.timeout = options.timeout;
|
||||
masks.push("timeout");
|
||||
}
|
||||
if (options.trigger.eventTrigger) {
|
||||
masks = _.concat(
|
||||
masks,
|
||||
@@ -183,6 +191,11 @@ function _listFunctions(projectId, region) {
|
||||
);
|
||||
}
|
||||
|
||||
function _listAllFunctions(projectId) {
|
||||
// "-" instead of a region string lists functions in all regions
|
||||
return _listFunctions(projectId, "-");
|
||||
}
|
||||
|
||||
function _checkOperation(operation) {
|
||||
return api
|
||||
.request("GET", "/" + API_VERSION + "/" + operation.name, {
|
||||
@@ -215,5 +228,6 @@ module.exports = {
|
||||
update: _updateFunction,
|
||||
delete: _deleteFunction,
|
||||
list: _listFunctions,
|
||||
listAll: _listAllFunctions,
|
||||
check: _checkOperation,
|
||||
};
|
||||
|
||||
155
test/lib/functionsDeployHelper.spec.js
Normal file
155
test/lib/functionsDeployHelper.spec.js
Normal file
@@ -0,0 +1,155 @@
|
||||
"use strict";
|
||||
|
||||
var chai = require("chai");
|
||||
var expect = chai.expect;
|
||||
|
||||
var helper = require("../../lib/functionsDeployHelper");
|
||||
|
||||
describe("functionsDeployHelper", function() {
|
||||
describe("getFilterGroups", function() {
|
||||
it("should parse multiple filters", function() {
|
||||
var options = {
|
||||
only: "functions:myFunc,functions:myOtherFunc",
|
||||
};
|
||||
expect(helper.getFilterGroups(options)).to.deep.equal([["myFunc"], ["myOtherFunc"]]);
|
||||
});
|
||||
it("should parse nested filters", function() {
|
||||
var options = {
|
||||
only: "functions:groupA.myFunc",
|
||||
};
|
||||
expect(helper.getFilterGroups(options)).to.deep.equal([["groupA", "myFunc"]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReleaseNames", function() {
|
||||
it("should handle function update", function() {
|
||||
var uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc"];
|
||||
var existingNames = ["projects/myProject/locations/us-central1/functions/myFunc"];
|
||||
var filter = [["myFunc"]];
|
||||
|
||||
expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([
|
||||
"projects/myProject/locations/us-central1/functions/myFunc",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle function deletion", function() {
|
||||
var uploadNames = [];
|
||||
var existingNames = ["projects/myProject/locations/us-central1/functions/myFunc"];
|
||||
var filter = [["myFunc"]];
|
||||
|
||||
expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([
|
||||
"projects/myProject/locations/us-central1/functions/myFunc",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle function creation", function() {
|
||||
var uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc"];
|
||||
var existingNames = [];
|
||||
var filter = [["myFunc"]];
|
||||
|
||||
expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([
|
||||
"projects/myProject/locations/us-central1/functions/myFunc",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle existing function not being in filter", function() {
|
||||
var uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc"];
|
||||
var existingNames = ["projects/myProject/locations/us-central1/functions/myFunc2"];
|
||||
var filter = [["myFunc"]];
|
||||
|
||||
expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([
|
||||
"projects/myProject/locations/us-central1/functions/myFunc",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle no functions satisfying filter", function() {
|
||||
var uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc2"];
|
||||
var existingNames = ["projects/myProject/locations/us-central1/functions/myFunc3"];
|
||||
var filter = [["myFunc"]];
|
||||
|
||||
expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("should handle entire function groups", function() {
|
||||
var uploadNames = ["projects/myProject/locations/us-central1/functions/myGroup-func1"];
|
||||
var existingNames = ["projects/myProject/locations/us-central1/functions/myGroup-func2"];
|
||||
var filter = [["myGroup"]];
|
||||
|
||||
expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([
|
||||
"projects/myProject/locations/us-central1/functions/myGroup-func1",
|
||||
"projects/myProject/locations/us-central1/functions/myGroup-func2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle functions within groups", function() {
|
||||
var uploadNames = ["projects/myProject/locations/us-central1/functions/myGroup-func1"];
|
||||
var existingNames = ["projects/myProject/locations/us-central1/functions/myGroup-func2"];
|
||||
var filter = [["myGroup", "func1"]];
|
||||
|
||||
expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([
|
||||
"projects/myProject/locations/us-central1/functions/myGroup-func1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFunctionsInfo", function() {
|
||||
it("should handle default region", function() {
|
||||
var triggers = [
|
||||
{
|
||||
name: "myFunc",
|
||||
},
|
||||
{
|
||||
name: "myOtherFunc",
|
||||
},
|
||||
];
|
||||
|
||||
expect(helper.getFunctionsInfo(triggers, "myProject")).to.deep.equal([
|
||||
{
|
||||
name: "projects/myProject/locations/us-central1/functions/myFunc",
|
||||
},
|
||||
{
|
||||
name: "projects/myProject/locations/us-central1/functions/myOtherFunc",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle customized region", function() {
|
||||
var triggers = [
|
||||
{
|
||||
name: "myFunc",
|
||||
regions: ["us-east1"],
|
||||
},
|
||||
{
|
||||
name: "myOtherFunc",
|
||||
},
|
||||
];
|
||||
|
||||
expect(helper.getFunctionsInfo(triggers, "myProject")).to.deep.equal([
|
||||
{
|
||||
name: "projects/myProject/locations/us-east1/functions/myFunc",
|
||||
},
|
||||
{
|
||||
name: "projects/myProject/locations/us-central1/functions/myOtherFunc",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle multiple customized region for a function", function() {
|
||||
var triggers = [
|
||||
{
|
||||
name: "myFunc",
|
||||
regions: ["us-east1", "eu-west1"],
|
||||
},
|
||||
];
|
||||
|
||||
expect(helper.getFunctionsInfo(triggers, "myProject")).to.deep.equal([
|
||||
{
|
||||
name: "projects/myProject/locations/us-east1/functions/myFunc",
|
||||
},
|
||||
{
|
||||
name: "projects/myProject/locations/eu-west1/functions/myFunc",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user