firebase-tools 15.22.3 → 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/accountExporter.js +1 -0
- package/lib/accountImporter.js +1 -0
- package/lib/apiv2.js +91 -32
- package/lib/apphosting/backend.js +11 -4
- package/lib/apphosting/localbuilds.js +50 -0
- 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/apphosting/prepare.js +100 -1
- package/lib/deploy/functions/backend.js +15 -1
- package/lib/deploy/functions/build.js +28 -0
- package/lib/deploy/functions/prepare.js +91 -2
- package/lib/deploy/functions/prompts.js +62 -0
- package/lib/deploy/functions/release/fabricator.js +79 -4
- package/lib/deploy/functions/release/index.js +42 -23
- package/lib/deploy/functions/release/lifecycle.js +113 -0
- 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 +50 -0
- package/lib/deploy/functions/runtimes/node/index.js +1 -2
- package/lib/deploy/functions/runtimes/python/index.js +1 -2
- package/lib/deploy/functions/validate.js +44 -1
- 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 +31 -31
- 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/firestore/api.js +3 -1
- package/lib/gcp/cloudtasks.js +4 -0
- 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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.determineDeploymentEvent = determineDeploymentEvent;
|
|
4
|
+
exports.executeLifecycleHooks = executeLifecycleHooks;
|
|
5
|
+
exports.executeHook = executeHook;
|
|
6
|
+
const backend = require("../backend");
|
|
7
|
+
const error_1 = require("../../../error");
|
|
8
|
+
const logger_1 = require("../../../logger");
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const cloudtasks = require("../../../gcp/cloudtasks");
|
|
11
|
+
const computeEngine = require("../../../gcp/computeEngine");
|
|
12
|
+
const projects_1 = require("../../../management/projects");
|
|
13
|
+
const functional_1 = require("../../../functional");
|
|
14
|
+
function determineDeploymentEvent(haveBackend) {
|
|
15
|
+
const hasExistingEndpoints = backend.someEndpoint(haveBackend, () => true);
|
|
16
|
+
if (!hasExistingEndpoints) {
|
|
17
|
+
return "afterFirstDeploy";
|
|
18
|
+
}
|
|
19
|
+
return "afterRedeploy";
|
|
20
|
+
}
|
|
21
|
+
async function executeLifecycleHooks(wantBackend, haveBackend, plan, codebase) {
|
|
22
|
+
const event = determineDeploymentEvent(haveBackend);
|
|
23
|
+
const hooks = wantBackend.lifecycleHooks || {};
|
|
24
|
+
const hook = hooks[event];
|
|
25
|
+
if (!hook) {
|
|
26
|
+
logger_1.logger.debug(`No lifecycle hook configured for event: ${event}`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (event === "afterRedeploy" && plan) {
|
|
30
|
+
const codebasePlans = codebase ? [plan[codebase]].filter(Boolean) : Object.values(plan);
|
|
31
|
+
const hasResourceModifications = codebasePlans.some((codebasePlan) => Object.values(codebasePlan.regionalChangesets).some((changeset) => changeset.endpointsToCreate.length > 0 ||
|
|
32
|
+
changeset.endpointsToUpdate.length > 0 ||
|
|
33
|
+
changeset.endpointsToDelete.length > 0));
|
|
34
|
+
if (!hasResourceModifications) {
|
|
35
|
+
(0, utils_1.logLabeledBullet)("functions", `No resources modified for codebase: ${codebase ?? "default"}. Skipping afterRedeploy lifecycle hook.`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await executeHook(event, hook, wantBackend);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
(0, utils_1.logLabeledWarning)("functions", `Failed to execute ${event} lifecycle hook: ${errorMsg}`);
|
|
46
|
+
(0, utils_1.logLabeledBullet)("functions", `You can retry the lifecycle hook in isolation by running: firebase functions:lifecycle:run ${event} ${codebase ?? "default"}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function executeTaskQueueHook(taskHook, wantBackend) {
|
|
51
|
+
const targetEndpoint = backend.findEndpoint(wantBackend, (ep) => ep.id === taskHook.function);
|
|
52
|
+
if (!targetEndpoint) {
|
|
53
|
+
throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" not found in backend for lifecycle hook.`);
|
|
54
|
+
}
|
|
55
|
+
if (!backend.isTaskQueueTriggered(targetEndpoint)) {
|
|
56
|
+
throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" is not a task queue function.`);
|
|
57
|
+
}
|
|
58
|
+
const queueName = cloudtasks.queueNameForEndpoint(targetEndpoint);
|
|
59
|
+
const bodyStr = taskHook.body ? JSON.stringify(taskHook.body) : "";
|
|
60
|
+
const body = bodyStr ? Buffer.from(bodyStr).toString("base64") : undefined;
|
|
61
|
+
const url = targetEndpoint.uri;
|
|
62
|
+
if (!url) {
|
|
63
|
+
throw new error_1.FirebaseError(`Target endpoint "${taskHook.function}" does not have a trigger URI.`);
|
|
64
|
+
}
|
|
65
|
+
const projectMetadata = await (0, projects_1.getProject)(targetEndpoint.project);
|
|
66
|
+
const projectNumber = projectMetadata.projectNumber;
|
|
67
|
+
const sa = targetEndpoint.serviceAccount || (await computeEngine.getDefaultServiceAccount(projectNumber));
|
|
68
|
+
const task = {
|
|
69
|
+
httpRequest: {
|
|
70
|
+
url,
|
|
71
|
+
httpMethod: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
},
|
|
75
|
+
oidcToken: {
|
|
76
|
+
serviceAccountEmail: sa,
|
|
77
|
+
audience: url,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
if (body) {
|
|
82
|
+
task.httpRequest.body = body;
|
|
83
|
+
}
|
|
84
|
+
await cloudtasks.enqueueTask(queueName, task);
|
|
85
|
+
(0, utils_1.logLabeledSuccess)("functions", `Successfully queued task for lifecycle hook ${taskHook.function} in queue ${queueName}.`);
|
|
86
|
+
return targetEndpoint;
|
|
87
|
+
}
|
|
88
|
+
function getCloudConsoleLogUrl(endpoint) {
|
|
89
|
+
const { project, region, id } = endpoint;
|
|
90
|
+
const serviceName = endpoint.runServiceId || id;
|
|
91
|
+
const query = `resource.type="cloud_run_revision"\nresource.labels.service_name="${serviceName}"\nresource.labels.location="${region}"`;
|
|
92
|
+
return `https://console.cloud.google.com/logs/query;query=${encodeURIComponent(query)};project=${project}`;
|
|
93
|
+
}
|
|
94
|
+
async function executeHook(event, hook, backendSpec) {
|
|
95
|
+
let executedEndpoint;
|
|
96
|
+
if ("task" in hook) {
|
|
97
|
+
(0, utils_1.logLabeledBullet)("functions", `Executing ${event} lifecycle hook targeting: ${hook.task.function}...`);
|
|
98
|
+
executedEndpoint = await executeTaskQueueHook(hook.task, backendSpec);
|
|
99
|
+
}
|
|
100
|
+
else if ("call" in hook) {
|
|
101
|
+
throw new error_1.FirebaseError(`Lifecycle hook action type "call" is not supported.`);
|
|
102
|
+
}
|
|
103
|
+
else if ("http" in hook) {
|
|
104
|
+
throw new error_1.FirebaseError(`Lifecycle hook action type "http" is not supported.`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
(0, functional_1.assertExhaustive)(hook);
|
|
108
|
+
}
|
|
109
|
+
if (executedEndpoint) {
|
|
110
|
+
(0, utils_1.logLabeledBullet)("functions", `View logs for ${event} at: ${getCloudConsoleLogUrl(executedEndpoint)}`);
|
|
111
|
+
}
|
|
112
|
+
return executedEndpoint;
|
|
113
|
+
}
|
|
@@ -75,9 +75,25 @@ function calculateUpdate(want, have) {
|
|
|
75
75
|
}
|
|
76
76
|
return update;
|
|
77
77
|
}
|
|
78
|
-
function createDeploymentPlan(args) {
|
|
79
|
-
let { wantBackend, haveBackend, codebase, filters, deleteAll } = args;
|
|
80
|
-
|
|
78
|
+
async function createDeploymentPlan(args) {
|
|
79
|
+
let { wantBackend, haveBackend, codebase, filters, deleteAll, haveRoles, existingManagedSA, managedSA, } = args;
|
|
80
|
+
const requiredRoles = wantBackend.requiredRoles;
|
|
81
|
+
const roles = haveRoles || [];
|
|
82
|
+
let rolesToAdd;
|
|
83
|
+
let rolesToRemove;
|
|
84
|
+
let serviceAccountToCreate;
|
|
85
|
+
let serviceAccountToDelete;
|
|
86
|
+
const isFiltered = !!(filters && filters.length > 0 && !deleteAll);
|
|
87
|
+
if (requiredRoles) {
|
|
88
|
+
rolesToAdd = requiredRoles.filter((r) => !roles.includes(r));
|
|
89
|
+
rolesToRemove = roles.filter((r) => !requiredRoles.includes(r));
|
|
90
|
+
if (!existingManagedSA && managedSA) {
|
|
91
|
+
serviceAccountToCreate = managedSA;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (existingManagedSA && !isFiltered) {
|
|
95
|
+
serviceAccountToDelete = existingManagedSA;
|
|
96
|
+
}
|
|
81
97
|
wantBackend = backend.matchingBackend(wantBackend, (endpoint) => {
|
|
82
98
|
return (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, filters);
|
|
83
99
|
});
|
|
@@ -85,13 +101,14 @@ function createDeploymentPlan(args) {
|
|
|
85
101
|
haveBackend = backend.matchingBackend(haveBackend, (endpoint) => {
|
|
86
102
|
return wantedEndpoint(endpoint) || (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, filters);
|
|
87
103
|
});
|
|
104
|
+
const regionalChangesets = {};
|
|
88
105
|
const regions = new Set([
|
|
89
106
|
...Object.keys(wantBackend.endpoints),
|
|
90
107
|
...Object.keys(haveBackend.endpoints),
|
|
91
108
|
]);
|
|
92
109
|
for (const region of regions) {
|
|
93
110
|
const changesets = calculateChangesets(wantBackend.endpoints[region] || {}, haveBackend.endpoints[region] || {}, (e) => `${codebase}-${e.region}-${e.availableMemoryMb || "default"}`, deleteAll);
|
|
94
|
-
|
|
111
|
+
Object.assign(regionalChangesets, changesets);
|
|
95
112
|
}
|
|
96
113
|
if (upgradedToGCFv2WithoutSettingConcurrency(wantBackend, haveBackend)) {
|
|
97
114
|
utils.logLabeledBullet("functions", "You are updating one or more functions to Google Cloud Functions v2, " +
|
|
@@ -99,7 +116,26 @@ function createDeploymentPlan(args) {
|
|
|
99
116
|
"default to 80 concurrent executions, but existing functions keep the " +
|
|
100
117
|
"old default of 1. You can change this with the 'concurrency' option.");
|
|
101
118
|
}
|
|
102
|
-
|
|
119
|
+
if (requiredRoles) {
|
|
120
|
+
if (!managedSA) {
|
|
121
|
+
throw new error_1.FirebaseError("managedServiceAccount is required when requiredRoles is defined.", {
|
|
122
|
+
exit: 1,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
regionalChangesets,
|
|
127
|
+
rolesToAdd,
|
|
128
|
+
rolesToRemove,
|
|
129
|
+
serviceAccountToCreate,
|
|
130
|
+
managedServiceAccount: managedSA,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
return {
|
|
135
|
+
regionalChangesets,
|
|
136
|
+
serviceAccountToDelete,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
103
139
|
}
|
|
104
140
|
function upgradedToGCFv2WithoutSettingConcurrency(want, have) {
|
|
105
141
|
return backend.someEndpoint(want, (endpoint) => {
|
|
@@ -5,7 +5,6 @@ exports.yamlToBuild = yamlToBuild;
|
|
|
5
5
|
exports.detectFromYaml = detectFromYaml;
|
|
6
6
|
exports.detectFromPort = detectFromPort;
|
|
7
7
|
exports.detectFromOutputPath = detectFromOutputPath;
|
|
8
|
-
const node_fetch_1 = require("node-fetch");
|
|
9
8
|
const fs = require("fs");
|
|
10
9
|
const path = require("path");
|
|
11
10
|
const yaml = require("yaml");
|
|
@@ -65,12 +64,14 @@ async function detectFromPort(port, project, runtime, initialDelay = 0, timeout
|
|
|
65
64
|
const url = `http://127.0.0.1:${port}/__/functions.yaml`;
|
|
66
65
|
while (true) {
|
|
67
66
|
try {
|
|
68
|
-
res = await Promise.race([(
|
|
67
|
+
res = await Promise.race([fetch(url), timedOut]);
|
|
69
68
|
break;
|
|
70
69
|
}
|
|
71
70
|
catch (err) {
|
|
71
|
+
const realErr = err?.cause || err;
|
|
72
72
|
if (err?.name === "FetchError" ||
|
|
73
|
-
|
|
73
|
+
realErr?.name === "FetchError" ||
|
|
74
|
+
["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"].includes(realErr?.code)) {
|
|
74
75
|
continue;
|
|
75
76
|
}
|
|
76
77
|
throw err;
|
|
@@ -20,12 +20,17 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
|
|
|
20
20
|
specVersion: "string",
|
|
21
21
|
params: "array",
|
|
22
22
|
requiredAPIs: "array",
|
|
23
|
+
requiredRoles: "array",
|
|
23
24
|
endpoints: "object",
|
|
24
25
|
extensions: "object",
|
|
26
|
+
lifecycleHooks: "object",
|
|
25
27
|
});
|
|
26
28
|
const bd = build.empty();
|
|
27
29
|
bd.params = manifest.params || [];
|
|
28
30
|
bd.requiredAPIs = parseRequiredAPIs(manifest);
|
|
31
|
+
if (manifest.requiredRoles) {
|
|
32
|
+
bd.requiredRoles = parseRequiredRoles(manifest);
|
|
33
|
+
}
|
|
29
34
|
for (const id of Object.keys(manifest.endpoints)) {
|
|
30
35
|
const me = manifest.endpoints[id];
|
|
31
36
|
assertBuildEndpoint(me, id);
|
|
@@ -41,8 +46,53 @@ function buildFromV1Alpha1(yaml, project, region, runtime) {
|
|
|
41
46
|
bd.extensions[id] = be;
|
|
42
47
|
}
|
|
43
48
|
}
|
|
49
|
+
if (manifest.lifecycleHooks) {
|
|
50
|
+
bd.lifecycleHooks = {};
|
|
51
|
+
for (const id of Object.keys(manifest.lifecycleHooks)) {
|
|
52
|
+
if (id !== "afterFirstDeploy" && id !== "afterRedeploy") {
|
|
53
|
+
throw new error_1.FirebaseError(`Invalid eventType "${id}" for lifecycle hook.`);
|
|
54
|
+
}
|
|
55
|
+
const hook = manifest.lifecycleHooks[id];
|
|
56
|
+
if (!hook || typeof hook !== "object") {
|
|
57
|
+
throw new error_1.FirebaseError(`Invalid lifecycle hook configuration for "${id}".`);
|
|
58
|
+
}
|
|
59
|
+
if (hook.task) {
|
|
60
|
+
if (typeof hook.task.function !== "string" || !hook.task.function) {
|
|
61
|
+
throw new error_1.FirebaseError(`Invalid target "${hook.task.function || ""}" for lifecycle hook "${id}"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (hook.call) {
|
|
65
|
+
if (typeof hook.call.function !== "string" || !hook.call.function) {
|
|
66
|
+
throw new error_1.FirebaseError(`Invalid target "${hook.call.function || ""}" for lifecycle hook "${id}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (hook.http) {
|
|
70
|
+
const target = hook.http.url || hook.http.function;
|
|
71
|
+
if (typeof target !== "string" || !target) {
|
|
72
|
+
throw new error_1.FirebaseError(`Invalid target "${target || ""}" for lifecycle hook "${id}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw new error_1.FirebaseError(`No action (task, call, or http) specified for lifecycle hook "${id}"`);
|
|
77
|
+
}
|
|
78
|
+
bd.lifecycleHooks[id] = hook;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
44
81
|
return bd;
|
|
45
82
|
}
|
|
83
|
+
const ROLE_REGEX = /^(roles\/[a-zA-Z0-9_\.\-]+|projects\/[a-zA-Z0-9\-]+\/roles\/[a-zA-Z0-9_\.\-]+|organizations\/[0-9]+\/roles\/[a-zA-Z0-9_\.\-]+)$/;
|
|
84
|
+
function parseRequiredRoles(manifest) {
|
|
85
|
+
const roles = manifest.requiredRoles || [];
|
|
86
|
+
for (const role of roles) {
|
|
87
|
+
if (typeof role !== "string") {
|
|
88
|
+
throw new error_1.FirebaseError(`Invalid role "${JSON.stringify(role)}". Expected string`);
|
|
89
|
+
}
|
|
90
|
+
if (!ROLE_REGEX.test(role)) {
|
|
91
|
+
throw new error_1.FirebaseError(`Invalid IAM role format "${role}".`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Array.from(new Set(roles));
|
|
95
|
+
}
|
|
46
96
|
function parseRequiredAPIs(manifest) {
|
|
47
97
|
const requiredAPIs = manifest.requiredAPIs || [];
|
|
48
98
|
for (const { api, reason } of requiredAPIs) {
|
|
@@ -8,7 +8,6 @@ const path = require("path");
|
|
|
8
8
|
const portfinder = require("portfinder");
|
|
9
9
|
const semver = require("semver");
|
|
10
10
|
const spawn = require("cross-spawn");
|
|
11
|
-
const node_fetch_1 = require("node-fetch");
|
|
12
11
|
const error_1 = require("../../../../error");
|
|
13
12
|
const parseRuntimeAndValidateSDK_1 = require("./parseRuntimeAndValidateSDK");
|
|
14
13
|
const logger_1 = require("../../../../logger");
|
|
@@ -156,7 +155,7 @@ class Delegate {
|
|
|
156
155
|
childProcess.once("error", reject);
|
|
157
156
|
});
|
|
158
157
|
try {
|
|
159
|
-
await (
|
|
158
|
+
await fetch(`http://localhost:${port}/__/quitquitquit`);
|
|
160
159
|
}
|
|
161
160
|
catch (e) {
|
|
162
161
|
logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
|
|
@@ -5,7 +5,6 @@ exports.tryCreateDelegate = tryCreateDelegate;
|
|
|
5
5
|
exports.getPythonBinary = getPythonBinary;
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const path = require("path");
|
|
8
|
-
const node_fetch_1 = require("node-fetch");
|
|
9
8
|
const util_1 = require("util");
|
|
10
9
|
const portfinder = require("portfinder");
|
|
11
10
|
const discovery = require("../discovery");
|
|
@@ -130,7 +129,7 @@ class Delegate {
|
|
|
130
129
|
});
|
|
131
130
|
return Promise.resolve(async () => {
|
|
132
131
|
try {
|
|
133
|
-
await (
|
|
132
|
+
await fetch(`http://127.0.0.1:${port}/__/quitquitquit`);
|
|
134
133
|
}
|
|
135
134
|
catch (e) {
|
|
136
135
|
logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
|
|
@@ -9,6 +9,7 @@ exports.functionsDirectoryExists = functionsDirectoryExists;
|
|
|
9
9
|
exports.functionIdsAreValid = functionIdsAreValid;
|
|
10
10
|
exports.secretsAreValid = secretsAreValid;
|
|
11
11
|
exports.checkFiltersIntegrity = checkFiltersIntegrity;
|
|
12
|
+
exports.validateLifecycleHooks = validateLifecycleHooks;
|
|
12
13
|
const path = require("path");
|
|
13
14
|
const clc = require("colorette");
|
|
14
15
|
const error_1 = require("../../error");
|
|
@@ -20,6 +21,7 @@ const fsutils = require("../../fsutils");
|
|
|
20
21
|
const backend = require("./backend");
|
|
21
22
|
const utils = require("../../utils");
|
|
22
23
|
const secrets = require("../../functions/secrets");
|
|
24
|
+
const functional_1 = require("../../functional");
|
|
23
25
|
const MAX_V1_TIMEOUT_SECONDS = 540;
|
|
24
26
|
const MAX_V2_EVENTS_TIMEOUT_SECONDS = 540;
|
|
25
27
|
const MAX_V2_SCHEDULE_TIMEOUT_SECONDS = 1800;
|
|
@@ -49,7 +51,8 @@ function validateScheduledTimeout(ep) {
|
|
|
49
51
|
`The attempt deadline will be capped at ${exports.MAX_V2_SCHEDULE_ATTEMPT_DEADLINE_SECONDS} seconds.`);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
|
-
function endpointsAreValid(wantBackend) {
|
|
54
|
+
function endpointsAreValid(wantBackend, existingBackend) {
|
|
55
|
+
validateLifecycleHooks(wantBackend, existingBackend);
|
|
53
56
|
const endpoints = backend.allEndpoints(wantBackend);
|
|
54
57
|
functionIdsAreValid(endpoints);
|
|
55
58
|
validateTimeoutConfig(endpoints);
|
|
@@ -288,3 +291,43 @@ function checkFiltersIntegrity(wantBackends, filters) {
|
|
|
288
291
|
}
|
|
289
292
|
}
|
|
290
293
|
}
|
|
294
|
+
function validateLifecycleHooks(wantBackend, existingBackend) {
|
|
295
|
+
if (!wantBackend.lifecycleHooks) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const endpoints = [
|
|
299
|
+
...backend.allEndpoints(wantBackend),
|
|
300
|
+
...(existingBackend ? backend.allEndpoints(existingBackend) : []),
|
|
301
|
+
];
|
|
302
|
+
for (const [eventType, hook] of Object.entries(wantBackend.lifecycleHooks)) {
|
|
303
|
+
const keys = Object.keys(hook).filter((k) => ["task", "call", "http"].includes(k));
|
|
304
|
+
if (keys.length !== 1) {
|
|
305
|
+
throw new error_1.FirebaseError(`Lifecycle hook "${eventType}" must specify exactly one action (task, call, or http).`);
|
|
306
|
+
}
|
|
307
|
+
if ("task" in hook) {
|
|
308
|
+
const targetEndpoint = findAndValidateTargetEndpoint(endpoints, hook.task.function, eventType);
|
|
309
|
+
if (!backend.isTaskQueueTriggered(targetEndpoint)) {
|
|
310
|
+
throw new error_1.FirebaseError(`Lifecycle hook "${eventType}" expects a task queue function.`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if ("call" in hook) {
|
|
314
|
+
throw new error_1.FirebaseError(`Lifecycle hook action type "call" is not supported in the CLI yet.`);
|
|
315
|
+
}
|
|
316
|
+
else if ("http" in hook) {
|
|
317
|
+
throw new error_1.FirebaseError(`Lifecycle hook action type "http" is not supported in the CLI yet.`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
(0, functional_1.assertExhaustive)(hook);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function findAndValidateTargetEndpoint(endpoints, functionName, eventType) {
|
|
325
|
+
const targetEndpoint = endpoints.find((e) => e.id === functionName);
|
|
326
|
+
if (!targetEndpoint) {
|
|
327
|
+
throw new error_1.FirebaseError(`Target endpoint "${functionName}" not found in backend for lifecycle hook "${eventType}".`);
|
|
328
|
+
}
|
|
329
|
+
if (targetEndpoint.platform === "gcfv1") {
|
|
330
|
+
throw new error_1.FirebaseError(`Target endpoint "${functionName}" is a GCF Gen 1 function. Lifecycle hooks are only supported for GCF Gen 2 functions.`);
|
|
331
|
+
}
|
|
332
|
+
return targetEndpoint;
|
|
333
|
+
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Uploader = void 0;
|
|
4
4
|
const lodash_1 = require("lodash");
|
|
5
|
-
const abort_controller_1 = require("abort-controller");
|
|
6
5
|
const clc = require("colorette");
|
|
7
6
|
const crypto = require("crypto");
|
|
8
7
|
const fs = require("fs");
|
|
@@ -172,7 +171,7 @@ class Uploader {
|
|
|
172
171
|
if (!this.uploadClient) {
|
|
173
172
|
throw new error_1.FirebaseError("No upload client available.", { exit: 2 });
|
|
174
173
|
}
|
|
175
|
-
const controller = new
|
|
174
|
+
const controller = new AbortController();
|
|
176
175
|
const timeout = setTimeout(() => {
|
|
177
176
|
controller.abort();
|
|
178
177
|
}, this.uploadTimeout(this.hashMap[toUpload]));
|
|
@@ -190,7 +189,7 @@ class Uploader {
|
|
|
190
189
|
}
|
|
191
190
|
if (res.status !== 200) {
|
|
192
191
|
const errorMessage = await res.response.text();
|
|
193
|
-
logger_1.logger.debug(`[hosting][upload] ${this.hashMap[toUpload]} (${toUpload}) HTTP ERROR ${res.status}: headers=${JSON.stringify(res.response.headers.
|
|
192
|
+
logger_1.logger.debug(`[hosting][upload] ${this.hashMap[toUpload]} (${toUpload}) HTTP ERROR ${res.status}: headers=${JSON.stringify(Object.fromEntries(res.response.headers.entries()))} ${errorMessage}`);
|
|
194
193
|
throw new Error(`Unexpected error while uploading file: ${errorMessage}`);
|
|
195
194
|
}
|
|
196
195
|
}
|
package/lib/downloadUtils.js
CHANGED
|
@@ -10,6 +10,7 @@ const ProgressBar = require("progress");
|
|
|
10
10
|
const tmp = require("tmp");
|
|
11
11
|
const apiv2_1 = require("./apiv2");
|
|
12
12
|
const error_1 = require("./error");
|
|
13
|
+
const streamUtils_1 = require("./streamUtils");
|
|
13
14
|
async function downloadToTmp(remoteUrl, auth = false) {
|
|
14
15
|
const u = new url_1.URL(remoteUrl);
|
|
15
16
|
const c = new apiv2_1.Client({ urlPrefix: u.origin, auth });
|
|
@@ -24,7 +25,8 @@ async function downloadToTmp(remoteUrl, auth = false) {
|
|
|
24
25
|
resolveOnHTTPError: true,
|
|
25
26
|
});
|
|
26
27
|
if (res.status !== 200) {
|
|
27
|
-
|
|
28
|
+
const errorText = await (0, streamUtils_1.streamToString)(res.body);
|
|
29
|
+
throw new error_1.FirebaseError(`download failed, status ${res.status}: ${errorText}`, {
|
|
28
30
|
status: res.status,
|
|
29
31
|
});
|
|
30
32
|
}
|
|
@@ -6,8 +6,6 @@ exports.setAccountInfoImpl = setAccountInfoImpl;
|
|
|
6
6
|
exports.parseBlockingFunctionJwt = parseBlockingFunctionJwt;
|
|
7
7
|
const url_1 = require("url");
|
|
8
8
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
9
|
-
const node_fetch_1 = require("node-fetch");
|
|
10
|
-
const abort_controller_1 = require("abort-controller");
|
|
11
9
|
const utils_1 = require("./utils");
|
|
12
10
|
const errors_1 = require("./errors");
|
|
13
11
|
const types_1 = require("../types");
|
|
@@ -2183,7 +2181,7 @@ async function fetchBlockingFunction(state, event, user, options = {}, oauthToke
|
|
|
2183
2181
|
jwt,
|
|
2184
2182
|
},
|
|
2185
2183
|
};
|
|
2186
|
-
const controller = new
|
|
2184
|
+
const controller = new AbortController();
|
|
2187
2185
|
const timeout = setTimeout(() => {
|
|
2188
2186
|
controller.abort();
|
|
2189
2187
|
}, timeoutMs);
|
|
@@ -2193,11 +2191,23 @@ async function fetchBlockingFunction(state, event, user, options = {}, oauthToke
|
|
|
2193
2191
|
let text;
|
|
2194
2192
|
try {
|
|
2195
2193
|
const signal = controller.signal;
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2194
|
+
try {
|
|
2195
|
+
if (!("reason" in signal)) {
|
|
2196
|
+
signal.reason = "";
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
catch (e) {
|
|
2200
|
+
}
|
|
2201
|
+
try {
|
|
2202
|
+
if (!("throwIfAborted" in signal)) {
|
|
2203
|
+
signal.throwIfAborted = () => {
|
|
2204
|
+
throw new error_1.FirebaseError("Aborted");
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
catch (e) {
|
|
2209
|
+
}
|
|
2210
|
+
const res = await fetch(url, {
|
|
2201
2211
|
method: "POST",
|
|
2202
2212
|
headers: { "Content-Type": "application/json" },
|
|
2203
2213
|
body: JSON.stringify(reqBody),
|
|
@@ -44,46 +44,46 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"pubsub": {
|
|
47
|
-
"version": "0.8.
|
|
48
|
-
"expectedSize":
|
|
49
|
-
"expectedChecksum": "
|
|
50
|
-
"expectedChecksumSHA256": "
|
|
51
|
-
"remoteUrl": "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.8.
|
|
52
|
-
"downloadPathRelativeToCacheDir": "pubsub-emulator-0.8.
|
|
53
|
-
"binaryPathRelativeToCacheDir": "pubsub-emulator-0.8.
|
|
47
|
+
"version": "0.8.34",
|
|
48
|
+
"expectedSize": 53008019,
|
|
49
|
+
"expectedChecksum": "3eb75c292dfa72d48a36428a49bac314",
|
|
50
|
+
"expectedChecksumSHA256": "062a71d30cbc0cc29de6e55f22ae9fc7eacb02c60272a2c4692826fdb83f0c25",
|
|
51
|
+
"remoteUrl": "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.8.34.zip",
|
|
52
|
+
"downloadPathRelativeToCacheDir": "pubsub-emulator-0.8.34.zip",
|
|
53
|
+
"binaryPathRelativeToCacheDir": "pubsub-emulator-0.8.34/pubsub-emulator/bin/cloud-pubsub-emulator"
|
|
54
54
|
},
|
|
55
55
|
"dataconnect": {
|
|
56
56
|
"darwin": {
|
|
57
|
-
"version": "3.4.
|
|
58
|
-
"expectedSize":
|
|
59
|
-
"expectedChecksum": "
|
|
60
|
-
"expectedChecksumSHA256": "
|
|
61
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.4.
|
|
62
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
57
|
+
"version": "3.4.15",
|
|
58
|
+
"expectedSize": 32502016,
|
|
59
|
+
"expectedChecksum": "00f4b06ef1a6a70fbba7b2b8c1cd8eac",
|
|
60
|
+
"expectedChecksumSHA256": "8ea50a699023e5464b3306689e464716b8df7c51cadfe4a3db5d346981e1a916",
|
|
61
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.4.15",
|
|
62
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
|
|
63
63
|
},
|
|
64
64
|
"darwin_arm64": {
|
|
65
|
-
"version": "3.4.
|
|
66
|
-
"expectedSize":
|
|
67
|
-
"expectedChecksum": "
|
|
68
|
-
"expectedChecksumSHA256": "
|
|
69
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.4.
|
|
70
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
65
|
+
"version": "3.4.15",
|
|
66
|
+
"expectedSize": 30638050,
|
|
67
|
+
"expectedChecksum": "5b824b0d725e87f4f1820f8c3d50f2ce",
|
|
68
|
+
"expectedChecksumSHA256": "563ea6788e2291b82ea1cd0744f2594f0661b79277bc13e4da190b939a99293d",
|
|
69
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.4.15",
|
|
70
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
|
|
71
71
|
},
|
|
72
72
|
"win32": {
|
|
73
|
-
"version": "3.4.
|
|
74
|
-
"expectedSize":
|
|
75
|
-
"expectedChecksum": "
|
|
76
|
-
"expectedChecksumSHA256": "
|
|
77
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.4.
|
|
78
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
73
|
+
"version": "3.4.15",
|
|
74
|
+
"expectedSize": 32543232,
|
|
75
|
+
"expectedChecksum": "dbdbdca5482d386a142cd602177e811d",
|
|
76
|
+
"expectedChecksumSHA256": "01b1408750b2f1612d3a99e89702a600803f9d8f77669e5ad2502651ed560796",
|
|
77
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.4.15",
|
|
78
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15.exe"
|
|
79
79
|
},
|
|
80
80
|
"linux": {
|
|
81
|
-
"version": "3.4.
|
|
82
|
-
"expectedSize":
|
|
83
|
-
"expectedChecksum": "
|
|
84
|
-
"expectedChecksumSHA256": "
|
|
85
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.4.
|
|
86
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.
|
|
81
|
+
"version": "3.4.15",
|
|
82
|
+
"expectedSize": 31654072,
|
|
83
|
+
"expectedChecksum": "b49d21a4704ddf9420fa58d0c233821d",
|
|
84
|
+
"expectedChecksumSHA256": "b152edccdf50f3e8e3d6904d6cd1437eb88cb7a447ede3e3dd5092d12262bdc1",
|
|
85
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.4.15",
|
|
86
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.4.15"
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -320,6 +320,67 @@ function createCloudEndpoints(emulator) {
|
|
|
320
320
|
}
|
|
321
321
|
return (0, shared_1.sendFileBytes)(getObjectResponse.metadata, getObjectResponse.data, req, res);
|
|
322
322
|
});
|
|
323
|
+
gcloudStorageAPI.post("/:bucketId", (req, res, next) => {
|
|
324
|
+
adminStorageLayer.createBucket(req.params.bucketId);
|
|
325
|
+
next();
|
|
326
|
+
}, async (req, res) => {
|
|
327
|
+
const contentTypeHeader = req.header("content-type");
|
|
328
|
+
if (!contentTypeHeader?.includes("multipart/form-data")) {
|
|
329
|
+
return res.status(400).send("Content-Type must be multipart/form-data");
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const bodyBuffer = await (0, request_1.reqBodyToBuffer)(req);
|
|
333
|
+
const formData = (0, multipart_1.parseFormDataMultipartRequest)(contentTypeHeader, bodyBuffer);
|
|
334
|
+
const keyPart = formData.find((p) => p.name === "key");
|
|
335
|
+
const filePart = formData.find((p) => p.type === "file");
|
|
336
|
+
if (keyPart?.type !== "field" || filePart?.type !== "file") {
|
|
337
|
+
return res.status(400).send("Missing 'key' or file.");
|
|
338
|
+
}
|
|
339
|
+
const metadata = {
|
|
340
|
+
contentType: filePart.contentType,
|
|
341
|
+
metadata: {},
|
|
342
|
+
};
|
|
343
|
+
const HEADER_MAP = {
|
|
344
|
+
"content-type": "contentType",
|
|
345
|
+
"cache-control": "cacheControl",
|
|
346
|
+
"content-disposition": "contentDisposition",
|
|
347
|
+
"content-encoding": "contentEncoding",
|
|
348
|
+
"content-language": "contentLanguage",
|
|
349
|
+
};
|
|
350
|
+
for (const part of formData) {
|
|
351
|
+
if (part.type === "file" || part.name === "key")
|
|
352
|
+
continue;
|
|
353
|
+
const key = part.name.toLowerCase();
|
|
354
|
+
if (key.startsWith("x-goog-meta-")) {
|
|
355
|
+
metadata.metadata[key.substring(12)] = part.value;
|
|
356
|
+
}
|
|
357
|
+
else if (HEADER_MAP[key]) {
|
|
358
|
+
metadata[HEADER_MAP[key]] = part.value.trim();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const upload = uploadService.multipartUpload({
|
|
362
|
+
bucketId: req.params.bucketId,
|
|
363
|
+
objectId: keyPart.value,
|
|
364
|
+
dataRaw: filePart.data,
|
|
365
|
+
metadata: metadata,
|
|
366
|
+
authorization: req.header("authorization"),
|
|
367
|
+
});
|
|
368
|
+
await adminStorageLayer.uploadObject(upload);
|
|
369
|
+
return res.sendStatus(204);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
if (err instanceof errors_1.ForbiddenError) {
|
|
373
|
+
return res.sendStatus(403);
|
|
374
|
+
}
|
|
375
|
+
if (err instanceof errors_1.NotFoundError) {
|
|
376
|
+
return res.sendStatus(404);
|
|
377
|
+
}
|
|
378
|
+
if (err instanceof Error) {
|
|
379
|
+
return res.status(400).send(err.message);
|
|
380
|
+
}
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
323
384
|
gcloudStorageAPI.post("/b/:bucketId/o/:objectId/:method(rewriteTo|copyTo)/b/:destBucketId/o/:destObjectId", (req, res, next) => {
|
|
324
385
|
if (req.params.method === "rewriteTo" && req.query.rewriteToken) {
|
|
325
386
|
return next();
|