Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
06b62a1
Adding onPoll option to operation-poller (#3046)
joehan Jan 19, 2021
6258dd5
Typescriptify functionsDeployHelper (#3059)
joehan Jan 20, 2021
ec7d079
Typescriptifying gcp.cloudfunctions (#3060)
joehan Jan 20, 2021
f583ef6
Typescriptifying functionsConfig (#3063)
joehan Jan 21, 2021
08b9d56
Typescriptifying deploymentTool (#3061)
joehan Jan 21, 2021
b4944a4
Refactoring prepare stage of functions deploy (#3067)
joehan Jan 21, 2021
3be0dca
refactoring release step of functions deploy to use typescript
joehan Jan 21, 2021
e0e703e
Adding logic to build regional deployments
joehan Jan 24, 2021
046c7d7
Implementing createDeploymentPlan
joehan Jan 26, 2021
b876523
First round of PR feedback, removing most usages of lodash
joehan Jan 28, 2021
9e0e6e9
moving function prompts into their own file
joehan Jan 28, 2021
2a3b547
seperating out a bunch of code from functionsDeployHelper
joehan Jan 28, 2021
51f2395
Resolves merge conflicts
joehan Jan 28, 2021
30cc0e9
refactoring release step of functions deploy to use typescript (#3071)
joehan Feb 1, 2021
6916000
Implements core logic of running function deploys
joehan Feb 1, 2021
3c8d4a0
Typescriptifying prepareFunctionsUpload (#3064)
joehan Feb 1, 2021
11956fa
Implementing createDeploymentPlan (#3081)
joehan Feb 1, 2021
85d0afe
adding timing and logs for deployments
joehan Feb 2, 2021
00b1989
cleaning up unused code
joehan Feb 2, 2021
397d7c4
Fixing some things that were broken while merging
joehan Feb 3, 2021
21f4906
Fixing up the order of wait and close to ensure that queue promsies a…
joehan Feb 4, 2021
3b3edbd
Format and clean up typos
joehan Feb 4, 2021
e428bcb
refactoring error handling to be cleaner
joehan Feb 5, 2021
4c8e2fb
cleaning up extera newlines
joehan Feb 8, 2021
7f48130
first round of pr fixes
joehan Feb 9, 2021
39a7e86
Readding some changes that I accidenttally wiped out during a merge
joehan Feb 9, 2021
1366955
Switching name to id where appropriate
joehan Feb 9, 2021
7513229
fixing another bug caused by functionName vs Id
joehan Feb 9, 2021
8d3d82d
Merge pull request #3107 from firebase/jh-execute-deployment-plans
joehan Feb 9, 2021
6d2260e
Refactor functions-delete (#3110)
joehan Feb 9, 2021
42e6c15
Cleaning up error reporting
joehan Feb 10, 2021
e4ce126
Merge remote-tracking branch 'public/master' into jh-functions-refactor
joehan Feb 10, 2021
12a48ea
Merge remote-tracking branch 'public/master' into jh-functions-refactor
joehan Feb 11, 2021
7cfe9d9
Implement validation for changing trigger types, and fixes from bug b…
joehan Feb 12, 2021
5eb08bd
Merge branch 'master' into jh-functions-refactor
joehan Feb 12, 2021
5ca6bbf
Merge branch 'master' into jh-functions-refactor
joehan Feb 16, 2021
344b674
fixes package.json
joehan Feb 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Typescriptify functionsDeployHelper (#3059)
* Typescriptifying and adding CloudFunctionTrigger type

* Using funcName instead of func

* formats
  • Loading branch information
joehan authored Jan 20, 2021
commit 6258dd5725f762954a0077f369624e2c2aa99794
6 changes: 1 addition & 5 deletions src/deploy/functions/checkIam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ export async function checkServiceAccountIam(projectId: string): Promise<void> {
* @param options The command-wide options object.
* @param payload The deploy payload.
*/
export async function checkHttpIam(
context: { projectId: string; existingFunctions: { name: string }[] },
options: unknown,
payload: { functions: { triggers: { name: string; httpsTrigger?: {} }[] } }
): Promise<void> {
export async function checkHttpIam(context: any, options: any, payload: any): Promise<void> {
const triggers = payload.functions.triggers;
const functionsInfo = getFunctionsInfo(triggers, context.projectId);
const filterGroups = getFilterGroups(options);
Expand Down
13 changes: 7 additions & 6 deletions src/deploy/functions/release.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,26 +73,27 @@ function _fetchTriggerUrls(projectId, ops, sourceUrl) {
}

var printSuccess = function (op) {
_endTimer(op.func);
_endTimer(op.funcName);
utils.logSuccess(
clc.bold.green("functions[" + helper.getFunctionLabel(op.func) + "]: ") +
clc.bold.green("functions[" + helper.getFunctionLabel(op.funcName) + "]: ") +
"Successful " +
op.type +
" operation. "
);
if (op.triggerUrl && op.type !== "delete") {
logger.info(
clc.bold("Function URL"),
"(" + helper.getFunctionName(op.func) + "):",
"(" + helper.getFunctionName(op.funcName) + "):",
op.triggerUrl
);
}
};
var printFail = function (op) {
_endTimer(op.func);
failedDeployments.push(helper.getFunctionName(op.func));
_endTimer(op.funcName);
failedDeployments.push(helper.getFunctionName(op.funcName));
utils.logWarning(
clc.bold.yellow("functions[" + helper.getFunctionLabel(op.func) + "]: ") + "Deployment error."
clc.bold.yellow("functions[" + helper.getFunctionLabel(op.funcName) + "]: ") +
"Deployment error."
);
if (op.error.code === 8) {
logger.debug(op.error.message);
Expand Down
198 changes: 119 additions & 79 deletions src/functionsDeployHelper.js → src/functionsDeployHelper.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,96 @@
"use strict";

var _ = require("lodash");
var clc = require("cli-color");

var { FirebaseError } = require("./error");
var logger = require("./logger");
var track = require("./track");
var utils = require("./utils");
var cloudfunctions = require("./gcp/cloudfunctions");
var pollOperations = require("./pollOperations");

function functionMatchesGroup(functionName, groupChunks) {
return _.isEqual(
groupChunks,
_.last(functionName.split("/")).split("-").slice(0, groupChunks.length)
);
import * as _ from "lodash";
import * as clc from "cli-color";

import { FirebaseError } from "./error";
import * as logger from "./logger";
import * as track from "./track";
import * as utils from "./utils";
import * as cloudfunctions from "./gcp/cloudfunctions";
import * as pollOperations from "./pollOperations";

// TODO: Get rid of this when switching to use operation-poller.
export interface Operation {
name: string;
type: string;
funcName: string;
eventType: string;
done: boolean;
triggerUrl?: string;
error?: { code: number; message: string };
}

function getFilterGroups(options) {
export interface CloudFunctionTrigger {
name: string;
sourceUploadUrl?: string;
labels: { [key: string]: string };
environmentVariables: { [key: string]: string };
entryPoint: string;
runtime?: string;
vpcConnector?: string;
vpcConnectorEgressSettings?: string;
ingressSettings?: string;
availableMemoryMb?: number;
timeout?: number;
maxInstances?: number;
serviceAccountEmail?: string;
httpsTrigger?: any;
eventTrigger?: any;
failurePolicy?: {};
schedule?: object;
timeZone?: string;
regions?: string[];
}

export function functionMatchesGroup(functionName: string, groupChunks: string[]): boolean {
const last = _.last(functionName.split("/"));
if (!last) {
return false;
}
const functionNameChunks = last.split("-").slice(0, groupChunks.length);
return _.isEqual(groupChunks, functionNameChunks);
}

export function getFilterGroups(options: any): string[][] {
if (!options.only) {
return [];
}

var opts;
let opts;
return _.chain(options.only.split(","))
.filter(function (filter) {
.filter((filter) => {
opts = filter.split(":");
return opts[0] === "functions" && opts[1];
})
.map(function (filter) {
.map((filter) => {
return filter.split(":")[1].split(/[.-]/);
})
.value();
}

function getReleaseNames(uploadNames, existingNames, functionFilterGroups) {
export function getReleaseNames(
uploadNames: string[],
existingNames: string[],
functionFilterGroups: string[][]
): string[] {
if (functionFilterGroups.length === 0) {
return uploadNames;
}

var allFunctions = _.union(uploadNames, existingNames);
return _.filter(allFunctions, function (functionName) {
const allFunctions = _.union(uploadNames, existingNames);
return _.filter(allFunctions, (functionName) => {
return _.some(
_.map(functionFilterGroups, function (groupChunks) {
_.map(functionFilterGroups, (groupChunks) => {
return functionMatchesGroup(functionName, groupChunks);
})
);
});
}

function logFilters(existingNames, releaseNames, functionFilterGroups) {
export function logFilters(
existingNames: string[],
releaseNames: string[],
functionFilterGroups: string[][]
): void {
if (functionFilterGroups.length === 0) {
return;
}
Expand All @@ -59,28 +100,28 @@ function logFilters(existingNames, releaseNames, functionFilterGroups) {

let list;
if (existingNames.length > 0) {
list = _.map(existingNames, function (name) {
list = _.map(existingNames, (name) => {
return getFunctionName(name) + "(" + getRegion(name) + ")";
}).join(", ");
utils.logBullet(clc.bold.cyan("functions: ") + "current functions in project: " + list);
}
if (releaseNames.length > 0) {
list = _.map(releaseNames, function (name) {
list = _.map(releaseNames, (name) => {
return getFunctionName(name) + "(" + getRegion(name) + ")";
}).join(", ");
utils.logBullet(clc.bold.cyan("functions: ") + "uploading functions in project: " + list);
}

var allFunctions = _.union(releaseNames, existingNames);
var unmatchedFilters = _.chain(functionFilterGroups)
.filter(function (filterGroup) {
const allFunctions = _.union(releaseNames, existingNames);
const unmatchedFilters = _.chain(functionFilterGroups)
.filter((filterGroup) => {
return !_.some(
_.map(allFunctions, function (functionName) {
_.map(allFunctions, (functionName) => {
return functionMatchesGroup(functionName, filterGroup);
})
);
})
.map(function (group) {
.map((group) => {
return group.join("-");
})
.value();
Expand All @@ -93,16 +134,16 @@ function logFilters(existingNames, releaseNames, functionFilterGroups) {
}
}

function getFunctionsInfo(parsedTriggers, projectId) {
var functionsInfo = [];
_.forEach(parsedTriggers, function (trigger) {
export function getFunctionsInfo(parsedTriggers: CloudFunctionTrigger[], projectId: string) {
const functionsInfo: CloudFunctionTrigger[] = [];
_.forEach(parsedTriggers, (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) {
var triggerDeepCopy = JSON.parse(JSON.stringify(trigger));
_.forEach(trigger.regions, (region) => {
const triggerDeepCopy = JSON.parse(JSON.stringify(trigger));
if (triggerDeepCopy.regions) {
delete triggerDeepCopy.regions;
}
Expand All @@ -120,11 +161,11 @@ function getFunctionsInfo(parsedTriggers, projectId) {
return functionsInfo;
}

function getFunctionTrigger(functionInfo) {
export function getFunctionTrigger(functionInfo: CloudFunctionTrigger) {
if (functionInfo.httpsTrigger) {
return { httpsTrigger: functionInfo.httpsTrigger };
} else if (functionInfo.eventTrigger) {
var trigger = functionInfo.eventTrigger;
const trigger = functionInfo.eventTrigger;
trigger.failurePolicy = functionInfo.failurePolicy;
return { eventTrigger: trigger };
}
Expand All @@ -133,7 +174,7 @@ function getFunctionTrigger(functionInfo) {
throw new FirebaseError("Could not parse function trigger, unknown trigger type.");
}

function getFunctionName(fullName) {
export function getFunctionName(fullName: string): string {
return fullName.split("/")[5];
}

Expand All @@ -145,8 +186,8 @@ function getFunctionName(fullName) {
** If you change this pattern, Firebase console will stop displaying schedule descriptions
** and schedules created under the old pattern will no longer be cleaned up correctly
*/
function getScheduleName(fullName, appEngineLocation) {
var [projectsPrefix, project, regionsPrefix, region, , functionName] = fullName.split("/");
export function getScheduleName(fullName: string, appEngineLocation: string): string {
const [projectsPrefix, project, regionsPrefix, region, , functionName] = fullName.split("/");
return `${projectsPrefix}/${project}/${regionsPrefix}/${appEngineLocation}/jobs/firebase-schedule-${functionName}-${region}`;
}

Expand All @@ -156,21 +197,27 @@ function getScheduleName(fullName, appEngineLocation) {
** DANGER: We use the pattern defined here to deploy and delete topics
** If you change this pattern, topics created under the old pattern will no longer be cleaned up correctly
*/
function getTopicName(fullName) {
var [projectsPrefix, project, , region, , functionName] = fullName.split("/");
export function getTopicName(fullName: string): string {
const [projectsPrefix, project, , region, , functionName] = fullName.split("/");
return `${projectsPrefix}/${project}/topics/firebase-schedule-${functionName}-${region}`;
}

function getRegion(fullName) {
export function getRegion(fullName: string): string {
return fullName.split("/")[3];
}

function getFunctionLabel(fullName) {
export function getFunctionLabel(fullName: string): string {
return getFunctionName(fullName) + "(" + getRegion(fullName) + ")";
}

function pollDeploys(operations, printSuccess, printFail, printTooManyOps, projectId) {
var interval;
export function pollDeploys(
operations: Operation[],
printSuccess: (op: Operation) => void,
printFail: (op: Operation) => void,
printTooManyOps: (projectId: string) => void,
projectId: string
) {
let 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) {
Expand All @@ -183,47 +230,40 @@ function pollDeploys(operations, printSuccess, printFail, printTooManyOps, proje
} else {
interval = 2 * 1000;
}
var pollFunction = cloudfunctions.check;
const pollFunction = cloudfunctions.check;

var retryCondition = function (result) {
const retryCondition = function (result: Operation) {
// The error codes from a Google.LongRunning operation follow google.rpc.Code format.

var retryableCodes = [
const retryableCodes = [
1, // cancelled by client
4, // deadline exceeded
10, // aborted (typically due to concurrency issue)
14, // unavailable
];

if (_.includes(retryableCodes, result.error.code)) {
if (_.includes(retryableCodes, result.error?.code)) {
return true;
}
return false;
};
return pollOperations
.pollAndRetry(operations, pollFunction, interval, printSuccess, printFail, retryCondition)
.catch(function () {
utils.logWarning(
clc.bold.yellow("functions:") + " failed to get status of all the deployments"
);
logger.info(
"You can check on their status at " + utils.consoleUrl(projectId, "/functions/logs")
);
return Promise.reject(new FirebaseError("Failed to get status of functions deployments."));
});
}

module.exports = {
getFilterGroups: getFilterGroups,
getReleaseNames: getReleaseNames,
logFilters: logFilters,
getFunctionsInfo: getFunctionsInfo,
getFunctionTrigger: getFunctionTrigger,
getFunctionName: getFunctionName,
getRegion: getRegion,
getScheduleName: getScheduleName,
getTopicName: getTopicName,
functionMatchesGroup: functionMatchesGroup,
getFunctionLabel: getFunctionLabel,
pollDeploys: pollDeploys,
};
try {
return pollOperations.pollAndRetry(
operations,
pollFunction,
interval,
printSuccess,
printFail,
retryCondition
);
} catch (err) {
utils.logWarning(
clc.bold.yellow("functions:") + " failed to get status of all the deployments"
);
logger.info(
"You can check on their status at " + utils.consoleUrl(projectId, "/functions/logs")
);
throw new FirebaseError("Failed to get status of functions deployments.");
}
}
Loading