firebase-tools 9.20.0 → 9.23.1
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 +5 -1
- package/lib/api.js +2 -0
- package/lib/apiv2.js +7 -4
- package/lib/commands/crashlytics-symbols-upload.js +2 -2
- package/lib/commands/deploy.js +9 -1
- package/lib/commands/ext-configure.js +1 -1
- package/lib/commands/ext-dev-deprecate.js +63 -0
- package/lib/commands/ext-dev-undeprecate.js +56 -0
- package/lib/commands/ext-dev-unpublish.js +10 -3
- package/lib/commands/ext-export.js +44 -0
- package/lib/commands/ext-install.js +1 -1
- package/lib/commands/ext-update.js +1 -1
- package/lib/commands/functions-delete.js +55 -42
- package/lib/commands/functions-list.js +11 -11
- package/lib/commands/index.js +6 -5
- package/lib/commands/init.js +3 -0
- package/lib/config.js +3 -2
- package/lib/deploy/extensions/args.js +2 -0
- package/lib/deploy/extensions/deploy.js +49 -0
- package/lib/deploy/extensions/deploymentSummary.js +52 -0
- package/lib/deploy/extensions/errors.js +31 -0
- package/lib/deploy/extensions/index.js +8 -0
- package/lib/deploy/extensions/params.js +39 -0
- package/lib/deploy/extensions/planner.js +94 -0
- package/lib/deploy/extensions/prepare.js +111 -0
- package/lib/deploy/extensions/release.js +43 -0
- package/lib/deploy/extensions/secrets.js +150 -0
- package/lib/deploy/extensions/tasks.js +98 -0
- package/lib/deploy/extensions/validate.js +17 -0
- package/lib/deploy/functions/backend.js +84 -115
- package/lib/deploy/functions/checkIam.js +73 -12
- package/lib/deploy/functions/containerCleaner.js +97 -50
- package/lib/deploy/functions/deploy.js +4 -10
- package/lib/deploy/functions/eventTypes.js +10 -0
- package/lib/deploy/functions/functionsDeployHelper.js +3 -68
- package/lib/deploy/functions/prepare.js +72 -29
- package/lib/deploy/functions/pricing.js +17 -17
- package/lib/deploy/functions/prompts.js +22 -21
- package/lib/deploy/functions/release/executor.js +39 -0
- package/lib/deploy/functions/release/fabricator.js +425 -0
- package/lib/deploy/functions/release/index.js +73 -0
- package/lib/deploy/functions/release/planner.js +162 -0
- package/lib/deploy/functions/release/reporter.js +165 -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 +129 -126
- package/lib/deploy/functions/runtimes/node/parseTriggers.js +32 -54
- package/lib/deploy/functions/services/index.js +38 -0
- package/lib/deploy/functions/services/storage.js +43 -0
- package/lib/deploy/functions/triggerRegionHelper.js +9 -25
- package/lib/deploy/functions/validate.js +1 -24
- package/lib/deploy/index.js +10 -1
- package/lib/emulator/auth/apiSpec.js +37 -6
- package/lib/emulator/auth/operations.js +45 -17
- package/lib/emulator/auth/server.js +16 -2
- package/lib/emulator/auth/state.js +34 -15
- package/lib/emulator/auth/widget_ui.js +14 -0
- package/lib/emulator/downloadableEmulators.js +7 -7
- package/lib/emulator/functionsEmulator.js +18 -4
- package/lib/emulator/storage/cloudFunctions.js +37 -7
- package/lib/ensureApiEnabled.js +10 -12
- package/lib/extensions/askUserForParam.js +14 -6
- package/lib/extensions/checkProjectBilling.js +7 -7
- package/lib/extensions/export.js +107 -0
- package/lib/extensions/extensionsApi.js +103 -21
- package/lib/extensions/extensionsHelper.js +5 -2
- package/lib/extensions/listExtensions.js +16 -11
- package/lib/extensions/paramHelper.js +6 -4
- package/lib/extensions/provisioningHelper.js +16 -3
- package/lib/extensions/refs.js +9 -1
- package/lib/extensions/secretsUtils.js +10 -9
- package/lib/extensions/updateHelper.js +12 -2
- package/lib/extensions/versionHelper.js +14 -0
- package/lib/extensions/warnings.js +33 -1
- package/lib/gcp/artifactregistry.js +16 -0
- package/lib/gcp/cloudfunctions.js +25 -72
- package/lib/gcp/cloudfunctionsv2.js +46 -98
- package/lib/gcp/cloudscheduler.js +22 -16
- package/lib/gcp/cloudtasks.js +143 -0
- package/lib/gcp/docker.js +36 -2
- package/lib/gcp/location.js +44 -0
- package/lib/gcp/proto.js +2 -2
- package/lib/gcp/pubsub.js +1 -9
- package/lib/gcp/secretManager.js +27 -6
- package/lib/gcp/storage.js +48 -32
- package/lib/init/features/project.js +2 -1
- package/lib/previews.js +1 -1
- package/lib/projectUtils.js +10 -1
- package/lib/utils.js +30 -1
- package/package.json +5 -4
- package/schema/firebase-config.json +9 -0
- 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/functions/listFunctions.js +0 -10
- package/lib/functionsDelete.js +0 -60
|
@@ -0,0 +1,425 @@
|
|
|
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 cloudtasks = require("../../../gcp/cloudtasks");
|
|
14
|
+
const deploymentTool = require("../../../deploymentTool");
|
|
15
|
+
const gcf = require("../../../gcp/cloudfunctions");
|
|
16
|
+
const gcfV2 = require("../../../gcp/cloudfunctionsv2");
|
|
17
|
+
const helper = require("../functionsDeployHelper");
|
|
18
|
+
const poller = require("../../../operation-poller");
|
|
19
|
+
const pubsub = require("../../../gcp/pubsub");
|
|
20
|
+
const reporter = require("./reporter");
|
|
21
|
+
const run = require("../../../gcp/run");
|
|
22
|
+
const scheduler = require("../../../gcp/cloudscheduler");
|
|
23
|
+
const utils = require("../../../utils");
|
|
24
|
+
const gcfV1PollerOptions = {
|
|
25
|
+
apiOrigin: api_1.functionsOrigin,
|
|
26
|
+
apiVersion: gcf.API_VERSION,
|
|
27
|
+
masterTimeout: 25 * 60 * 1000,
|
|
28
|
+
maxBackoff: 10000,
|
|
29
|
+
};
|
|
30
|
+
const gcfV2PollerOptions = {
|
|
31
|
+
apiOrigin: api_1.functionsV2Origin,
|
|
32
|
+
apiVersion: gcfV2.API_VERSION,
|
|
33
|
+
masterTimeout: 25 * 60 * 1000,
|
|
34
|
+
maxBackoff: 10000,
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_GCFV2_CONCURRENCY = 80;
|
|
37
|
+
const rethrowAs = (endpoint, op) => (err) => {
|
|
38
|
+
throw new reporter.DeploymentError(endpoint, op, err);
|
|
39
|
+
};
|
|
40
|
+
class Fabricator {
|
|
41
|
+
constructor(args) {
|
|
42
|
+
this.executor = args.executor;
|
|
43
|
+
this.functionExecutor = args.functionExecutor;
|
|
44
|
+
this.sourceUrl = args.sourceUrl;
|
|
45
|
+
this.storage = args.storage;
|
|
46
|
+
this.appEngineLocation = args.appEngineLocation;
|
|
47
|
+
}
|
|
48
|
+
async applyPlan(plan) {
|
|
49
|
+
const timer = new timer_1.Timer();
|
|
50
|
+
const summary = {
|
|
51
|
+
totalTime: 0,
|
|
52
|
+
results: [],
|
|
53
|
+
};
|
|
54
|
+
const deployRegions = Object.values(plan).map(async (changes) => {
|
|
55
|
+
const results = await this.applyRegionalChanges(changes);
|
|
56
|
+
summary.results.push(...results);
|
|
57
|
+
return;
|
|
58
|
+
});
|
|
59
|
+
const promiseResults = await utils.allSettled(deployRegions);
|
|
60
|
+
const errs = promiseResults
|
|
61
|
+
.filter((r) => r.status === "rejected")
|
|
62
|
+
.map((r) => r.reason);
|
|
63
|
+
if (errs.length) {
|
|
64
|
+
logger_1.logger.debug("Fabricator.applyRegionalChanges returned an unhandled exception. This should never happen", JSON.stringify(errs, null, 2));
|
|
65
|
+
}
|
|
66
|
+
summary.totalTime = timer.stop();
|
|
67
|
+
return summary;
|
|
68
|
+
}
|
|
69
|
+
async applyRegionalChanges(changes) {
|
|
70
|
+
const deployResults = [];
|
|
71
|
+
const handle = async (op, endpoint, fn) => {
|
|
72
|
+
const timer = new timer_1.Timer();
|
|
73
|
+
const result = { endpoint };
|
|
74
|
+
try {
|
|
75
|
+
await fn();
|
|
76
|
+
this.logOpSuccess(op, endpoint);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
result.error = err;
|
|
80
|
+
}
|
|
81
|
+
result.durationMs = timer.stop();
|
|
82
|
+
deployResults.push(result);
|
|
83
|
+
};
|
|
84
|
+
const upserts = [];
|
|
85
|
+
const scraper = new sourceTokenScraper_1.SourceTokenScraper();
|
|
86
|
+
for (const endpoint of changes.endpointsToCreate) {
|
|
87
|
+
this.logOpStart("creating", endpoint);
|
|
88
|
+
upserts.push(handle("create", endpoint, () => this.createEndpoint(endpoint, scraper)));
|
|
89
|
+
}
|
|
90
|
+
for (const update of changes.endpointsToUpdate) {
|
|
91
|
+
this.logOpStart("updating", update.endpoint);
|
|
92
|
+
upserts.push(handle("update", update.endpoint, () => this.updateEndpoint(update, scraper)));
|
|
93
|
+
}
|
|
94
|
+
await utils.allSettled(upserts);
|
|
95
|
+
if (deployResults.find((r) => r.error)) {
|
|
96
|
+
for (const endpoint of changes.endpointsToDelete) {
|
|
97
|
+
deployResults.push({
|
|
98
|
+
endpoint,
|
|
99
|
+
durationMs: 0,
|
|
100
|
+
error: new reporter.AbortedDeploymentError(endpoint),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return deployResults;
|
|
104
|
+
}
|
|
105
|
+
const deletes = [];
|
|
106
|
+
for (const endpoint of changes.endpointsToDelete) {
|
|
107
|
+
this.logOpStart("deleting", endpoint);
|
|
108
|
+
deletes.push(handle("delete", endpoint, () => this.deleteEndpoint(endpoint)));
|
|
109
|
+
}
|
|
110
|
+
await utils.allSettled(deletes);
|
|
111
|
+
return deployResults;
|
|
112
|
+
}
|
|
113
|
+
async createEndpoint(endpoint, scraper) {
|
|
114
|
+
endpoint.labels = Object.assign(Object.assign({}, endpoint.labels), deploymentTool.labels());
|
|
115
|
+
if (endpoint.platform === "gcfv1") {
|
|
116
|
+
await this.createV1Function(endpoint, scraper);
|
|
117
|
+
}
|
|
118
|
+
else if (endpoint.platform === "gcfv2") {
|
|
119
|
+
await this.createV2Function(endpoint);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
functional_1.assertExhaustive(endpoint.platform);
|
|
123
|
+
}
|
|
124
|
+
await this.setTrigger(endpoint);
|
|
125
|
+
}
|
|
126
|
+
async updateEndpoint(update, scraper) {
|
|
127
|
+
update.endpoint.labels = Object.assign(Object.assign({}, update.endpoint.labels), deploymentTool.labels());
|
|
128
|
+
if (update.deleteAndRecreate) {
|
|
129
|
+
await this.deleteEndpoint(update.deleteAndRecreate);
|
|
130
|
+
await this.createEndpoint(update.endpoint, scraper);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (update.endpoint.platform === "gcfv1") {
|
|
134
|
+
await this.updateV1Function(update.endpoint, scraper);
|
|
135
|
+
}
|
|
136
|
+
else if (update.endpoint.platform === "gcfv2") {
|
|
137
|
+
await this.updateV2Function(update.endpoint);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
functional_1.assertExhaustive(update.endpoint.platform);
|
|
141
|
+
}
|
|
142
|
+
await this.setTrigger(update.endpoint);
|
|
143
|
+
}
|
|
144
|
+
async deleteEndpoint(endpoint) {
|
|
145
|
+
await this.deleteTrigger(endpoint);
|
|
146
|
+
if (endpoint.platform === "gcfv1") {
|
|
147
|
+
await this.deleteV1Function(endpoint);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
await this.deleteV2Function(endpoint);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async createV1Function(endpoint, scraper) {
|
|
154
|
+
var _a;
|
|
155
|
+
if (!this.sourceUrl) {
|
|
156
|
+
logger_1.logger.debug("Precondition failed. Cannot create a GCF function without sourceUrl");
|
|
157
|
+
throw new Error("Precondition failed");
|
|
158
|
+
}
|
|
159
|
+
const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
|
|
160
|
+
if (apiFunction.httpsTrigger) {
|
|
161
|
+
apiFunction.httpsTrigger.securityLevel = "SECURE_ALWAYS";
|
|
162
|
+
}
|
|
163
|
+
apiFunction.sourceToken = await scraper.tokenPromise();
|
|
164
|
+
const resultFunction = await this.functionExecutor
|
|
165
|
+
.run(async () => {
|
|
166
|
+
const op = await gcf.createFunction(apiFunction);
|
|
167
|
+
return poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `create-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
|
|
168
|
+
})
|
|
169
|
+
.catch(rethrowAs(endpoint, "create"));
|
|
170
|
+
endpoint.uri = (_a = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _a === void 0 ? void 0 : _a.url;
|
|
171
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
172
|
+
const invoker = endpoint.httpsTrigger.invoker || ["public"];
|
|
173
|
+
if (!invoker.includes("private")) {
|
|
174
|
+
await this.executor
|
|
175
|
+
.run(async () => {
|
|
176
|
+
await gcf.setInvokerCreate(endpoint.project, backend.functionName(endpoint), invoker);
|
|
177
|
+
})
|
|
178
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (backend.isTaskQueueTriggered(endpoint)) {
|
|
182
|
+
const invoker = endpoint.taskQueueTrigger.invoker;
|
|
183
|
+
if (invoker && !invoker.includes("private")) {
|
|
184
|
+
await this.executor
|
|
185
|
+
.run(async () => {
|
|
186
|
+
await gcf.setInvokerCreate(endpoint.project, backend.functionName(endpoint), invoker);
|
|
187
|
+
})
|
|
188
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async createV2Function(endpoint) {
|
|
193
|
+
var _a;
|
|
194
|
+
if (!this.storage) {
|
|
195
|
+
logger_1.logger.debug("Precondition failed. Cannot create a GCFv2 function without storage");
|
|
196
|
+
throw new Error("Precondition failed");
|
|
197
|
+
}
|
|
198
|
+
const apiFunction = gcfV2.functionFromEndpoint(endpoint, this.storage[endpoint.region]);
|
|
199
|
+
const topic = (_a = apiFunction.eventTrigger) === null || _a === void 0 ? void 0 : _a.pubsubTopic;
|
|
200
|
+
if (topic) {
|
|
201
|
+
await this.executor
|
|
202
|
+
.run(async () => {
|
|
203
|
+
try {
|
|
204
|
+
await pubsub.createTopic({ name: topic });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (err.status === 409) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
throw new error_1.FirebaseError("Unexpected error creating Pub/Sub topic", {
|
|
211
|
+
original: err,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
.catch(rethrowAs(endpoint, "create topic"));
|
|
216
|
+
}
|
|
217
|
+
const resultFunction = (await this.functionExecutor
|
|
218
|
+
.run(async () => {
|
|
219
|
+
const op = await gcfV2.createFunction(apiFunction);
|
|
220
|
+
return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `create-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
|
|
221
|
+
})
|
|
222
|
+
.catch(rethrowAs(endpoint, "create")));
|
|
223
|
+
endpoint.uri = resultFunction.serviceConfig.uri;
|
|
224
|
+
const serviceName = resultFunction.serviceConfig.service;
|
|
225
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
226
|
+
const invoker = endpoint.httpsTrigger.invoker || ["public"];
|
|
227
|
+
if (!invoker.includes("private")) {
|
|
228
|
+
await this.executor
|
|
229
|
+
.run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker))
|
|
230
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (backend.isTaskQueueTriggered(endpoint)) {
|
|
234
|
+
const invoker = endpoint.taskQueueTrigger.invoker;
|
|
235
|
+
if (invoker && !invoker.includes("private")) {
|
|
236
|
+
await this.executor
|
|
237
|
+
.run(async () => {
|
|
238
|
+
await gcf.setInvokerCreate(endpoint.project, backend.functionName(endpoint), invoker);
|
|
239
|
+
})
|
|
240
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await this.setConcurrency(endpoint, serviceName, endpoint.concurrency || DEFAULT_GCFV2_CONCURRENCY);
|
|
244
|
+
}
|
|
245
|
+
async updateV1Function(endpoint, scraper) {
|
|
246
|
+
var _a;
|
|
247
|
+
if (!this.sourceUrl) {
|
|
248
|
+
logger_1.logger.debug("Precondition failed. Cannot update a GCF function without sourceUrl");
|
|
249
|
+
throw new Error("Precondition failed");
|
|
250
|
+
}
|
|
251
|
+
const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
|
|
252
|
+
apiFunction.sourceToken = await scraper.tokenPromise();
|
|
253
|
+
const resultFunction = await this.functionExecutor
|
|
254
|
+
.run(async () => {
|
|
255
|
+
const op = await gcf.updateFunction(apiFunction);
|
|
256
|
+
return await poller.pollOperation(Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `update-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, onPoll: scraper.poller }));
|
|
257
|
+
})
|
|
258
|
+
.catch(rethrowAs(endpoint, "update"));
|
|
259
|
+
endpoint.uri = (_a = resultFunction === null || resultFunction === void 0 ? void 0 : resultFunction.httpsTrigger) === null || _a === void 0 ? void 0 : _a.url;
|
|
260
|
+
let invoker;
|
|
261
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
262
|
+
invoker = endpoint.httpsTrigger.invoker;
|
|
263
|
+
}
|
|
264
|
+
else if (backend.isTaskQueueTriggered(endpoint)) {
|
|
265
|
+
invoker = endpoint.taskQueueTrigger.invoker;
|
|
266
|
+
}
|
|
267
|
+
if (invoker) {
|
|
268
|
+
await this.executor
|
|
269
|
+
.run(() => gcf.setInvokerUpdate(endpoint.project, backend.functionName(endpoint), invoker))
|
|
270
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async updateV2Function(endpoint) {
|
|
274
|
+
var _a;
|
|
275
|
+
if (!this.storage) {
|
|
276
|
+
logger_1.logger.debug("Precondition failed. Cannot update a GCFv2 function without storage");
|
|
277
|
+
throw new Error("Precondition failed");
|
|
278
|
+
}
|
|
279
|
+
const apiFunction = gcfV2.functionFromEndpoint(endpoint, this.storage[endpoint.region]);
|
|
280
|
+
if ((_a = apiFunction.eventTrigger) === null || _a === void 0 ? void 0 : _a.pubsubTopic) {
|
|
281
|
+
delete apiFunction.eventTrigger.pubsubTopic;
|
|
282
|
+
}
|
|
283
|
+
const resultFunction = await this.functionExecutor
|
|
284
|
+
.run(async () => {
|
|
285
|
+
const op = await gcfV2.updateFunction(apiFunction);
|
|
286
|
+
return await poller.pollOperation(Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `update-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name }));
|
|
287
|
+
})
|
|
288
|
+
.catch(rethrowAs(endpoint, "update"));
|
|
289
|
+
endpoint.uri = resultFunction.serviceConfig.uri;
|
|
290
|
+
const serviceName = resultFunction.serviceConfig.service;
|
|
291
|
+
let invoker;
|
|
292
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
293
|
+
invoker = endpoint.httpsTrigger.invoker;
|
|
294
|
+
}
|
|
295
|
+
else if (backend.isTaskQueueTriggered(endpoint)) {
|
|
296
|
+
invoker = endpoint.taskQueueTrigger.invoker;
|
|
297
|
+
}
|
|
298
|
+
if (invoker) {
|
|
299
|
+
await this.executor
|
|
300
|
+
.run(() => run.setInvokerUpdate(endpoint.project, serviceName, invoker))
|
|
301
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
302
|
+
}
|
|
303
|
+
if (endpoint.concurrency) {
|
|
304
|
+
await this.setConcurrency(endpoint, serviceName, endpoint.concurrency);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async deleteV1Function(endpoint) {
|
|
308
|
+
const fnName = backend.functionName(endpoint);
|
|
309
|
+
await this.functionExecutor
|
|
310
|
+
.run(async () => {
|
|
311
|
+
const op = await gcf.deleteFunction(fnName);
|
|
312
|
+
const pollerOptions = Object.assign(Object.assign({}, gcfV1PollerOptions), { pollerName: `delete-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
|
|
313
|
+
await poller.pollOperation(pollerOptions);
|
|
314
|
+
})
|
|
315
|
+
.catch(rethrowAs(endpoint, "delete"));
|
|
316
|
+
}
|
|
317
|
+
async deleteV2Function(endpoint) {
|
|
318
|
+
const fnName = backend.functionName(endpoint);
|
|
319
|
+
await this.functionExecutor
|
|
320
|
+
.run(async () => {
|
|
321
|
+
const op = await gcfV2.deleteFunction(fnName);
|
|
322
|
+
const pollerOptions = Object.assign(Object.assign({}, gcfV2PollerOptions), { pollerName: `delete-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name });
|
|
323
|
+
await poller.pollOperation(pollerOptions);
|
|
324
|
+
})
|
|
325
|
+
.catch(rethrowAs(endpoint, "delete"));
|
|
326
|
+
}
|
|
327
|
+
async setConcurrency(endpoint, serviceName, concurrency) {
|
|
328
|
+
await this.functionExecutor
|
|
329
|
+
.run(async () => {
|
|
330
|
+
const service = await run.getService(serviceName);
|
|
331
|
+
if (service.spec.template.spec.containerConcurrency === concurrency) {
|
|
332
|
+
logger_1.logger.debug("Skipping setConcurrency on", serviceName, " because it already matches");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
delete service.status;
|
|
336
|
+
delete service.spec.template.metadata.name;
|
|
337
|
+
service.spec.template.spec.containerConcurrency = concurrency;
|
|
338
|
+
await run.replaceService(serviceName, service);
|
|
339
|
+
})
|
|
340
|
+
.catch(rethrowAs(endpoint, "set concurrency"));
|
|
341
|
+
}
|
|
342
|
+
async setTrigger(endpoint) {
|
|
343
|
+
if (backend.isScheduleTriggered(endpoint)) {
|
|
344
|
+
if (endpoint.platform === "gcfv1") {
|
|
345
|
+
await this.upsertScheduleV1(endpoint);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
else if (endpoint.platform === "gcfv2") {
|
|
349
|
+
await this.upsertScheduleV2(endpoint);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
functional_1.assertExhaustive(endpoint.platform);
|
|
353
|
+
}
|
|
354
|
+
else if (backend.isTaskQueueTriggered(endpoint)) {
|
|
355
|
+
await this.upsertTaskQueue(endpoint);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async deleteTrigger(endpoint) {
|
|
359
|
+
if (backend.isScheduleTriggered(endpoint)) {
|
|
360
|
+
if (endpoint.platform === "gcfv1") {
|
|
361
|
+
await this.deleteScheduleV1(endpoint);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
else if (endpoint.platform === "gcfv2") {
|
|
365
|
+
await this.deleteScheduleV2(endpoint);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
functional_1.assertExhaustive(endpoint.platform);
|
|
369
|
+
}
|
|
370
|
+
else if (backend.isTaskQueueTriggered(endpoint)) {
|
|
371
|
+
await this.disableTaskQueue(endpoint);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async upsertScheduleV1(endpoint) {
|
|
375
|
+
const job = scheduler.jobFromEndpoint(endpoint, this.appEngineLocation);
|
|
376
|
+
await this.executor
|
|
377
|
+
.run(() => scheduler.createOrReplaceJob(job))
|
|
378
|
+
.catch(rethrowAs(endpoint, "upsert schedule"));
|
|
379
|
+
}
|
|
380
|
+
upsertScheduleV2(endpoint) {
|
|
381
|
+
return Promise.reject(new reporter.DeploymentError(endpoint, "upsert schedule", new Error("Not implemented")));
|
|
382
|
+
}
|
|
383
|
+
async upsertTaskQueue(endpoint) {
|
|
384
|
+
const queue = cloudtasks.queueFromEndpoint(endpoint);
|
|
385
|
+
await this.executor
|
|
386
|
+
.run(() => cloudtasks.upsertQueue(queue))
|
|
387
|
+
.catch(rethrowAs(endpoint, "upsert task queue"));
|
|
388
|
+
if (endpoint.taskQueueTrigger.invoker) {
|
|
389
|
+
await this.executor
|
|
390
|
+
.run(() => cloudtasks.setEnqueuer(queue.name, endpoint.taskQueueTrigger.invoker))
|
|
391
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async deleteScheduleV1(endpoint) {
|
|
395
|
+
const job = scheduler.jobFromEndpoint(endpoint, this.appEngineLocation);
|
|
396
|
+
await this.executor
|
|
397
|
+
.run(() => scheduler.deleteJob(job.name))
|
|
398
|
+
.catch(rethrowAs(endpoint, "delete schedule"));
|
|
399
|
+
await this.executor
|
|
400
|
+
.run(() => pubsub.deleteTopic(job.pubsubTarget.topicName))
|
|
401
|
+
.catch(rethrowAs(endpoint, "delete topic"));
|
|
402
|
+
}
|
|
403
|
+
deleteScheduleV2(endpoint) {
|
|
404
|
+
return Promise.reject(new reporter.DeploymentError(endpoint, "delete schedule", new Error("Not implemented")));
|
|
405
|
+
}
|
|
406
|
+
async disableTaskQueue(endpoint) {
|
|
407
|
+
const update = {
|
|
408
|
+
name: cloudtasks.queueNameForEndpoint(endpoint),
|
|
409
|
+
state: "DISABLED",
|
|
410
|
+
};
|
|
411
|
+
await this.executor
|
|
412
|
+
.run(() => cloudtasks.updateQueue(update))
|
|
413
|
+
.catch(rethrowAs(endpoint, "disable task queue"));
|
|
414
|
+
}
|
|
415
|
+
logOpStart(op, endpoint) {
|
|
416
|
+
const runtime = runtimes_1.getHumanFriendlyRuntimeName(endpoint.runtime);
|
|
417
|
+
const label = helper.getFunctionLabel(endpoint);
|
|
418
|
+
utils.logBullet(`${clc.bold.cyan("functions:")} ${op} ${runtime} function ${clc.bold(label)}...`);
|
|
419
|
+
}
|
|
420
|
+
logOpSuccess(op, endpoint) {
|
|
421
|
+
const label = helper.getFunctionLabel(endpoint);
|
|
422
|
+
utils.logSuccess(`${clc.bold.green(`functions[${label}]`)} Successful ${op} operation.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
exports.Fabricator = Fabricator;
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
const haveEndpoints = backend.allEndpoints(payload.functions.backend);
|
|
49
|
+
const deletedEndpoints = Object.values(plan)
|
|
50
|
+
.map((r) => r.endpointsToDelete)
|
|
51
|
+
.reduce(functional_1.reduceFlat, []);
|
|
52
|
+
await containerCleaner.cleanupBuildImages(haveEndpoints, deletedEndpoints);
|
|
53
|
+
const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
|
|
54
|
+
if (allErrors.length) {
|
|
55
|
+
const opts = allErrors.length == 1 ? { original: allErrors[0] } : { children: allErrors };
|
|
56
|
+
throw new error_1.FirebaseError("There was an error deploying functions", Object.assign(Object.assign({}, opts), { exit: 2 }));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.release = release;
|
|
60
|
+
function printTriggerUrls(results) {
|
|
61
|
+
const httpsFunctions = backend.allEndpoints(results).filter(backend.isHttpsTriggered);
|
|
62
|
+
if (httpsFunctions.length === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const httpsFunc of httpsFunctions) {
|
|
66
|
+
if (!httpsFunc.uri) {
|
|
67
|
+
logger_1.logger.debug("Missing URI for HTTPS function in printTriggerUrls. This shouldn't happen");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
logger_1.logger.info(clc.bold("Function URL"), `(${functionsDeployHelper_1.getFunctionLabel(httpsFunc)}):`, httpsFunc.uri);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.printTriggerUrls = printTriggerUrls;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkForV2Upgrade = exports.checkForIllegalUpdate = exports.upgradedScheduleFromV1ToV2 = exports.changedV2PubSubTopic = exports.changedTriggerRegion = exports.upgradedToGCFv2WithoutSettingConcurrency = exports.createDeploymentPlan = exports.calculateUpdate = exports.calculateRegionalChanges = void 0;
|
|
4
|
+
const functionsDeployHelper_1 = require("../functionsDeployHelper");
|
|
5
|
+
const functionsDeployHelper_2 = require("../functionsDeployHelper");
|
|
6
|
+
const deploymentTool_1 = require("../../../deploymentTool");
|
|
7
|
+
const error_1 = require("../../../error");
|
|
8
|
+
const utils = require("../../../utils");
|
|
9
|
+
const backend = require("../backend");
|
|
10
|
+
const gcfv2 = require("../../../gcp/cloudfunctionsv2");
|
|
11
|
+
function calculateRegionalChanges(want, have, options) {
|
|
12
|
+
const endpointsToCreate = Object.keys(want)
|
|
13
|
+
.filter((id) => !have[id])
|
|
14
|
+
.map((id) => want[id]);
|
|
15
|
+
const endpointsToDelete = Object.keys(have)
|
|
16
|
+
.filter((id) => !want[id])
|
|
17
|
+
.filter((id) => options.deleteAll || deploymentTool_1.isFirebaseManaged(have[id].labels || {}))
|
|
18
|
+
.map((id) => have[id]);
|
|
19
|
+
const endpointsToUpdate = Object.keys(want)
|
|
20
|
+
.filter((id) => have[id])
|
|
21
|
+
.map((id) => calculateUpdate(want[id], have[id]));
|
|
22
|
+
return { endpointsToCreate, endpointsToUpdate, endpointsToDelete };
|
|
23
|
+
}
|
|
24
|
+
exports.calculateRegionalChanges = calculateRegionalChanges;
|
|
25
|
+
function calculateUpdate(want, have) {
|
|
26
|
+
checkForIllegalUpdate(want, have);
|
|
27
|
+
const update = {
|
|
28
|
+
endpoint: want,
|
|
29
|
+
};
|
|
30
|
+
const needsDelete = changedTriggerRegion(want, have) ||
|
|
31
|
+
changedV2PubSubTopic(want, have) ||
|
|
32
|
+
upgradedScheduleFromV1ToV2(want, have);
|
|
33
|
+
if (needsDelete) {
|
|
34
|
+
update.deleteAndRecreate = have;
|
|
35
|
+
}
|
|
36
|
+
return update;
|
|
37
|
+
}
|
|
38
|
+
exports.calculateUpdate = calculateUpdate;
|
|
39
|
+
function createDeploymentPlan(want, have, options = {}) {
|
|
40
|
+
const deployment = {};
|
|
41
|
+
want = backend.matchingBackend(want, (endpoint) => {
|
|
42
|
+
return functionsDeployHelper_1.functionMatchesAnyGroup(endpoint, options.filters || []);
|
|
43
|
+
});
|
|
44
|
+
have = backend.matchingBackend(have, (endpoint) => {
|
|
45
|
+
return functionsDeployHelper_1.functionMatchesAnyGroup(endpoint, options.filters || []);
|
|
46
|
+
});
|
|
47
|
+
const regions = new Set([...Object.keys(want.endpoints), ...Object.keys(have.endpoints)]);
|
|
48
|
+
for (const region of regions) {
|
|
49
|
+
deployment[region] = calculateRegionalChanges(want.endpoints[region] || {}, have.endpoints[region] || {}, options);
|
|
50
|
+
}
|
|
51
|
+
if (upgradedToGCFv2WithoutSettingConcurrency(want, have)) {
|
|
52
|
+
utils.logLabeledBullet("functions", "You are updating one or more functions to Google Cloud Functions v2, " +
|
|
53
|
+
"which introduces support for concurrent execution. New functions " +
|
|
54
|
+
"default to 80 concurrent executions, but existing functions keep the " +
|
|
55
|
+
"old default of 1. You can change this with the 'concurrency' option.");
|
|
56
|
+
}
|
|
57
|
+
return deployment;
|
|
58
|
+
}
|
|
59
|
+
exports.createDeploymentPlan = createDeploymentPlan;
|
|
60
|
+
function upgradedToGCFv2WithoutSettingConcurrency(want, have) {
|
|
61
|
+
return backend.someEndpoint(want, (endpoint) => {
|
|
62
|
+
var _a, _b;
|
|
63
|
+
if (((_b = (_a = have.endpoints[endpoint.region]) === null || _a === void 0 ? void 0 : _a[endpoint.id]) === null || _b === void 0 ? void 0 : _b.platform) !== "gcfv1") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (endpoint.platform !== "gcfv2") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (endpoint.concurrency) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
exports.upgradedToGCFv2WithoutSettingConcurrency = upgradedToGCFv2WithoutSettingConcurrency;
|
|
76
|
+
function changedTriggerRegion(want, have) {
|
|
77
|
+
if (want.platform != "gcfv2") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (have.platform != "gcfv2") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (!backend.isEventTriggered(want)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (!backend.isEventTriggered(have)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return want.eventTrigger.region != have.eventTrigger.region;
|
|
90
|
+
}
|
|
91
|
+
exports.changedTriggerRegion = changedTriggerRegion;
|
|
92
|
+
function changedV2PubSubTopic(want, have) {
|
|
93
|
+
if (want.platform !== "gcfv2") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (have.platform !== "gcfv2") {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (!backend.isEventTriggered(want)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (!backend.isEventTriggered(have)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (want.eventTrigger.eventType != gcfv2.PUBSUB_PUBLISH_EVENT) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (have.eventTrigger.eventType !== gcfv2.PUBSUB_PUBLISH_EVENT) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return have.eventTrigger.eventFilters["resource"] != want.eventTrigger.eventFilters["resource"];
|
|
112
|
+
}
|
|
113
|
+
exports.changedV2PubSubTopic = changedV2PubSubTopic;
|
|
114
|
+
function upgradedScheduleFromV1ToV2(want, have) {
|
|
115
|
+
if (have.platform !== "gcfv1") {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (want.platform !== "gcfv2") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (!backend.isScheduleTriggered(have)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (!backend.isScheduleTriggered(want)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
exports.upgradedScheduleFromV1ToV2 = upgradedScheduleFromV1ToV2;
|
|
130
|
+
function checkForIllegalUpdate(want, have) {
|
|
131
|
+
const triggerType = (e) => {
|
|
132
|
+
if (backend.isHttpsTriggered(e)) {
|
|
133
|
+
return "an HTTPS";
|
|
134
|
+
}
|
|
135
|
+
else if (backend.isEventTriggered(e)) {
|
|
136
|
+
return "a background triggered";
|
|
137
|
+
}
|
|
138
|
+
else if (backend.isScheduleTriggered(e)) {
|
|
139
|
+
return "a scheduled";
|
|
140
|
+
}
|
|
141
|
+
else if (backend.isTaskQueueTriggered(e)) {
|
|
142
|
+
return "a task queue";
|
|
143
|
+
}
|
|
144
|
+
throw Error("Functions release planner is not able to handle an unknown trigger type");
|
|
145
|
+
};
|
|
146
|
+
const wantType = triggerType(want);
|
|
147
|
+
const haveType = triggerType(have);
|
|
148
|
+
if (wantType != haveType) {
|
|
149
|
+
throw new error_1.FirebaseError(`[${functionsDeployHelper_2.getFunctionLabel(want)}] Changing from ${haveType} function to ${wantType} function is not allowed. Please delete your function and create a new one instead.`);
|
|
150
|
+
}
|
|
151
|
+
if (want.platform == "gcfv1" && have.platform == "gcfv2") {
|
|
152
|
+
throw new error_1.FirebaseError(`[${functionsDeployHelper_2.getFunctionLabel(want)}] Functions cannot be downgraded from GCFv2 to GCFv1`);
|
|
153
|
+
}
|
|
154
|
+
exports.checkForV2Upgrade(want, have);
|
|
155
|
+
}
|
|
156
|
+
exports.checkForIllegalUpdate = checkForIllegalUpdate;
|
|
157
|
+
function checkForV2Upgrade(want, have) {
|
|
158
|
+
if (want.platform == "gcfv2" && have.platform == "gcfv1") {
|
|
159
|
+
throw new error_1.FirebaseError(`[${functionsDeployHelper_2.getFunctionLabel(have)}] Upgrading from GCFv1 to GCFv2 is not yet supported. Please delete your old function or wait for this feature to be ready.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.checkForV2Upgrade = checkForV2Upgrade;
|