diff --git a/lib/deploy/functions/release.js b/lib/deploy/functions/release.js index c79d89e7..2b9fb52e 100644 --- a/lib/deploy/functions/release.js +++ b/lib/deploy/functions/release.js @@ -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( diff --git a/lib/functionsDeployHelper.js b/lib/functionsDeployHelper.js new file mode 100644 index 00000000..10d84e2f --- /dev/null +++ b/lib/functionsDeployHelper.js @@ -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, +}; diff --git a/lib/gcp/cloudfunctions.js b/lib/gcp/cloudfunctions.js index 82f766c6..4e1a75cf 100644 --- a/lib/gcp/cloudfunctions.js +++ b/lib/gcp/cloudfunctions.js @@ -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, }; diff --git a/test/lib/functionsDeployHelper.spec.js b/test/lib/functionsDeployHelper.spec.js new file mode 100644 index 00000000..742d3e01 --- /dev/null +++ b/test/lib/functionsDeployHelper.spec.js @@ -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", + }, + ]); + }); + }); +});