firebase-tools 15.22.4 → 15.23.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/lib/apiv2.js +91 -32
- package/lib/apphosting/backend.js +11 -4
- package/lib/commands/functions-delete.js +5 -3
- package/lib/commands/functions-lifecycle-list.js +94 -0
- package/lib/commands/functions-lifecycle-run.js +29 -0
- package/lib/commands/index.js +3 -0
- package/lib/database/import.js +3 -3
- package/lib/dataconnect/webhook.js +1 -2
- package/lib/deploy/functions/backend.js +9 -0
- package/lib/deploy/functions/build.js +3 -0
- package/lib/deploy/functions/prepare.js +90 -1
- package/lib/deploy/functions/prompts.js +62 -0
- package/lib/deploy/functions/release/fabricator.js +79 -4
- package/lib/deploy/functions/release/index.js +40 -25
- package/lib/deploy/functions/release/lifecycle.js +44 -51
- package/lib/deploy/functions/release/planner.js +41 -5
- package/lib/deploy/functions/runtimes/discovery/index.js +4 -3
- package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +18 -1
- package/lib/deploy/functions/runtimes/node/index.js +1 -2
- package/lib/deploy/functions/runtimes/python/index.js +1 -2
- package/lib/deploy/hosting/uploader.js +2 -3
- package/lib/downloadUtils.js +3 -1
- package/lib/emulator/auth/operations.js +18 -8
- package/lib/emulator/downloadableEmulatorInfo.json +24 -24
- package/lib/emulator/storage/apis/gcloud.js +61 -0
- package/lib/emulator/storage/multipart.js +82 -2
- package/lib/emulator/taskQueue.js +3 -11
- package/lib/extensions/extensionsHelper.js +6 -4
- package/lib/gcp/iam.js +68 -2
- package/lib/gcp/knownRoles.json +99 -0
- package/lib/gcp/resourceManager.js +42 -1
- package/lib/gemini/fdcExperience.js +2 -2
- package/lib/hosting/initMiddleware.js +1 -1
- package/lib/hosting/proxy.js +42 -20
- package/lib/management/apps.js +4 -2
- package/lib/profiler.js +1 -2
- package/lib/streamUtils.js +24 -0
- package/lib/track.js +1 -2
- package/lib/tsconfig.compile.tsbuildinfo +1 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/lib/utils.js +4 -21
- package/package.json +2 -4
package/lib/apiv2.js
CHANGED
|
@@ -7,10 +7,8 @@ exports.getAccessToken = getAccessToken;
|
|
|
7
7
|
exports.noKeepAliveAgent = noKeepAliveAgent;
|
|
8
8
|
const url_1 = require("url");
|
|
9
9
|
const stream_1 = require("stream");
|
|
10
|
-
const
|
|
10
|
+
const undici_1 = require("undici");
|
|
11
11
|
const retry = require("retry");
|
|
12
|
-
const abort_controller_1 = require("abort-controller");
|
|
13
|
-
const node_fetch_1 = require("node-fetch");
|
|
14
12
|
const http = require("http");
|
|
15
13
|
const https = require("https");
|
|
16
14
|
const util_1 = require("util");
|
|
@@ -19,6 +17,7 @@ const error_1 = require("./error");
|
|
|
19
17
|
const env_1 = require("./env");
|
|
20
18
|
const logger_1 = require("./logger");
|
|
21
19
|
const responseToError_1 = require("./responseToError");
|
|
20
|
+
const streamUtils_1 = require("./streamUtils");
|
|
22
21
|
const FormData = require("form-data");
|
|
23
22
|
const pkg = require("../package.json");
|
|
24
23
|
const CLI_VERSION = pkg.version;
|
|
@@ -36,7 +35,6 @@ const standardHeaders = () => {
|
|
|
36
35
|
exports.standardHeaders = standardHeaders;
|
|
37
36
|
const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
|
|
38
37
|
exports.GOOG_USER_PROJECT_HEADER = "x-goog-user-project";
|
|
39
|
-
const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
|
|
40
38
|
exports.CLI_OAUTH_PROJECT_NUMBER = "563584335869";
|
|
41
39
|
let accessToken = "";
|
|
42
40
|
let refreshToken = "";
|
|
@@ -56,11 +54,15 @@ async function getAccessToken() {
|
|
|
56
54
|
return data.access_token;
|
|
57
55
|
}
|
|
58
56
|
function proxyURIFromEnv() {
|
|
59
|
-
|
|
57
|
+
const uri = process.env.HTTPS_PROXY ||
|
|
60
58
|
process.env.https_proxy ||
|
|
61
59
|
process.env.HTTP_PROXY ||
|
|
62
60
|
process.env.http_proxy ||
|
|
63
|
-
undefined
|
|
61
|
+
undefined;
|
|
62
|
+
if (uri === "undefined" || uri === "null") {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return uri;
|
|
64
66
|
}
|
|
65
67
|
const httpAgentNoKeepAlive = new http.Agent({ keepAlive: false });
|
|
66
68
|
const httpsAgentNoKeepAlive = new https.Agent({ keepAlive: false });
|
|
@@ -147,7 +149,7 @@ class Client {
|
|
|
147
149
|
throw new error_1.FirebaseError("apiv2 will not handle HTTP errors while streaming and you must set `resolveOnHTTPError` and check for res.status >= 400 on your own", { exit: 2 });
|
|
148
150
|
}
|
|
149
151
|
let internalReqOptions = Object.assign(reqOptions, {
|
|
150
|
-
headers: new
|
|
152
|
+
headers: new Headers(reqOptions.headers),
|
|
151
153
|
});
|
|
152
154
|
internalReqOptions = this.addRequestHeaders(internalReqOptions);
|
|
153
155
|
if (this.opts.auth) {
|
|
@@ -176,7 +178,7 @@ class Client {
|
|
|
176
178
|
}
|
|
177
179
|
addRequestHeaders(reqOptions) {
|
|
178
180
|
if (!reqOptions.headers) {
|
|
179
|
-
reqOptions.headers = new
|
|
181
|
+
reqOptions.headers = new Headers();
|
|
180
182
|
}
|
|
181
183
|
for (const [h, v] of Object.entries((0, exports.standardHeaders)())) {
|
|
182
184
|
if (!reqOptions.headers.has(h)) {
|
|
@@ -189,15 +191,15 @@ class Client {
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
if (!reqOptions.ignoreQuotaProject &&
|
|
192
|
-
GOOGLE_CLOUD_QUOTA_PROJECT &&
|
|
193
|
-
GOOGLE_CLOUD_QUOTA_PROJECT !== "") {
|
|
194
|
-
reqOptions.headers.set(exports.GOOG_USER_PROJECT_HEADER, GOOGLE_CLOUD_QUOTA_PROJECT);
|
|
194
|
+
process.env.GOOGLE_CLOUD_QUOTA_PROJECT &&
|
|
195
|
+
process.env.GOOGLE_CLOUD_QUOTA_PROJECT !== "") {
|
|
196
|
+
reqOptions.headers.set(exports.GOOG_USER_PROJECT_HEADER, process.env.GOOGLE_CLOUD_QUOTA_PROJECT);
|
|
195
197
|
}
|
|
196
198
|
return reqOptions;
|
|
197
199
|
}
|
|
198
200
|
async addAuthHeader(reqOptions) {
|
|
199
201
|
if (!reqOptions.headers) {
|
|
200
|
-
reqOptions.headers = new
|
|
202
|
+
reqOptions.headers = new Headers();
|
|
201
203
|
}
|
|
202
204
|
let token;
|
|
203
205
|
if (isLocalInsecureRequest(this.opts.urlPrefix)) {
|
|
@@ -236,31 +238,21 @@ class Client {
|
|
|
236
238
|
headers: options.headers,
|
|
237
239
|
method: options.method,
|
|
238
240
|
redirect: options.redirect,
|
|
239
|
-
compress: options.compress,
|
|
240
241
|
};
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
const proxyURI = proxyURIFromEnv();
|
|
243
|
+
if (proxyURI) {
|
|
244
|
+
fetchOptions.dispatcher = new undici_1.ProxyAgent({ uri: proxyURI });
|
|
243
245
|
}
|
|
244
246
|
if (options.signal) {
|
|
245
|
-
|
|
246
|
-
signal.reason = "";
|
|
247
|
-
signal.throwIfAborted = () => {
|
|
248
|
-
throw new error_1.FirebaseError("Aborted");
|
|
249
|
-
};
|
|
250
|
-
fetchOptions.signal = signal;
|
|
247
|
+
fetchOptions.signal = options.signal;
|
|
251
248
|
}
|
|
252
249
|
let reqTimeout;
|
|
253
250
|
if (options.timeout) {
|
|
254
|
-
const controller = new
|
|
251
|
+
const controller = new AbortController();
|
|
255
252
|
reqTimeout = setTimeout(() => {
|
|
256
253
|
controller.abort();
|
|
257
254
|
}, options.timeout);
|
|
258
|
-
|
|
259
|
-
signal.reason = "";
|
|
260
|
-
signal.throwIfAborted = () => {
|
|
261
|
-
throw new error_1.FirebaseError("Aborted");
|
|
262
|
-
};
|
|
263
|
-
fetchOptions.signal = signal;
|
|
255
|
+
fetchOptions.signal = controller.signal;
|
|
264
256
|
}
|
|
265
257
|
let bodyReplayable = true;
|
|
266
258
|
if (typeof options.body === "string" || Buffer.isBuffer(options.body)) {
|
|
@@ -271,6 +263,7 @@ class Client {
|
|
|
271
263
|
}
|
|
272
264
|
else if (isStream(options.body)) {
|
|
273
265
|
fetchOptions.body = options.body;
|
|
266
|
+
fetchOptions.duplex = "half";
|
|
274
267
|
bodyReplayable = false;
|
|
275
268
|
}
|
|
276
269
|
else if (options.body !== undefined) {
|
|
@@ -302,10 +295,39 @@ class Client {
|
|
|
302
295
|
}
|
|
303
296
|
this.logRequest(options);
|
|
304
297
|
try {
|
|
305
|
-
|
|
298
|
+
if (options.compress === false) {
|
|
299
|
+
const undiciOptions = {
|
|
300
|
+
method: fetchOptions.method,
|
|
301
|
+
headers: {},
|
|
302
|
+
decompress: false,
|
|
303
|
+
};
|
|
304
|
+
if (fetchOptions.dispatcher) {
|
|
305
|
+
undiciOptions.dispatcher = fetchOptions.dispatcher;
|
|
306
|
+
}
|
|
307
|
+
if (fetchOptions.signal) {
|
|
308
|
+
undiciOptions.signal = fetchOptions.signal;
|
|
309
|
+
}
|
|
310
|
+
if (fetchOptions.body) {
|
|
311
|
+
undiciOptions.body = fetchOptions.body;
|
|
312
|
+
}
|
|
313
|
+
if (fetchOptions.headers) {
|
|
314
|
+
for (const [key, value] of fetchOptions.headers.entries()) {
|
|
315
|
+
undiciOptions.headers[key] = value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const undiciRes = await (0, undici_1.request)(fetchURL, undiciOptions);
|
|
319
|
+
res = new UndiciResponseCompat(undiciRes);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
res = await fetch(fetchURL, fetchOptions);
|
|
323
|
+
}
|
|
306
324
|
}
|
|
307
325
|
catch (thrown) {
|
|
308
|
-
const err = thrown
|
|
326
|
+
const err = thrown && typeof thrown === "object" && thrown.cause instanceof Error
|
|
327
|
+
? thrown.cause
|
|
328
|
+
: thrown instanceof Error
|
|
329
|
+
? thrown
|
|
330
|
+
: new Error(thrown);
|
|
309
331
|
logger_1.logger.debug(`*** [apiv2] error from fetch(${fetchURL}, ${JSON.stringify(fetchOptions)}): ${err}`);
|
|
310
332
|
const isAbortError = err.name.includes("AbortError");
|
|
311
333
|
if (isAbortError) {
|
|
@@ -339,7 +361,19 @@ class Client {
|
|
|
339
361
|
body = (await res.text());
|
|
340
362
|
}
|
|
341
363
|
else if (options.responseType === "stream") {
|
|
342
|
-
|
|
364
|
+
if (res.body) {
|
|
365
|
+
if (typeof res.body.getReader === "function") {
|
|
366
|
+
body = stream_1.Readable.fromWeb(res.body);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
body = res.body;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const emptyStream = new stream_1.Readable();
|
|
374
|
+
emptyStream.push(null);
|
|
375
|
+
body = emptyStream;
|
|
376
|
+
}
|
|
343
377
|
}
|
|
344
378
|
else {
|
|
345
379
|
throw new error_1.FirebaseError(`Unable to interpret response. Please set responseType.`, {
|
|
@@ -354,7 +388,7 @@ class Client {
|
|
|
354
388
|
isPrematureCloseError(err)) {
|
|
355
389
|
disabledKeepAlive = true;
|
|
356
390
|
fetchOptions.agent = noKeepAliveAgent;
|
|
357
|
-
const closeHeaders = new
|
|
391
|
+
const closeHeaders = new Headers(fetchOptions.headers);
|
|
358
392
|
closeHeaders.set("Connection", "close");
|
|
359
393
|
fetchOptions.headers = closeHeaders;
|
|
360
394
|
logger_1.logger.debug(`*** [apiv2] retrying ${fetchURL} without keep-alive after a premature close error`);
|
|
@@ -435,6 +469,31 @@ function isLocalInsecureRequest(urlPrefix) {
|
|
|
435
469
|
const u = new url_1.URL(urlPrefix);
|
|
436
470
|
return u.protocol === "http:";
|
|
437
471
|
}
|
|
472
|
+
class UndiciResponseCompat {
|
|
473
|
+
constructor(undiciRes) {
|
|
474
|
+
this.undiciRes = undiciRes;
|
|
475
|
+
this.status = undiciRes.statusCode;
|
|
476
|
+
this.ok = undiciRes.statusCode >= 200 && undiciRes.statusCode < 300;
|
|
477
|
+
this.headers = new Headers();
|
|
478
|
+
for (const [key, value] of Object.entries(undiciRes.headers)) {
|
|
479
|
+
if (Array.isArray(value)) {
|
|
480
|
+
for (const v of value) {
|
|
481
|
+
this.headers.append(key, v);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else if (value !== undefined) {
|
|
485
|
+
this.headers.set(key, String(value));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
this.body = undiciRes.body;
|
|
489
|
+
}
|
|
490
|
+
async text() {
|
|
491
|
+
if (!this.body) {
|
|
492
|
+
return "";
|
|
493
|
+
}
|
|
494
|
+
return (0, streamUtils_1.streamToString)(this.body);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
438
497
|
function bodyToString(body) {
|
|
439
498
|
if (isStream(body)) {
|
|
440
499
|
return "[stream]";
|
|
@@ -31,7 +31,6 @@ const ensureApiEnabled_1 = require("../ensureApiEnabled");
|
|
|
31
31
|
const deploymentTool = require("../deploymentTool");
|
|
32
32
|
const app_1 = require("./app");
|
|
33
33
|
const ora = require("ora");
|
|
34
|
-
const node_fetch_1 = require("node-fetch");
|
|
35
34
|
const rollout_1 = require("./rollout");
|
|
36
35
|
const fuzzy = require("fuzzy");
|
|
37
36
|
const experiments_1 = require("../experiments");
|
|
@@ -45,7 +44,7 @@ const apphostingPollerOptions = {
|
|
|
45
44
|
};
|
|
46
45
|
async function tlsReady(url) {
|
|
47
46
|
try {
|
|
48
|
-
await (
|
|
47
|
+
await fetch(url);
|
|
49
48
|
return true;
|
|
50
49
|
}
|
|
51
50
|
catch (err) {
|
|
@@ -213,8 +212,16 @@ async function promptNewBackendId(projectId, location) {
|
|
|
213
212
|
while (true) {
|
|
214
213
|
const backendId = await (0, prompt_1.input)({
|
|
215
214
|
default: "my-web-app",
|
|
216
|
-
message: "Provide a name for your backend [
|
|
217
|
-
validate: (s) =>
|
|
215
|
+
message: "Provide a name for your backend [3-30 characters]",
|
|
216
|
+
validate: (s) => {
|
|
217
|
+
if (!/^[a-z](?:[a-z0-9-]*[a-z0-9])?$/.test(s)) {
|
|
218
|
+
return "Must begin with a letter, can contain only lowercase, digits, hyphens, and cannot end with hyphen";
|
|
219
|
+
}
|
|
220
|
+
else if (s.length < 3 || s.length > 30) {
|
|
221
|
+
return "Must be between 3 and 30 characters";
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
},
|
|
218
225
|
});
|
|
219
226
|
try {
|
|
220
227
|
await apphosting.getBackend(projectId, location, backendId);
|
|
@@ -40,14 +40,15 @@ exports.command = new command_1.Command("functions:delete [filters...]")
|
|
|
40
40
|
if (options.region) {
|
|
41
41
|
existingBackend.endpoints = { [options.region]: existingBackend.endpoints[options.region] };
|
|
42
42
|
}
|
|
43
|
-
const plan = planner.createDeploymentPlan({
|
|
43
|
+
const plan = await planner.createDeploymentPlan({
|
|
44
44
|
wantBackend: backend.empty(),
|
|
45
45
|
haveBackend: existingBackend,
|
|
46
46
|
codebase: "",
|
|
47
|
+
projectId: context.projectId,
|
|
47
48
|
filters: context.filters,
|
|
48
49
|
deleteAll: true,
|
|
49
50
|
});
|
|
50
|
-
const allEpToDelete = Object.values(plan)
|
|
51
|
+
const allEpToDelete = Object.values(plan.regionalChangesets)
|
|
51
52
|
.map((changes) => changes.endpointsToDelete)
|
|
52
53
|
.reduce(functional_1.reduceFlat, [])
|
|
53
54
|
.sort(backend.compareFunctions);
|
|
@@ -80,8 +81,9 @@ exports.command = new command_1.Command("functions:delete [filters...]")
|
|
|
80
81
|
executor: new executor.QueueExecutor({}),
|
|
81
82
|
sources: {},
|
|
82
83
|
projectNumber: options.projectNumber || (await (0, getProjectNumber_1.getProjectNumber)({ projectId: context.projectId })),
|
|
84
|
+
projectId: context.projectId,
|
|
83
85
|
});
|
|
84
|
-
const summary = await fab.applyPlan(plan);
|
|
86
|
+
const summary = await fab.applyPlan({ default: plan });
|
|
85
87
|
await reporter.logAndTrackDeployStats(summary);
|
|
86
88
|
reporter.printErrors(summary);
|
|
87
89
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
exports.loadCodebaseBuild = loadCodebaseBuild;
|
|
5
|
+
const command_1 = require("../command");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
const prepare_1 = require("../deploy/functions/prepare");
|
|
8
|
+
const projectConfig_1 = require("../functions/projectConfig");
|
|
9
|
+
const adminSdkConfig_1 = require("../emulator/adminSdkConfig");
|
|
10
|
+
const projectUtils_1 = require("../projectUtils");
|
|
11
|
+
const error_1 = require("../error");
|
|
12
|
+
const ensureApiEnabled = require("../ensureApiEnabled");
|
|
13
|
+
const api_1 = require("../api");
|
|
14
|
+
const prepareFunctionsUpload_1 = require("../deploy/functions/prepareFunctionsUpload");
|
|
15
|
+
const self = require("./functions-lifecycle-list");
|
|
16
|
+
async function loadCodebaseBuild(codebase, options) {
|
|
17
|
+
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
18
|
+
if (!options.config) {
|
|
19
|
+
throw new error_1.FirebaseError("Not in a Firebase project directory (firebase.json not found).");
|
|
20
|
+
}
|
|
21
|
+
const fnConfig = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions);
|
|
22
|
+
const hasCodebase = fnConfig.some((c) => c.codebase === codebase);
|
|
23
|
+
if (!hasCodebase) {
|
|
24
|
+
throw new error_1.FirebaseError(`Codebase "${codebase}" is not defined in firebase.json.`);
|
|
25
|
+
}
|
|
26
|
+
const firebaseConfig = await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(projectId);
|
|
27
|
+
if (!firebaseConfig) {
|
|
28
|
+
throw new error_1.FirebaseError("Admin SDK config unexpectedly undefined - have you run firebase init?");
|
|
29
|
+
}
|
|
30
|
+
let runtimeConfig = { firebase: firebaseConfig };
|
|
31
|
+
if (fnConfig.some(projectConfig_1.shouldUseRuntimeConfig)) {
|
|
32
|
+
try {
|
|
33
|
+
const runtimeConfigApiEnabled = await ensureApiEnabled.check(projectId, (0, api_1.runtimeconfigOrigin)(), "runtimeconfig", true);
|
|
34
|
+
if (runtimeConfigApiEnabled) {
|
|
35
|
+
runtimeConfig = { ...runtimeConfig, ...(await (0, prepareFunctionsUpload_1.getFunctionsConfig)(projectId)) };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
logger_1.logger.debug("Could not check Runtime Config API status, assuming disabled:", err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const wantBuilds = await (0, prepare_1.loadCodebases)(fnConfig, options, firebaseConfig, runtimeConfig, undefined);
|
|
43
|
+
const codebaseBuild = wantBuilds[codebase];
|
|
44
|
+
if (!codebaseBuild) {
|
|
45
|
+
throw new error_1.FirebaseError(`Failed to load build for codebase "${codebase}".`);
|
|
46
|
+
}
|
|
47
|
+
return codebaseBuild;
|
|
48
|
+
}
|
|
49
|
+
exports.command = new command_1.Command("functions:lifecycle:list <codebase>")
|
|
50
|
+
.description("list all the lifecycle hooks defined in a codebase")
|
|
51
|
+
.action(async (codebase, options) => {
|
|
52
|
+
const codebaseBuild = await self.loadCodebaseBuild(codebase, options);
|
|
53
|
+
const hooks = codebaseBuild.lifecycleHooks || {};
|
|
54
|
+
if (Object.keys(hooks).length === 0) {
|
|
55
|
+
logger_1.logger.info(`No lifecycle hooks configured for codebase "${codebase}".`);
|
|
56
|
+
return hooks;
|
|
57
|
+
}
|
|
58
|
+
for (const [event, hook] of Object.entries(hooks)) {
|
|
59
|
+
logger_1.logger.info(`\nEvent: ${event}`);
|
|
60
|
+
if ("task" in hook) {
|
|
61
|
+
logger_1.logger.info(` Action: Task`);
|
|
62
|
+
logger_1.logger.info(` Target Function: ${hook.task.function}`);
|
|
63
|
+
if (hook.task.body) {
|
|
64
|
+
logger_1.logger.info(` Body: ${JSON.stringify(hook.task.body, null, 2).replace(/\n/g, "\n ")}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if ("call" in hook) {
|
|
68
|
+
logger_1.logger.info(` Action: Call`);
|
|
69
|
+
logger_1.logger.info(` Target Function: ${hook.call.function}`);
|
|
70
|
+
if (hook.call.params) {
|
|
71
|
+
logger_1.logger.info(` Params: ${JSON.stringify(hook.call.params, null, 2).replace(/\n/g, "\n ")}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if ("http" in hook) {
|
|
75
|
+
logger_1.logger.info(` Action: HTTP`);
|
|
76
|
+
if (hook.http.function) {
|
|
77
|
+
logger_1.logger.info(` Target Function: ${hook.http.function}`);
|
|
78
|
+
}
|
|
79
|
+
if (hook.http.url) {
|
|
80
|
+
logger_1.logger.info(` URL: ${hook.http.url}`);
|
|
81
|
+
}
|
|
82
|
+
if (hook.http.method) {
|
|
83
|
+
logger_1.logger.info(` Method: ${hook.http.method}`);
|
|
84
|
+
}
|
|
85
|
+
if (hook.http.headers) {
|
|
86
|
+
logger_1.logger.info(` Headers: ${JSON.stringify(hook.http.headers, null, 2).replace(/\n/g, "\n ")}`);
|
|
87
|
+
}
|
|
88
|
+
if (hook.http.body) {
|
|
89
|
+
logger_1.logger.info(` Body: ${JSON.stringify(hook.http.body, null, 2).replace(/\n/g, "\n ")}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return hooks;
|
|
94
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const projectUtils_1 = require("../projectUtils");
|
|
6
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
const functions_lifecycle_list_1 = require("./functions-lifecycle-list");
|
|
9
|
+
const backend = require("../deploy/functions/backend");
|
|
10
|
+
const lifecycle_1 = require("../deploy/functions/release/lifecycle");
|
|
11
|
+
exports.command = new command_1.Command("functions:lifecycle:run <hookName> <codebase>")
|
|
12
|
+
.description("run a specific lifecycle hook in isolation")
|
|
13
|
+
.before(requirePermissions_1.requirePermissions, ["cloudfunctions.functions.list", "run.services.list"])
|
|
14
|
+
.action(async (hookName, codebase, options) => {
|
|
15
|
+
if (hookName !== "afterFirstDeploy" && hookName !== "afterRedeploy") {
|
|
16
|
+
throw new error_1.FirebaseError(`Invalid hook name "${hookName}". Supported hooks are "afterFirstDeploy" and "afterRedeploy".`);
|
|
17
|
+
}
|
|
18
|
+
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
19
|
+
const codebaseBuild = await (0, functions_lifecycle_list_1.loadCodebaseBuild)(codebase, options);
|
|
20
|
+
const hook = codebaseBuild.lifecycleHooks?.[hookName];
|
|
21
|
+
if (!hook) {
|
|
22
|
+
throw new error_1.FirebaseError(`No lifecycle hook "${hookName}" configured for codebase "${codebase}".`);
|
|
23
|
+
}
|
|
24
|
+
const context = {
|
|
25
|
+
projectId,
|
|
26
|
+
};
|
|
27
|
+
const existingBackend = await backend.existingBackend(context);
|
|
28
|
+
await (0, lifecycle_1.executeHook)(hookName, hook, existingBackend);
|
|
29
|
+
});
|
package/lib/commands/index.js
CHANGED
|
@@ -151,6 +151,9 @@ function load(client) {
|
|
|
151
151
|
client.functions.log = loadCommand("functions-log");
|
|
152
152
|
client.functions.shell = loadCommand("functions-shell");
|
|
153
153
|
client.functions.list = loadCommand("functions-list");
|
|
154
|
+
client.functions.lifecycle = {};
|
|
155
|
+
client.functions.lifecycle.list = loadCommand("functions-lifecycle-list");
|
|
156
|
+
client.functions.lifecycle.run = loadCommand("functions-lifecycle-run");
|
|
154
157
|
if (experiments.isEnabled("deletegcfartifacts")) {
|
|
155
158
|
client.functions.deletegcfartifacts = loadCommand("functions-deletegcfartifacts");
|
|
156
159
|
}
|
package/lib/database/import.js
CHANGED
|
@@ -6,7 +6,6 @@ const Filter = require("stream-json/filters/Filter");
|
|
|
6
6
|
const stream = require("stream");
|
|
7
7
|
const StreamObject = require("stream-json/streamers/StreamObject");
|
|
8
8
|
const apiv2_1 = require("../apiv2");
|
|
9
|
-
const node_fetch_1 = require("node-fetch");
|
|
10
9
|
const error_1 = require("../error");
|
|
11
10
|
const utils_1 = require("../utils");
|
|
12
11
|
class BatchChunks extends stream.Transform {
|
|
@@ -169,8 +168,9 @@ class DatabaseImporter {
|
|
|
169
168
|
}
|
|
170
169
|
catch (err) {
|
|
171
170
|
const isTimeoutErr = err instanceof error_1.FirebaseError &&
|
|
172
|
-
err.original
|
|
173
|
-
|
|
171
|
+
(err.original?.name === "AbortError" ||
|
|
172
|
+
err.original?.code === "ETIMEDOUT" ||
|
|
173
|
+
err.original?.cause?.code === "ETIMEDOUT");
|
|
174
174
|
if (isTimeoutErr) {
|
|
175
175
|
await new Promise((res) => setTimeout(res, this.nonFatalRetryTimeout));
|
|
176
176
|
return await doRequest();
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.port = exports.DEFAULT_PORT = exports.VSCODE_MESSAGE = void 0;
|
|
4
4
|
exports.sendVSCodeMessage = sendVSCodeMessage;
|
|
5
|
-
const node_fetch_1 = require("node-fetch");
|
|
6
5
|
const logger_1 = require("../logger");
|
|
7
6
|
var VSCODE_MESSAGE;
|
|
8
7
|
(function (VSCODE_MESSAGE) {
|
|
@@ -15,7 +14,7 @@ exports.port = process.env.VSCODE_WEBHOOK_PORT || exports.DEFAULT_PORT;
|
|
|
15
14
|
async function sendVSCodeMessage(body) {
|
|
16
15
|
const jsonBody = JSON.stringify(body);
|
|
17
16
|
try {
|
|
18
|
-
return await (
|
|
17
|
+
return await fetch(`http://localhost:${exports.port}/vscode/notify`, {
|
|
19
18
|
method: "POST",
|
|
20
19
|
headers: {
|
|
21
20
|
Accept: "application/json",
|
|
@@ -165,6 +165,7 @@ function of(...endpoints) {
|
|
|
165
165
|
function merge(...backends) {
|
|
166
166
|
const merged = of(...(0, functional_1.flattenArray)(backends.map((b) => allEndpoints(b))));
|
|
167
167
|
const apiToReasons = {};
|
|
168
|
+
const requiredRoles = new Set();
|
|
168
169
|
for (const b of backends) {
|
|
169
170
|
for (const { api, reason } of b.requiredAPIs) {
|
|
170
171
|
const reasons = apiToReasons[api] || new Set();
|
|
@@ -174,6 +175,11 @@ function merge(...backends) {
|
|
|
174
175
|
apiToReasons[api] = reasons;
|
|
175
176
|
}
|
|
176
177
|
merged.environmentVariables = { ...merged.environmentVariables, ...b.environmentVariables };
|
|
178
|
+
if (b.requiredRoles) {
|
|
179
|
+
for (const role of b.requiredRoles) {
|
|
180
|
+
requiredRoles.add(role);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
177
183
|
if (b.lifecycleHooks) {
|
|
178
184
|
merged.lifecycleHooks = { ...(merged.lifecycleHooks || {}), ...b.lifecycleHooks };
|
|
179
185
|
}
|
|
@@ -181,6 +187,9 @@ function merge(...backends) {
|
|
|
181
187
|
for (const [api, reasons] of Object.entries(apiToReasons)) {
|
|
182
188
|
merged.requiredAPIs.push({ api, reason: Array.from(reasons).join(" ") });
|
|
183
189
|
}
|
|
190
|
+
if (requiredRoles.size > 0) {
|
|
191
|
+
merged.requiredRoles = Array.from(requiredRoles);
|
|
192
|
+
}
|
|
184
193
|
return merged;
|
|
185
194
|
}
|
|
186
195
|
function isEmptyBackend(backend) {
|
|
@@ -263,6 +263,9 @@ function toBackend(build, paramValues) {
|
|
|
263
263
|
}
|
|
264
264
|
const bkend = backend.of(...bkEndpoints);
|
|
265
265
|
bkend.requiredAPIs = build.requiredAPIs;
|
|
266
|
+
if (build.requiredRoles) {
|
|
267
|
+
bkend.requiredRoles = build.requiredRoles;
|
|
268
|
+
}
|
|
266
269
|
if (build.lifecycleHooks) {
|
|
267
270
|
bkend.lifecycleHooks = build.lifecycleHooks;
|
|
268
271
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EVENTARC_SOURCE_ENV = void 0;
|
|
4
|
+
exports.discoverSecurityDetails = discoverSecurityDetails;
|
|
4
5
|
exports.prepare = prepare;
|
|
5
6
|
exports.resolveDefaultRegionsForBuild = resolveDefaultRegionsForBuild;
|
|
6
7
|
exports.inferDetailsFromExisting = inferDetailsFromExisting;
|
|
@@ -43,7 +44,94 @@ const backend_1 = require("./backend");
|
|
|
43
44
|
const functional_1 = require("../../functional");
|
|
44
45
|
const prepare_1 = require("../extensions/prepare");
|
|
45
46
|
const prompt = require("../../prompt");
|
|
47
|
+
const iam = require("../../gcp/iam");
|
|
48
|
+
const resourcemanager = require("../../gcp/resourceManager");
|
|
46
49
|
exports.EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE";
|
|
50
|
+
async function discoverSecurityDetails(codebase, want, have, projectId) {
|
|
51
|
+
const requiredRoles = want.requiredRoles;
|
|
52
|
+
const firstHave = backend.allEndpoints(have)[0];
|
|
53
|
+
let existingManagedSA;
|
|
54
|
+
let haveRolesEtag;
|
|
55
|
+
if (firstHave) {
|
|
56
|
+
haveRolesEtag = firstHave.labels?.["firebase-declarative-security-etag"];
|
|
57
|
+
existingManagedSA = firstHave.serviceAccount?.startsWith("firebase-fn-")
|
|
58
|
+
? firstHave.serviceAccount
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
if (!requiredRoles && (!existingManagedSA || !haveRolesEtag)) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
if (requiredRoles &&
|
|
65
|
+
backend.someEndpoint(want, (e) => typeof e.serviceAccount === "string" &&
|
|
66
|
+
e.serviceAccount !== "default" &&
|
|
67
|
+
!e.serviceAccount.startsWith("firebase-fn-"))) {
|
|
68
|
+
throw new error_1.FirebaseError(`Cannot use explicit custom service accounts on functions while using declarative security in codebase ${codebase}.`);
|
|
69
|
+
}
|
|
70
|
+
if (!requiredRoles && existingManagedSA && haveRolesEtag) {
|
|
71
|
+
for (const endpoint of backend.allEndpoints(want)) {
|
|
72
|
+
if (!endpoint.serviceAccount || endpoint.serviceAccount === existingManagedSA) {
|
|
73
|
+
endpoint.serviceAccount = "default";
|
|
74
|
+
}
|
|
75
|
+
if (endpoint.labels) {
|
|
76
|
+
delete endpoint.labels["firebase-declarative-security-etag"];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
haveRolesEtag,
|
|
81
|
+
existingManagedSA,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let managedSA = existingManagedSA;
|
|
85
|
+
if (!managedSA) {
|
|
86
|
+
const saToCreate = await iam.generateManagedServiceAccountName(projectId, "firebase-fn");
|
|
87
|
+
managedSA = `${saToCreate}@${projectId}.iam.gserviceaccount.com`;
|
|
88
|
+
}
|
|
89
|
+
const existingSalt = haveRolesEtag ? haveRolesEtag.split("-")[0] : undefined;
|
|
90
|
+
const newEtag = iam.computeRolesEtag(requiredRoles, existingSalt);
|
|
91
|
+
for (const endpoint of backend.allEndpoints(want)) {
|
|
92
|
+
endpoint.serviceAccount = managedSA;
|
|
93
|
+
endpoint.labels = endpoint.labels || {};
|
|
94
|
+
endpoint.labels["firebase-declarative-security-etag"] = newEtag;
|
|
95
|
+
}
|
|
96
|
+
if (haveRolesEtag && haveRolesEtag === newEtag) {
|
|
97
|
+
return {
|
|
98
|
+
haveRoles: requiredRoles,
|
|
99
|
+
haveRolesEtag,
|
|
100
|
+
existingManagedSA,
|
|
101
|
+
managedSA,
|
|
102
|
+
newEtag,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const permissionsToTest = ["resourcemanager.projects.setIamPolicy"];
|
|
106
|
+
if (!existingManagedSA) {
|
|
107
|
+
permissionsToTest.push("iam.serviceAccounts.create");
|
|
108
|
+
}
|
|
109
|
+
const iamResult = await iam.testIamPermissions(projectId, permissionsToTest);
|
|
110
|
+
if (!iamResult.passed) {
|
|
111
|
+
if (!existingManagedSA) {
|
|
112
|
+
throw new error_1.FirebaseError(`Cannot enable declarative security because you do not have permissions necessary to create the service account. Please ask an IAM administrator to perform the next deploy.`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
throw new error_1.FirebaseError(`You do not have access to make the policy changes required in codebase ${codebase} deploy. Please ask an IAM administrator to perform the next deploy.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let haveRoles = [];
|
|
119
|
+
if (existingManagedSA) {
|
|
120
|
+
try {
|
|
121
|
+
haveRoles = await resourcemanager.getServiceAccountRoles(projectId, managedSA);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
throw new error_1.FirebaseError(`The declarative security roles for codebase ${codebase} have changed, but you do not have access to see what has changed. Please ask an IAM administrator to perform the next deploy.`, { original: err });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
haveRoles,
|
|
129
|
+
haveRolesEtag,
|
|
130
|
+
existingManagedSA,
|
|
131
|
+
managedSA,
|
|
132
|
+
newEtag,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
47
135
|
async function prepare(context, options, payload) {
|
|
48
136
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
49
137
|
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
@@ -196,7 +284,8 @@ async function prepare(context, options, payload) {
|
|
|
196
284
|
const haveBackends = (0, functionsDeployHelper_1.groupEndpointsByCodebase)(wantBackends, backend.allEndpoints(existingBackend));
|
|
197
285
|
for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
|
|
198
286
|
const haveBackend = haveBackends[codebase] || backend.empty();
|
|
199
|
-
|
|
287
|
+
const security = await discoverSecurityDetails(codebase, wantBackend, haveBackend, projectId);
|
|
288
|
+
payload.functions[codebase] = { wantBackend, haveBackend, ...security };
|
|
200
289
|
}
|
|
201
290
|
for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) {
|
|
202
291
|
inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase));
|