firebase-tools 9.17.0 → 9.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -7
- package/lib/api.js +1 -0
- package/lib/apiv2.js +5 -3
- package/lib/appdistribution/client.js +84 -72
- package/lib/appdistribution/distribution.js +8 -26
- package/lib/appdistribution/options-parser-util.js +51 -0
- package/lib/command.js +1 -1
- package/lib/commands/appdistribution-distribute.js +74 -91
- package/lib/commands/appdistribution-testers-add.js +18 -0
- package/lib/commands/appdistribution-testers-remove.js +32 -0
- package/lib/commands/crashlytics-symbols-upload.js +146 -0
- package/lib/commands/ext-configure.js +9 -1
- package/lib/commands/ext-dev-extension-delete.js +2 -1
- package/lib/commands/ext-dev-publish.js +11 -4
- package/lib/commands/ext-dev-unpublish.js +12 -4
- package/lib/commands/ext-install.js +115 -48
- package/lib/commands/ext-uninstall.js +6 -0
- package/lib/commands/ext-update.js +61 -18
- package/lib/commands/functions-config-export.js +115 -0
- package/lib/commands/functions-delete.js +45 -25
- package/lib/commands/functions-list.js +54 -0
- package/lib/commands/functions-log.js +5 -22
- package/lib/commands/hosting-channel-deploy.js +6 -4
- package/lib/commands/index.js +12 -0
- package/lib/deploy/functions/backend.js +118 -121
- package/lib/deploy/functions/checkIam.js +8 -8
- package/lib/deploy/functions/containerCleaner.js +5 -1
- package/lib/deploy/functions/deploy.js +11 -15
- package/lib/deploy/functions/functionsDeployHelper.js +3 -68
- package/lib/deploy/functions/prepare.js +67 -33
- package/lib/deploy/functions/pricing.js +17 -17
- package/lib/deploy/functions/prompts.js +24 -41
- package/lib/deploy/functions/release/executor.js +39 -0
- package/lib/deploy/functions/release/fabricator.js +362 -0
- package/lib/deploy/functions/release/index.js +69 -0
- package/lib/deploy/functions/release/planner.js +159 -0
- package/lib/deploy/functions/release/reporter.js +162 -0
- package/lib/deploy/functions/release/sourceTokenScraper.js +28 -0
- package/lib/deploy/functions/release/timer.js +14 -0
- package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +102 -126
- package/lib/deploy/functions/runtimes/node/parseTriggers.js +34 -50
- package/lib/deploy/functions/triggerRegionHelper.js +40 -0
- package/lib/deploy/functions/validate.js +1 -24
- package/lib/downloadUtils.js +37 -0
- package/lib/emulator/auth/apiSpec.js +1788 -403
- package/lib/emulator/auth/handlers.js +6 -5
- package/lib/emulator/auth/operations.js +439 -40
- package/lib/emulator/auth/server.js +32 -11
- package/lib/emulator/auth/state.js +205 -5
- package/lib/emulator/auth/widget_ui.js +2 -2
- package/lib/emulator/download.js +2 -31
- package/lib/emulator/downloadableEmulators.js +7 -7
- package/lib/emulator/emulatorLogger.js +0 -3
- package/lib/emulator/events/types.js +16 -0
- package/lib/emulator/functionsEmulator.js +117 -20
- package/lib/emulator/functionsEmulatorRuntime.js +46 -121
- package/lib/emulator/functionsEmulatorShared.js +51 -7
- package/lib/emulator/functionsEmulatorShell.js +1 -1
- package/lib/emulator/pubsubEmulator.js +61 -40
- package/lib/emulator/storage/cloudFunctions.js +37 -7
- package/lib/extensions/askUserForConsent.js +16 -13
- package/lib/extensions/askUserForParam.js +72 -3
- package/lib/extensions/billingMigrationHelper.js +1 -11
- package/lib/extensions/changelog.js +2 -1
- package/lib/extensions/displayExtensionInfo.js +35 -33
- package/lib/extensions/emulator/optionsHelper.js +3 -3
- package/lib/extensions/emulator/triggerHelper.js +2 -32
- package/lib/extensions/extensionsApi.js +67 -94
- package/lib/extensions/extensionsHelper.js +49 -35
- package/lib/extensions/paramHelper.js +79 -36
- package/lib/extensions/refs.js +59 -0
- package/lib/extensions/secretsUtils.js +58 -0
- package/lib/extensions/updateHelper.js +21 -45
- package/lib/extensions/warnings.js +1 -7
- package/lib/functional.js +64 -0
- package/lib/functions/env.js +26 -13
- package/lib/functions/functionslog.js +40 -0
- package/lib/functions/runtimeConfigExport.js +137 -0
- package/lib/gcp/cloudfunctions.js +46 -38
- package/lib/gcp/cloudfunctionsv2.js +47 -47
- package/lib/gcp/cloudlogging.js +27 -21
- package/lib/gcp/cloudscheduler.js +22 -16
- package/lib/gcp/pubsub.js +1 -9
- package/lib/gcp/secretManager.js +111 -0
- package/lib/gcp/storage.js +16 -0
- package/lib/previews.js +1 -1
- package/lib/requireInteractive.js +12 -0
- package/lib/utils.js +30 -1
- package/package.json +5 -4
- package/lib/deploy/functions/deploymentPlanner.js +0 -113
- package/lib/deploy/functions/deploymentTimer.js +0 -23
- package/lib/deploy/functions/errorHandler.js +0 -75
- package/lib/deploy/functions/release.js +0 -116
- package/lib/deploy/functions/tasks.js +0 -324
- package/lib/functionsDelete.js +0 -60
|
@@ -102,52 +102,52 @@ const MB_TO_GHZ = {
|
|
|
102
102
|
4096: 4.8,
|
|
103
103
|
8192: 4.8,
|
|
104
104
|
};
|
|
105
|
-
function canCalculateMinInstanceCost(
|
|
106
|
-
if (!
|
|
105
|
+
function canCalculateMinInstanceCost(endpoint) {
|
|
106
|
+
if (!endpoint.minInstances) {
|
|
107
107
|
return true;
|
|
108
108
|
}
|
|
109
|
-
if (
|
|
110
|
-
if (!MB_TO_GHZ[
|
|
109
|
+
if (endpoint.platform == "gcfv1") {
|
|
110
|
+
if (!MB_TO_GHZ[endpoint.availableMemoryMb || 256]) {
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
113
|
-
if (!V1_REGION_TO_TIER[
|
|
113
|
+
if (!V1_REGION_TO_TIER[endpoint.region]) {
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
|
-
if (!V2_REGION_TO_TIER[
|
|
118
|
+
if (!V2_REGION_TO_TIER[endpoint.region]) {
|
|
119
119
|
return false;
|
|
120
120
|
}
|
|
121
121
|
return true;
|
|
122
122
|
}
|
|
123
123
|
exports.canCalculateMinInstanceCost = canCalculateMinInstanceCost;
|
|
124
124
|
const SECONDS_PER_MONTH = 30 * 24 * 60 * 60;
|
|
125
|
-
function monthlyMinInstanceCost(
|
|
125
|
+
function monthlyMinInstanceCost(endpoints) {
|
|
126
126
|
const usage = {
|
|
127
127
|
gcfv1: { 1: { ram: 0, cpu: 0 }, 2: { ram: 0, cpu: 0 } },
|
|
128
128
|
gcfv2: { 1: { ram: 0, cpu: 0 }, 2: { ram: 0, cpu: 0 } },
|
|
129
129
|
};
|
|
130
|
-
for (const
|
|
131
|
-
if (!
|
|
130
|
+
for (const endpoint of endpoints) {
|
|
131
|
+
if (!endpoint.minInstances) {
|
|
132
132
|
continue;
|
|
133
133
|
}
|
|
134
|
-
const ramMb =
|
|
134
|
+
const ramMb = endpoint.availableMemoryMb || 256;
|
|
135
135
|
const ramGb = ramMb / 1024;
|
|
136
|
-
if (
|
|
136
|
+
if (endpoint.platform === "gcfv1") {
|
|
137
137
|
const cpu = MB_TO_GHZ[ramMb];
|
|
138
|
-
const tier = V1_REGION_TO_TIER[
|
|
138
|
+
const tier = V1_REGION_TO_TIER[endpoint.region];
|
|
139
139
|
usage["gcfv1"][tier].ram =
|
|
140
|
-
usage["gcfv1"][tier].ram + ramGb * SECONDS_PER_MONTH *
|
|
140
|
+
usage["gcfv1"][tier].ram + ramGb * SECONDS_PER_MONTH * endpoint.minInstances;
|
|
141
141
|
usage["gcfv1"][tier].cpu =
|
|
142
|
-
usage["gcfv1"][tier].cpu +
|
|
142
|
+
usage["gcfv1"][tier].cpu + cpu * SECONDS_PER_MONTH * endpoint.minInstances;
|
|
143
143
|
}
|
|
144
144
|
else {
|
|
145
145
|
const cpu = 1;
|
|
146
|
-
const tier = V2_REGION_TO_TIER[
|
|
146
|
+
const tier = V2_REGION_TO_TIER[endpoint.region];
|
|
147
147
|
usage["gcfv2"][tier].ram =
|
|
148
|
-
usage["gcfv2"][tier].ram + ramGb * SECONDS_PER_MONTH *
|
|
148
|
+
usage["gcfv2"][tier].ram + ramGb * SECONDS_PER_MONTH * endpoint.minInstances;
|
|
149
149
|
usage["gcfv2"][tier].cpu =
|
|
150
|
-
usage["gcfv2"][tier].cpu + cpu * SECONDS_PER_MONTH *
|
|
150
|
+
usage["gcfv2"][tier].cpu + cpu * SECONDS_PER_MONTH * endpoint.minInstances;
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
let v1MemoryBill = usage["gcfv1"][1].ram * exports.V1_RATES.memoryGb[1] + usage["gcfv1"][2].ram * exports.V1_RATES.memoryGb[2];
|
|
@@ -9,42 +9,23 @@ const logger_1 = require("../../logger");
|
|
|
9
9
|
const backend = require("./backend");
|
|
10
10
|
const pricing = require("./pricing");
|
|
11
11
|
const utils = require("../../utils");
|
|
12
|
-
function compareFunctions(left, right) {
|
|
13
|
-
if (left.platform != right.platform) {
|
|
14
|
-
return right.platform < left.platform ? -1 : 1;
|
|
15
|
-
}
|
|
16
|
-
if (left.region < right.region) {
|
|
17
|
-
return -1;
|
|
18
|
-
}
|
|
19
|
-
if (left.region > right.region) {
|
|
20
|
-
return 1;
|
|
21
|
-
}
|
|
22
|
-
if (left.id < right.id) {
|
|
23
|
-
return -1;
|
|
24
|
-
}
|
|
25
|
-
if (left.id > right.id) {
|
|
26
|
-
return 1;
|
|
27
|
-
}
|
|
28
|
-
return 0;
|
|
29
|
-
}
|
|
30
12
|
async function promptForFailurePolicies(options, want, have) {
|
|
31
|
-
const
|
|
32
|
-
return backend.
|
|
13
|
+
const retryEndpoints = backend.allEndpoints(want).filter((e) => {
|
|
14
|
+
return backend.isEventTriggered(e) && e.eventTrigger.retry;
|
|
33
15
|
});
|
|
34
|
-
if (
|
|
16
|
+
if (retryEndpoints.length === 0) {
|
|
35
17
|
return;
|
|
36
18
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return !existingRetryFunctions.some(backend.sameFunctionName(fn));
|
|
19
|
+
const newRetryEndpoints = retryEndpoints.filter((endpoint) => {
|
|
20
|
+
var _a;
|
|
21
|
+
const existing = (_a = have.endpoints[endpoint.region]) === null || _a === void 0 ? void 0 : _a[endpoint.id];
|
|
22
|
+
return !(existing && backend.isEventTriggered(existing) && existing.eventTrigger.retry);
|
|
42
23
|
});
|
|
43
|
-
if (
|
|
24
|
+
if (newRetryEndpoints.length == 0) {
|
|
44
25
|
return;
|
|
45
26
|
}
|
|
46
27
|
const warnMessage = "The following functions will newly be retried in case of failure: " +
|
|
47
|
-
clc.bold(
|
|
28
|
+
clc.bold(newRetryEndpoints.sort(backend.compareFunctions).map(functionsDeployHelper_1.getFunctionLabel).join(", ")) +
|
|
48
29
|
". " +
|
|
49
30
|
"Retried executions are billed as any other execution, and functions are retried repeatedly until they either successfully execute or the maximum retry period has elapsed, which can be up to 7 days. " +
|
|
50
31
|
"For safety, you might want to ensure that your functions are idempotent; see https://firebase.google.com/docs/functions/retries to learn more.";
|
|
@@ -74,7 +55,7 @@ async function promptForFunctionDeletion(functionsToDelete, force, nonInteractiv
|
|
|
74
55
|
return true;
|
|
75
56
|
}
|
|
76
57
|
const deleteList = functionsToDelete
|
|
77
|
-
.sort(compareFunctions)
|
|
58
|
+
.sort(backend.compareFunctions)
|
|
78
59
|
.map((fn) => "\t" + functionsDeployHelper_1.getFunctionLabel(fn))
|
|
79
60
|
.join("\n");
|
|
80
61
|
if (nonInteractive) {
|
|
@@ -108,21 +89,22 @@ async function promptForMinInstances(options, want, have) {
|
|
|
108
89
|
if (options.force) {
|
|
109
90
|
return;
|
|
110
91
|
}
|
|
111
|
-
const increasesCost =
|
|
112
|
-
|
|
92
|
+
const increasesCost = backend.someEndpoint(want, (wantE) => {
|
|
93
|
+
var _a;
|
|
94
|
+
if (!pricing.canCalculateMinInstanceCost(wantE)) {
|
|
113
95
|
return true;
|
|
114
96
|
}
|
|
115
|
-
const wantCost = pricing.monthlyMinInstanceCost([
|
|
116
|
-
const
|
|
97
|
+
const wantCost = pricing.monthlyMinInstanceCost([wantE]);
|
|
98
|
+
const haveE = (_a = have.endpoints[wantE.region]) === null || _a === void 0 ? void 0 : _a[wantE.id];
|
|
117
99
|
let haveCost;
|
|
118
|
-
if (!
|
|
100
|
+
if (!haveE) {
|
|
119
101
|
haveCost = 0;
|
|
120
102
|
}
|
|
121
|
-
else if (!pricing.canCalculateMinInstanceCost(
|
|
103
|
+
else if (!pricing.canCalculateMinInstanceCost(wantE)) {
|
|
122
104
|
return true;
|
|
123
105
|
}
|
|
124
106
|
else {
|
|
125
|
-
haveCost = pricing.monthlyMinInstanceCost([
|
|
107
|
+
haveCost = pricing.monthlyMinInstanceCost([haveE]);
|
|
126
108
|
}
|
|
127
109
|
return wantCost > haveCost;
|
|
128
110
|
});
|
|
@@ -134,9 +116,10 @@ async function promptForMinInstances(options, want, have) {
|
|
|
134
116
|
exit: 1,
|
|
135
117
|
});
|
|
136
118
|
}
|
|
137
|
-
const functionLines =
|
|
119
|
+
const functionLines = backend
|
|
120
|
+
.allEndpoints(want)
|
|
138
121
|
.filter((fn) => fn.minInstances)
|
|
139
|
-
.sort(compareFunctions)
|
|
122
|
+
.sort(backend.compareFunctions)
|
|
140
123
|
.map((fn) => {
|
|
141
124
|
return (`\t${functionsDeployHelper_1.getFunctionLabel(fn)}: ${fn.minInstances} instances, ` +
|
|
142
125
|
backend.memoryOptionDisplayName(fn.availableMemoryMb || 256) +
|
|
@@ -144,17 +127,17 @@ async function promptForMinInstances(options, want, have) {
|
|
|
144
127
|
})
|
|
145
128
|
.join("\n");
|
|
146
129
|
let costLine;
|
|
147
|
-
if (
|
|
130
|
+
if (backend.someEndpoint(want, (fn) => !pricing.canCalculateMinInstanceCost(fn))) {
|
|
148
131
|
costLine =
|
|
149
132
|
"Cannot calculate the minimum monthly bill for this configuration. Consider running " +
|
|
150
133
|
clc.bold("npm install -g firebase-tools");
|
|
151
134
|
}
|
|
152
135
|
else {
|
|
153
|
-
const cost = pricing.monthlyMinInstanceCost(want).toFixed(2);
|
|
136
|
+
const cost = pricing.monthlyMinInstanceCost(backend.allEndpoints(want)).toFixed(2);
|
|
154
137
|
costLine = `With these options, your minimum bill will be $${cost} in a 30-day month`;
|
|
155
138
|
}
|
|
156
139
|
let cudAnnotation = "";
|
|
157
|
-
if (
|
|
140
|
+
if (backend.someEndpoint(want, (fn) => fn.platform == "gcfv2" && !!fn.minInstances)) {
|
|
158
141
|
cudAnnotation =
|
|
159
142
|
"\nThis bill can be lowered with a one year commitment. See https://cloud.google.com/run/cud for more";
|
|
160
143
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InlineExecutor = exports.QueueExecutor = void 0;
|
|
4
|
+
const queue_1 = require("../../../throttler/queue");
|
|
5
|
+
async function handler(op) {
|
|
6
|
+
var _a, _b, _c, _d, _e, _f;
|
|
7
|
+
try {
|
|
8
|
+
op.result = await op.func();
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
const code = err.status ||
|
|
12
|
+
err.code || ((_b = (_a = err.context) === null || _a === void 0 ? void 0 : _a.response) === null || _b === void 0 ? void 0 : _b.statusCode) || ((_c = err.original) === null || _c === void 0 ? void 0 : _c.code) || ((_f = (_e = (_d = err.original) === null || _d === void 0 ? void 0 : _d.context) === null || _e === void 0 ? void 0 : _e.response) === null || _f === void 0 ? void 0 : _f.statusCode);
|
|
13
|
+
if (code === 429 || code === 409) {
|
|
14
|
+
throw err;
|
|
15
|
+
}
|
|
16
|
+
op.error = err;
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
class QueueExecutor {
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.queue = new queue_1.Queue(Object.assign(Object.assign({}, options), { handler }));
|
|
23
|
+
}
|
|
24
|
+
async run(func) {
|
|
25
|
+
const op = { func };
|
|
26
|
+
await this.queue.run(op);
|
|
27
|
+
if (op.error) {
|
|
28
|
+
throw op.error;
|
|
29
|
+
}
|
|
30
|
+
return op.result;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.QueueExecutor = QueueExecutor;
|
|
34
|
+
class InlineExecutor {
|
|
35
|
+
run(func) {
|
|
36
|
+
return func();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.InlineExecutor = InlineExecutor;
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Fabricator = void 0;
|
|
4
|
+
const clc = require("cli-color");
|
|
5
|
+
const error_1 = require("../../../error");
|
|
6
|
+
const sourceTokenScraper_1 = require("./sourceTokenScraper");
|
|
7
|
+
const timer_1 = require("./timer");
|
|
8
|
+
const functional_1 = require("../../../functional");
|
|
9
|
+
const runtimes_1 = require("../runtimes");
|
|
10
|
+
const api_1 = require("../../../api");
|
|
11
|
+
const logger_1 = require("../../../logger");
|
|
12
|
+
const backend = require("../backend");
|
|
13
|
+
const deploymentTool = require("../../../deploymentTool");
|
|
14
|
+
const gcf = require("../../../gcp/cloudfunctions");
|
|
15
|
+
const gcfV2 = require("../../../gcp/cloudfunctionsv2");
|
|
16
|
+
const helper = require("../functionsDeployHelper");
|
|
17
|
+
const poller = require("../../../operation-poller");
|
|
18
|
+
const pubsub = require("../../../gcp/pubsub");
|
|
19
|
+
const reporter = require("./reporter");
|
|
20
|
+
const run = require("../../../gcp/run");
|
|
21
|
+
const scheduler = require("../../../gcp/cloudscheduler");
|
|
22
|
+
const utils = require("../../../utils");
|
|
23
|
+
const gcfV1PollerOptions = {
|
|
24
|
+
apiOrigin: api_1.functionsOrigin,
|
|
25
|
+
apiVersion: gcf.API_VERSION,
|
|
26
|
+
masterTimeout: 25 * 60 * 1000,
|
|
27
|
+
};
|
|
28
|
+
const gcfV2PollerOptions = {
|
|
29
|
+
apiOrigin: api_1.functionsV2Origin,
|
|
30
|
+
apiVersion: gcfV2.API_VERSION,
|
|
31
|
+
masterTimeout: 25 * 60 * 1000,
|
|
32
|
+
};
|
|
33
|
+
const DEFAULT_GCFV2_CONCURRENCY = 80;
|
|
34
|
+
const rethrowAs = (endpoint, op) => (err) => {
|
|
35
|
+
throw new reporter.DeploymentError(endpoint, op, err);
|
|
36
|
+
};
|
|
37
|
+
class Fabricator {
|
|
38
|
+
constructor(args) {
|
|
39
|
+
this.executor = args.executor;
|
|
40
|
+
this.functionExecutor = args.functionExecutor;
|
|
41
|
+
this.sourceUrl = args.sourceUrl;
|
|
42
|
+
this.storage = args.storage;
|
|
43
|
+
this.appEngineLocation = args.appEngineLocation;
|
|
44
|
+
}
|
|
45
|
+
async applyPlan(plan) {
|
|
46
|
+
const timer = new timer_1.Timer();
|
|
47
|
+
const summary = {
|
|
48
|
+
totalTime: 0,
|
|
49
|
+
results: [],
|
|
50
|
+
};
|
|
51
|
+
const deployRegions = Object.values(plan).map(async (changes) => {
|
|
52
|
+
const results = await this.applyRegionalChanges(changes);
|
|
53
|
+
summary.results.push(...results);
|
|
54
|
+
return;
|
|
55
|
+
});
|
|
56
|
+
const promiseResults = await utils.allSettled(deployRegions);
|
|
57
|
+
const errs = promiseResults
|
|
58
|
+
.filter((r) => r.status === "rejected")
|
|
59
|
+
.map((r) => r.reason);
|
|
60
|
+
if (errs.length) {
|
|
61
|
+
logger_1.logger.debug("Fabricator.applyRegionalChanges returned an unhandled exception. This should never happen", JSON.stringify(errs, null, 2));
|
|
62
|
+
}
|
|
63
|
+
summary.totalTime = timer.stop();
|
|
64
|
+
return summary;
|
|
65
|
+
}
|
|
66
|
+
async applyRegionalChanges(changes) {
|
|
67
|
+
const deployResults = [];
|
|
68
|
+
const handle = async (op, endpoint, fn) => {
|
|
69
|
+
const timer = new timer_1.Timer();
|
|
70
|
+
const result = { endpoint };
|
|
71
|
+
try {
|
|
72
|
+
await fn();
|
|
73
|
+
this.logOpSuccess(op, endpoint);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
result.error = err;
|
|
77
|
+
}
|
|
78
|
+
result.durationMs = timer.stop();
|
|
79
|
+
deployResults.push(result);
|
|
80
|
+
};
|
|
81
|
+
const upserts = [];
|
|
82
|
+
const scraper = new sourceTokenScraper_1.SourceTokenScraper();
|
|
83
|
+
for (const endpoint of changes.endpointsToCreate) {
|
|
84
|
+
this.logOpStart("creating", endpoint);
|
|
85
|
+
upserts.push(handle("create", endpoint, () => this.createEndpoint(endpoint, scraper)));
|
|
86
|
+
}
|
|
87
|
+
for (const update of changes.endpointsToUpdate) {
|
|
88
|
+
this.logOpStart("updating", update.endpoint);
|
|
89
|
+
upserts.push(handle("update", update.endpoint, () => this.updateEndpoint(update, scraper)));
|
|
90
|
+
}
|
|
91
|
+
await utils.allSettled(upserts);
|
|
92
|
+
if (deployResults.find((r) => r.error)) {
|
|
93
|
+
for (const endpoint of changes.endpointsToDelete) {
|
|
94
|
+
deployResults.push({
|
|
95
|
+
endpoint,
|
|
96
|
+
durationMs: 0,
|
|
97
|
+
error: new reporter.AbortedDeploymentError(endpoint),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return deployResults;
|
|
101
|
+
}
|
|
102
|
+
const deletes = [];
|
|
103
|
+
for (const endpoint of changes.endpointsToDelete) {
|
|
104
|
+
this.logOpStart("deleting", endpoint);
|
|
105
|
+
deletes.push(handle("delete", endpoint, () => this.deleteEndpoint(endpoint)));
|
|
106
|
+
}
|
|
107
|
+
await utils.allSettled(deletes);
|
|
108
|
+
return deployResults;
|
|
109
|
+
}
|
|
110
|
+
async createEndpoint(endpoint, scraper) {
|
|
111
|
+
endpoint.labels = Object.assign(Object.assign({}, endpoint.labels), deploymentTool.labels());
|
|
112
|
+
if (endpoint.platform === "gcfv1") {
|
|
113
|
+
await this.createV1Function(endpoint, scraper);
|
|
114
|
+
}
|
|
115
|
+
else if (endpoint.platform === "gcfv2") {
|
|
116
|
+
await this.createV2Function(endpoint);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
functional_1.assertExhaustive(endpoint.platform);
|
|
120
|
+
}
|
|
121
|
+
await this.setTrigger(endpoint);
|
|
122
|
+
}
|
|
123
|
+
async updateEndpoint(update, scraper) {
|
|
124
|
+
update.endpoint.labels = Object.assign(Object.assign({}, update.endpoint.labels), deploymentTool.labels());
|
|
125
|
+
if (update.deleteAndRecreate) {
|
|
126
|
+
await this.deleteEndpoint(update.deleteAndRecreate);
|
|
127
|
+
await this.createEndpoint(update.endpoint, scraper);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (update.endpoint.platform === "gcfv1") {
|
|
131
|
+
await this.updateV1Function(update.endpoint, scraper);
|
|
132
|
+
}
|
|
133
|
+
else if (update.endpoint.platform === "gcfv2") {
|
|
134
|
+
await this.updateV2Function(update.endpoint);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
functional_1.assertExhaustive(update.endpoint.platform);
|
|
138
|
+
}
|
|
139
|
+
await this.setTrigger(update.endpoint);
|
|
140
|
+
}
|
|
141
|
+
async deleteEndpoint(endpoint) {
|
|
142
|
+
await this.deleteTrigger(endpoint);
|
|
143
|
+
if (endpoint.platform === "gcfv1") {
|
|
144
|
+
await this.deleteV1Function(endpoint);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
await this.deleteV2Function(endpoint);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async createV1Function(endpoint, scraper) {
|
|
151
|
+
var _a;
|
|
152
|
+
if (!this.sourceUrl) {
|
|
153
|
+
logger_1.logger.debug("Precondition failed. Cannot create a GCF function without sourceUrl");
|
|
154
|
+
throw new Error("Precondition failed");
|
|
155
|
+
}
|
|
156
|
+
const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
|
|
157
|
+
apiFunction.sourceToken = await scraper.tokenPromise();
|
|
158
|
+
const resultFunction = await this.functionExecutor
|
|
159
|
+
.run(async () => {
|
|
160
|
+
const op = await gcf.createFunction(apiFunction);
|
|
161
|
+
return poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `create-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
|
|
162
|
+
})
|
|
163
|
+
.catch(rethrowAs(endpoint, "create"));
|
|
164
|
+
endpoint.uri = (_a = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _a === void 0 ? void 0 : _a.url;
|
|
165
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
166
|
+
const invoker = endpoint.httpsTrigger.invoker || ["public"];
|
|
167
|
+
if (!invoker.includes("private")) {
|
|
168
|
+
await this.executor
|
|
169
|
+
.run(async () => {
|
|
170
|
+
await gcf.setInvokerCreate(endpoint.project, backend.functionName(endpoint), invoker);
|
|
171
|
+
})
|
|
172
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async createV2Function(endpoint) {
|
|
177
|
+
var _a;
|
|
178
|
+
if (!this.storage) {
|
|
179
|
+
logger_1.logger.debug("Precondition failed. Cannot create a GCFv2 function without storage");
|
|
180
|
+
throw new Error("Precondition failed");
|
|
181
|
+
}
|
|
182
|
+
const apiFunction = gcfV2.functionFromEndpoint(endpoint, this.storage[endpoint.region]);
|
|
183
|
+
const topic = (_a = apiFunction.eventTrigger) === null || _a === void 0 ? void 0 : _a.pubsubTopic;
|
|
184
|
+
if (topic) {
|
|
185
|
+
await this.executor
|
|
186
|
+
.run(async () => {
|
|
187
|
+
try {
|
|
188
|
+
await pubsub.createTopic({ name: topic });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err.status === 409) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
throw new error_1.FirebaseError("Unexpected error creating Pub/Sub topic", {
|
|
195
|
+
original: err,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
.catch(rethrowAs(endpoint, "create topic"));
|
|
200
|
+
}
|
|
201
|
+
const resultFunction = (await this.functionExecutor
|
|
202
|
+
.run(async () => {
|
|
203
|
+
const op = await gcfV2.createFunction(apiFunction);
|
|
204
|
+
return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `create-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
|
|
205
|
+
})
|
|
206
|
+
.catch(rethrowAs(endpoint, "create")));
|
|
207
|
+
endpoint.uri = resultFunction.serviceConfig.uri;
|
|
208
|
+
const serviceName = resultFunction.serviceConfig.service;
|
|
209
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
210
|
+
const invoker = endpoint.httpsTrigger.invoker || ["public"];
|
|
211
|
+
if (!invoker.includes("private")) {
|
|
212
|
+
await this.executor
|
|
213
|
+
.run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker))
|
|
214
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
await this.setConcurrency(endpoint, serviceName, endpoint.concurrency || DEFAULT_GCFV2_CONCURRENCY);
|
|
218
|
+
}
|
|
219
|
+
async updateV1Function(endpoint, scraper) {
|
|
220
|
+
var _a;
|
|
221
|
+
if (!this.sourceUrl) {
|
|
222
|
+
logger_1.logger.debug("Precondition failed. Cannot update a GCF function without sourceUrl");
|
|
223
|
+
throw new Error("Precondition failed");
|
|
224
|
+
}
|
|
225
|
+
const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
|
|
226
|
+
apiFunction.sourceToken = await scraper.tokenPromise();
|
|
227
|
+
const resultFunction = await this.functionExecutor
|
|
228
|
+
.run(async () => {
|
|
229
|
+
const op = await gcf.updateFunction(apiFunction);
|
|
230
|
+
return await poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `update-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
|
|
231
|
+
})
|
|
232
|
+
.catch(rethrowAs(endpoint, "update"));
|
|
233
|
+
endpoint.uri = (_a = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _a === void 0 ? void 0 : _a.url;
|
|
234
|
+
if (backend.isHttpsTriggered(endpoint) && endpoint.httpsTrigger.invoker) {
|
|
235
|
+
await this.executor
|
|
236
|
+
.run(async () => {
|
|
237
|
+
await gcf.setInvokerUpdate(endpoint.project, backend.functionName(endpoint), endpoint.httpsTrigger.invoker);
|
|
238
|
+
return;
|
|
239
|
+
})
|
|
240
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async updateV2Function(endpoint) {
|
|
244
|
+
var _a;
|
|
245
|
+
if (!this.storage) {
|
|
246
|
+
logger_1.logger.debug("Precondition failed. Cannot update a GCFv2 function without storage");
|
|
247
|
+
throw new Error("Precondition failed");
|
|
248
|
+
}
|
|
249
|
+
const apiFunction = gcfV2.functionFromEndpoint(endpoint, this.storage[endpoint.region]);
|
|
250
|
+
if ((_a = apiFunction.eventTrigger) === null || _a === void 0 ? void 0 : _a.pubsubTopic) {
|
|
251
|
+
delete apiFunction.eventTrigger.pubsubTopic;
|
|
252
|
+
}
|
|
253
|
+
const resultFunction = (await this.functionExecutor
|
|
254
|
+
.run(async () => {
|
|
255
|
+
const op = await gcfV2.updateFunction(apiFunction);
|
|
256
|
+
return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `update-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
|
|
257
|
+
})
|
|
258
|
+
.catch(rethrowAs(endpoint, "update")));
|
|
259
|
+
endpoint.uri = resultFunction.serviceConfig.uri;
|
|
260
|
+
const serviceName = resultFunction.serviceConfig.service;
|
|
261
|
+
if (backend.isHttpsTriggered(endpoint) && endpoint.httpsTrigger.invoker) {
|
|
262
|
+
await this.executor
|
|
263
|
+
.run(() => run.setInvokerUpdate(endpoint.project, serviceName, endpoint.httpsTrigger.invoker))
|
|
264
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
265
|
+
}
|
|
266
|
+
if (endpoint.concurrency) {
|
|
267
|
+
await this.setConcurrency(endpoint, serviceName, endpoint.concurrency);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async deleteV1Function(endpoint) {
|
|
271
|
+
const fnName = backend.functionName(endpoint);
|
|
272
|
+
await this.functionExecutor
|
|
273
|
+
.run(async () => {
|
|
274
|
+
const op = await gcf.deleteFunction(fnName);
|
|
275
|
+
const pollerOptions = Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `delete-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
|
|
276
|
+
await poller.pollOperation(pollerOptions);
|
|
277
|
+
})
|
|
278
|
+
.catch(rethrowAs(endpoint, "delete"));
|
|
279
|
+
}
|
|
280
|
+
async deleteV2Function(endpoint) {
|
|
281
|
+
const fnName = backend.functionName(endpoint);
|
|
282
|
+
await this.functionExecutor
|
|
283
|
+
.run(async () => {
|
|
284
|
+
const op = await gcfV2.deleteFunction(fnName);
|
|
285
|
+
const pollerOptions = Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `delete-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
|
|
286
|
+
await poller.pollOperation(pollerOptions);
|
|
287
|
+
})
|
|
288
|
+
.catch(rethrowAs(endpoint, "delete"));
|
|
289
|
+
}
|
|
290
|
+
async setConcurrency(endpoint, serviceName, concurrency) {
|
|
291
|
+
await this.functionExecutor
|
|
292
|
+
.run(async () => {
|
|
293
|
+
const service = await run.getService(serviceName);
|
|
294
|
+
if (service.spec.template.spec.containerConcurrency === concurrency) {
|
|
295
|
+
logger_1.logger.debug("Skipping setConcurrency on", serviceName, " because it already matches");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
delete service.status;
|
|
299
|
+
delete service.spec.template.metadata.name;
|
|
300
|
+
service.spec.template.spec.containerConcurrency = concurrency;
|
|
301
|
+
await run.replaceService(serviceName, service);
|
|
302
|
+
})
|
|
303
|
+
.catch(rethrowAs(endpoint, "set concurrency"));
|
|
304
|
+
}
|
|
305
|
+
async setTrigger(endpoint) {
|
|
306
|
+
if (backend.isScheduleTriggered(endpoint)) {
|
|
307
|
+
if (endpoint.platform === "gcfv1") {
|
|
308
|
+
await this.upsertScheduleV1(endpoint);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
else if (endpoint.platform === "gcfv2") {
|
|
312
|
+
await this.upsertScheduleV2(endpoint);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
functional_1.assertExhaustive(endpoint.platform);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async deleteTrigger(endpoint) {
|
|
319
|
+
if (backend.isScheduleTriggered(endpoint)) {
|
|
320
|
+
if (endpoint.platform === "gcfv1") {
|
|
321
|
+
await this.deleteScheduleV1(endpoint);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
else if (endpoint.platform === "gcfv2") {
|
|
325
|
+
await this.deleteScheduleV2(endpoint);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
functional_1.assertExhaustive(endpoint.platform);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async upsertScheduleV1(endpoint) {
|
|
332
|
+
const job = scheduler.jobFromEndpoint(endpoint, this.appEngineLocation);
|
|
333
|
+
await this.executor
|
|
334
|
+
.run(() => scheduler.createOrReplaceJob(job))
|
|
335
|
+
.catch(rethrowAs(endpoint, "upsert schedule"));
|
|
336
|
+
}
|
|
337
|
+
upsertScheduleV2(endpoint) {
|
|
338
|
+
return Promise.reject(new reporter.DeploymentError(endpoint, "upsert schedule", new Error("Not implemented")));
|
|
339
|
+
}
|
|
340
|
+
async deleteScheduleV1(endpoint) {
|
|
341
|
+
const job = scheduler.jobFromEndpoint(endpoint, this.appEngineLocation);
|
|
342
|
+
await this.executor
|
|
343
|
+
.run(() => scheduler.deleteJob(job.name))
|
|
344
|
+
.catch(rethrowAs(endpoint, "delete schedule"));
|
|
345
|
+
await this.executor
|
|
346
|
+
.run(() => pubsub.deleteTopic(job.pubsubTarget.topicName))
|
|
347
|
+
.catch(rethrowAs(endpoint, "delete topic"));
|
|
348
|
+
}
|
|
349
|
+
deleteScheduleV2(endpoint) {
|
|
350
|
+
return Promise.reject(new reporter.DeploymentError(endpoint, "delete schedule", new Error("Not implemented")));
|
|
351
|
+
}
|
|
352
|
+
logOpStart(op, endpoint) {
|
|
353
|
+
const runtime = runtimes_1.getHumanFriendlyRuntimeName(endpoint.runtime);
|
|
354
|
+
const label = helper.getFunctionLabel(endpoint);
|
|
355
|
+
utils.logBullet(`${clc.bold.cyan("functions:")} ${op} ${runtime} function ${clc.bold(label)}...`);
|
|
356
|
+
}
|
|
357
|
+
logOpSuccess(op, endpoint) {
|
|
358
|
+
const label = helper.getFunctionLabel(endpoint);
|
|
359
|
+
utils.logSuccess(`${clc.bold.green(`functions[${label}]`)} Successful ${op} operation.`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
exports.Fabricator = Fabricator;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.printTriggerUrls = exports.release = void 0;
|
|
4
|
+
const clc = require("cli-color");
|
|
5
|
+
const logger_1 = require("../../../logger");
|
|
6
|
+
const functional_1 = require("../../../functional");
|
|
7
|
+
const backend = require("../backend");
|
|
8
|
+
const containerCleaner = require("../containerCleaner");
|
|
9
|
+
const planner = require("./planner");
|
|
10
|
+
const fabricator = require("./fabricator");
|
|
11
|
+
const reporter = require("./reporter");
|
|
12
|
+
const executor = require("./executor");
|
|
13
|
+
const prompts = require("../prompts");
|
|
14
|
+
const functionsConfig_1 = require("../../../functionsConfig");
|
|
15
|
+
const functionsDeployHelper_1 = require("../functionsDeployHelper");
|
|
16
|
+
const error_1 = require("../../../error");
|
|
17
|
+
async function release(context, options, payload) {
|
|
18
|
+
if (!options.config.has("functions")) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const plan = planner.createDeploymentPlan(payload.functions.backend, await backend.existingBackend(context), { filters: context.filters });
|
|
22
|
+
const fnsToDelete = Object.values(plan)
|
|
23
|
+
.map((regionalChanges) => regionalChanges.endpointsToDelete)
|
|
24
|
+
.reduce(functional_1.reduceFlat, []);
|
|
25
|
+
const shouldDelete = await prompts.promptForFunctionDeletion(fnsToDelete, options.force, options.nonInteractive);
|
|
26
|
+
if (!shouldDelete) {
|
|
27
|
+
for (const change of Object.values(plan)) {
|
|
28
|
+
change.endpointsToDelete = [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const functionExecutor = new executor.QueueExecutor({
|
|
32
|
+
retries: 30,
|
|
33
|
+
backoff: 20000,
|
|
34
|
+
concurrency: 40,
|
|
35
|
+
maxBackoff: 40000,
|
|
36
|
+
});
|
|
37
|
+
const fab = new fabricator.Fabricator({
|
|
38
|
+
functionExecutor,
|
|
39
|
+
executor: new executor.QueueExecutor({}),
|
|
40
|
+
sourceUrl: context.uploadUrl,
|
|
41
|
+
storage: context.storage,
|
|
42
|
+
appEngineLocation: functionsConfig_1.getAppEngineLocation(context.firebaseConfig),
|
|
43
|
+
});
|
|
44
|
+
const summary = await fab.applyPlan(plan);
|
|
45
|
+
await reporter.logAndTrackDeployStats(summary);
|
|
46
|
+
reporter.printErrors(summary);
|
|
47
|
+
printTriggerUrls(payload.functions.backend);
|
|
48
|
+
await containerCleaner.cleanupBuildImages(backend.allEndpoints(payload.functions.backend));
|
|
49
|
+
const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
|
|
50
|
+
if (allErrors.length) {
|
|
51
|
+
const opts = allErrors.length == 1 ? { original: allErrors[0] } : { children: allErrors };
|
|
52
|
+
throw new error_1.FirebaseError("There was an error deploying functions", Object.assign(Object.assign({}, opts), { exit: 2 }));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.release = release;
|
|
56
|
+
function printTriggerUrls(results) {
|
|
57
|
+
const httpsFunctions = backend.allEndpoints(results).filter(backend.isHttpsTriggered);
|
|
58
|
+
if (httpsFunctions.length === 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const httpsFunc of httpsFunctions) {
|
|
62
|
+
if (!httpsFunc.uri) {
|
|
63
|
+
logger_1.logger.debug("Missing URI for HTTPS function in printTriggerUrls. This shouldn't happen");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
logger_1.logger.info(clc.bold("Function URL"), `(${functionsDeployHelper_1.getFunctionLabel(httpsFunc)}):`, httpsFunc.uri);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.printTriggerUrls = printTriggerUrls;
|