firebase-tools 9.23.0 → 10.0.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/lib/api.js +0 -1
- package/lib/commands/database-remove.js +2 -2
- package/lib/commands/database-set.js +2 -2
- package/lib/commands/database-update.js +2 -2
- package/lib/commands/firestore-delete.js +2 -2
- package/lib/commands/functions-delete.js +7 -1
- package/lib/commands/hosting-disable.js +3 -3
- package/lib/commands/remoteconfig-get.js +6 -5
- package/lib/deploy/functions/checkIam.js +65 -4
- package/lib/deploy/functions/containerCleaner.js +28 -38
- package/lib/deploy/functions/deploy.js +6 -7
- package/lib/deploy/functions/eventTypes.js +10 -0
- package/lib/deploy/functions/prepare.js +12 -2
- package/lib/deploy/functions/release/fabricator.js +6 -1
- package/lib/deploy/functions/release/index.js +6 -2
- package/lib/deploy/functions/release/reporter.js +4 -0
- package/lib/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.js +1 -1
- package/lib/deploy/functions/runtimes/node/parseTriggers.js +3 -9
- package/lib/deploy/functions/services/index.js +38 -0
- package/lib/deploy/functions/services/storage.js +43 -0
- package/lib/deploy/functions/triggerRegionHelper.js +6 -30
- package/lib/emulator/auth/handlers.js +1 -1
- package/lib/emulator/auth/operations.js +27 -9
- package/lib/emulator/auth/widget_ui.js +17 -3
- package/lib/emulator/downloadableEmulators.js +5 -5
- package/lib/emulator/functionsEmulator.js +15 -1
- package/lib/emulator/functionsEmulatorRuntime.js +1 -1
- package/lib/emulator/functionsEmulatorShared.js +1 -0
- package/lib/emulator/pubsubEmulator.js +58 -45
- package/lib/emulator/storage/cloudFunctions.js +13 -6
- package/lib/ensureApiEnabled.js +11 -14
- package/lib/extensions/askUserForParam.js +32 -8
- package/lib/extensions/emulator/triggerHelper.js +1 -0
- package/lib/functions/env.js +2 -2
- package/lib/gcp/cloudfunctions.js +11 -2
- package/lib/gcp/cloudfunctionsv2.js +35 -3
- package/lib/gcp/docker.js +29 -1
- package/lib/gcp/location.js +44 -0
- package/lib/gcp/storage.js +48 -32
- package/lib/init/features/functions/index.js +3 -3
- package/lib/init/features/hosting/github.js +3 -0
- package/lib/init/features/project.js +2 -1
- package/lib/previews.js +1 -1
- package/package.json +3 -3
- package/templates/init/functions/javascript/package.lint.json +1 -1
- package/templates/init/functions/javascript/package.nolint.json +1 -1
- package/templates/init/functions/typescript/package.lint.json +1 -1
- package/templates/init/functions/typescript/package.nolint.json +1 -1
- package/CHANGELOG.md +0 -1
package/lib/api.js
CHANGED
|
@@ -93,7 +93,6 @@ var api = {
|
|
|
93
93
|
functionsOrigin: utils.envOverride("FIREBASE_FUNCTIONS_URL", "https://cloudfunctions.googleapis.com"),
|
|
94
94
|
functionsV2Origin: utils.envOverride("FIREBASE_FUNCTIONS_V2_URL", "https://cloudfunctions.googleapis.com"),
|
|
95
95
|
runOrigin: utils.envOverride("CLOUD_RUN_URL", "https://run.googleapis.com"),
|
|
96
|
-
functionsUploadRegion: utils.envOverride("FIREBASE_FUNCTIONS_UPLOAD_REGION", "us-central1"),
|
|
97
96
|
functionsDefaultRegion: utils.envOverride("FIREBASE_FUNCTIONS_DEFAULT_REGION", "us-central1"),
|
|
98
97
|
cloudschedulerOrigin: utils.envOverride("FIREBASE_CLOUDSCHEDULER_URL", "https://cloudscheduler.googleapis.com"),
|
|
99
98
|
cloudTasksOrigin: utils.envOverride("FIREBASE_CLOUD_TAKS_URL", "https://cloudtasks.googleapis.com"),
|
|
@@ -14,7 +14,7 @@ const clc = require("cli-color");
|
|
|
14
14
|
const _ = require("lodash");
|
|
15
15
|
module.exports = new command_1.Command("database:remove <path>")
|
|
16
16
|
.description("remove data from your Firebase at the specified path")
|
|
17
|
-
.option("-
|
|
17
|
+
.option("-f, --force", "pass this option to bypass confirmation prompt")
|
|
18
18
|
.option("--instance <instance>", "use the database <instance>.firebaseio.com (if omitted, use default database instance)")
|
|
19
19
|
.before(requirePermissions_1.requirePermissions, ["firebasedatabase.instances.update"])
|
|
20
20
|
.before(requireDatabaseInstance_1.requireDatabaseInstance)
|
|
@@ -28,7 +28,7 @@ module.exports = new command_1.Command("database:remove <path>")
|
|
|
28
28
|
const databaseUrl = utils.getDatabaseUrl(origin, options.instance, path);
|
|
29
29
|
const confirm = await prompt_1.promptOnce({
|
|
30
30
|
type: "confirm",
|
|
31
|
-
name: "
|
|
31
|
+
name: "force",
|
|
32
32
|
default: false,
|
|
33
33
|
message: "You are about to remove all data at " + clc.cyan(databaseUrl) + ". Are you sure?",
|
|
34
34
|
}, options);
|
|
@@ -19,7 +19,7 @@ const utils = require("../utils");
|
|
|
19
19
|
exports.default = new command_1.Command("database:set <path> [infile]")
|
|
20
20
|
.description("store JSON data at the specified path via STDIN, arg, or file")
|
|
21
21
|
.option("-d, --data <data>", "specify escaped JSON directly")
|
|
22
|
-
.option("-
|
|
22
|
+
.option("-f, --force", "pass this option to bypass confirmation prompt")
|
|
23
23
|
.option("--instance <instance>", "use the database <instance>.firebaseio.com (if omitted, use default database instance)")
|
|
24
24
|
.before(requirePermissions_1.requirePermissions, ["firebasedatabase.instances.update"])
|
|
25
25
|
.before(requireDatabaseInstance_1.requireDatabaseInstance)
|
|
@@ -34,7 +34,7 @@ exports.default = new command_1.Command("database:set <path> [infile]")
|
|
|
34
34
|
const dbJsonURL = new url_1.URL(utils.getDatabaseUrl(origin, options.instance, path + ".json"));
|
|
35
35
|
const confirm = await prompt_1.promptOnce({
|
|
36
36
|
type: "confirm",
|
|
37
|
-
name: "
|
|
37
|
+
name: "force",
|
|
38
38
|
default: false,
|
|
39
39
|
message: "You are about to overwrite all data at " + clc.cyan(dbPath) + ". Are you sure?",
|
|
40
40
|
}, options);
|
|
@@ -18,7 +18,7 @@ const utils = require("../utils");
|
|
|
18
18
|
exports.default = new command_1.Command("database:update <path> [infile]")
|
|
19
19
|
.description("update some of the keys for the defined path in your Firebase")
|
|
20
20
|
.option("-d, --data <data>", "specify escaped JSON directly")
|
|
21
|
-
.option("-
|
|
21
|
+
.option("-f, --force", "pass this option to bypass confirmation prompt")
|
|
22
22
|
.option("--instance <instance>", "use the database <instance>.firebaseio.com (if omitted, use default database instance)")
|
|
23
23
|
.before(requirePermissions_1.requirePermissions, ["firebasedatabase.instances.update"])
|
|
24
24
|
.before(requireDatabaseInstance_1.requireDatabaseInstance)
|
|
@@ -32,7 +32,7 @@ exports.default = new command_1.Command("database:update <path> [infile]")
|
|
|
32
32
|
const url = utils.getDatabaseUrl(origin, options.instance, path);
|
|
33
33
|
const confirmed = await prompt_1.promptOnce({
|
|
34
34
|
type: "confirm",
|
|
35
|
-
name: "
|
|
35
|
+
name: "force",
|
|
36
36
|
default: false,
|
|
37
37
|
message: `You are about to modify data at ${clc.cyan(url)}. Are you sure?`,
|
|
38
38
|
}, options);
|
|
@@ -54,7 +54,7 @@ module.exports = new command_1.Command("firestore:delete [path]")
|
|
|
54
54
|
"subcollections. May not be passed along with -r.")
|
|
55
55
|
.option("--all-collections", "Delete all. Deletes the entire Firestore database, " +
|
|
56
56
|
"including all collections and documents. Any other flags or arguments will be ignored.")
|
|
57
|
-
.option("-
|
|
57
|
+
.option("-f, --force", "No confirmation. Otherwise, a confirmation prompt will appear.")
|
|
58
58
|
.before(commandUtils_1.printNoticeIfEmulated, types_1.Emulators.FIRESTORE)
|
|
59
59
|
.before(requirePermissions_1.requirePermissions, ["datastore.entities.list", "datastore.entities.delete"])
|
|
60
60
|
.action(async (path, options) => {
|
|
@@ -68,7 +68,7 @@ module.exports = new command_1.Command("firestore:delete [path]")
|
|
|
68
68
|
});
|
|
69
69
|
const confirm = await prompt_1.promptOnce({
|
|
70
70
|
type: "confirm",
|
|
71
|
-
name: "
|
|
71
|
+
name: "force",
|
|
72
72
|
default: false,
|
|
73
73
|
message: getConfirmationMessage(deleteOp, options),
|
|
74
74
|
}, options);
|
|
@@ -8,6 +8,7 @@ const projectUtils_1 = require("../projectUtils");
|
|
|
8
8
|
const prompt_1 = require("../prompt");
|
|
9
9
|
const functional_1 = require("../functional");
|
|
10
10
|
const requirePermissions_1 = require("../requirePermissions");
|
|
11
|
+
const ensure = require("../ensureApiEnabled");
|
|
11
12
|
const helper = require("../deploy/functions/functionsDeployHelper");
|
|
12
13
|
const utils = require("../utils");
|
|
13
14
|
const backend = require("../deploy/functions/backend");
|
|
@@ -84,5 +85,10 @@ exports.default = new command_1.Command("functions:delete [filters...]")
|
|
|
84
85
|
exit: 1,
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
const opts = {};
|
|
89
|
+
const arEnabled = await ensure.check(projectUtils_1.needProjectId(options), "artifactregistry.googleapis.com", "functions", true);
|
|
90
|
+
if (!arEnabled) {
|
|
91
|
+
opts.ar = new containerCleaner.NoopArtifactRegistryCleaner();
|
|
92
|
+
}
|
|
93
|
+
await containerCleaner.cleanupBuildImages([], allEpToDelete, opts);
|
|
88
94
|
});
|
|
@@ -10,7 +10,7 @@ const requirePermissions_1 = require("../requirePermissions");
|
|
|
10
10
|
const utils = require("../utils");
|
|
11
11
|
exports.default = new command_1.Command("hosting:disable")
|
|
12
12
|
.description("stop serving web traffic to your Firebase Hosting site")
|
|
13
|
-
.option("-
|
|
13
|
+
.option("-f, --force", "skip confirmation")
|
|
14
14
|
.option("-s, --site <siteName>", "the site to disable")
|
|
15
15
|
.before(requirePermissions_1.requirePermissions, ["firebasehosting.sites.update"])
|
|
16
16
|
.before(requireHostingSite_1.requireHostingSite)
|
|
@@ -18,9 +18,9 @@ exports.default = new command_1.Command("hosting:disable")
|
|
|
18
18
|
const siteToDisable = options.site;
|
|
19
19
|
const confirm = await prompt_1.promptOnce({
|
|
20
20
|
type: "confirm",
|
|
21
|
-
name: "
|
|
21
|
+
name: "force",
|
|
22
22
|
message: `Are you sure you want to disable Firebase Hosting for the site ${clc.underline(siteToDisable)}\n${clc.underline("This will immediately make your site inaccessible!")}`,
|
|
23
|
-
});
|
|
23
|
+
}, options);
|
|
24
24
|
if (!confirm) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
@@ -11,13 +11,14 @@ const utils = require("../utils");
|
|
|
11
11
|
const Table = require("cli-table");
|
|
12
12
|
const fs = require("fs");
|
|
13
13
|
const util = require("util");
|
|
14
|
+
const error_1 = require("../error");
|
|
14
15
|
const tableHead = ["Entry Name", "Value"];
|
|
15
16
|
const MAX_DISPLAY_ITEMS = 20;
|
|
16
|
-
function
|
|
17
|
-
if (typeof Number(versionNumber) == "number") {
|
|
17
|
+
function checkValidOptionalNumber(versionNumber) {
|
|
18
|
+
if (!versionNumber || typeof Number(versionNumber) == "number") {
|
|
18
19
|
return versionNumber;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
throw new error_1.FirebaseError(`Could not interpret "${versionNumber}" as a valid number.`);
|
|
21
22
|
}
|
|
22
23
|
module.exports = new command_1.Command("remoteconfig:get")
|
|
23
24
|
.description("get a Firebase project's Remote Config template")
|
|
@@ -26,8 +27,8 @@ module.exports = new command_1.Command("remoteconfig:get")
|
|
|
26
27
|
.before(requireAuth_1.requireAuth)
|
|
27
28
|
.before(requirePermissions_1.requirePermissions, ["cloudconfig.configs.get"])
|
|
28
29
|
.action(async (options) => {
|
|
29
|
-
utils.
|
|
30
|
-
const template = await rcGet.getTemplate(projectUtils_1.needProjectId(options),
|
|
30
|
+
utils.assertIsStringOrUndefined(options.versionNumber);
|
|
31
|
+
const template = await rcGet.getTemplate(projectUtils_1.needProjectId(options), checkValidOptionalNumber(options.versionNumber));
|
|
31
32
|
const table = new Table({ head: tableHead, style: { head: ["green"] } });
|
|
32
33
|
if (template.conditions) {
|
|
33
34
|
let updatedConditions = template.conditions
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.checkHttpIam = exports.checkServiceAccountIam = void 0;
|
|
3
|
+
exports.ensureServiceAgentRoles = exports.mergeBindings = exports.checkHttpIam = exports.checkServiceAccountIam = void 0;
|
|
4
4
|
const cli_color_1 = require("cli-color");
|
|
5
5
|
const logger_1 = require("../../logger");
|
|
6
6
|
const functionsDeployHelper_1 = require("./functionsDeployHelper");
|
|
7
7
|
const error_1 = require("../../error");
|
|
8
|
-
const
|
|
8
|
+
const iam = require("../../gcp/iam");
|
|
9
9
|
const backend = require("./backend");
|
|
10
10
|
const track = require("../../track");
|
|
11
|
+
const utils = require("../../utils");
|
|
12
|
+
const resourceManager_1 = require("../../gcp/resourceManager");
|
|
13
|
+
const services_1 = require("./services");
|
|
11
14
|
const PERMISSION = "cloudfunctions.functions.setIamPolicy";
|
|
12
15
|
async function checkServiceAccountIam(projectId) {
|
|
13
16
|
const saEmail = `${projectId}@appspot.gserviceaccount.com`;
|
|
14
17
|
let passed = false;
|
|
15
18
|
try {
|
|
16
|
-
const iamResult = await
|
|
19
|
+
const iamResult = await iam.testResourceIamPermissions("https://iam.googleapis.com", "v1", `projects/${projectId}/serviceAccounts/${saEmail}`, ["iam.serviceAccounts.actAs"]);
|
|
17
20
|
passed = iamResult.passed;
|
|
18
21
|
}
|
|
19
22
|
catch (err) {
|
|
@@ -41,7 +44,7 @@ async function checkHttpIam(context, options, payload) {
|
|
|
41
44
|
logger_1.logger.debug("[functions] found", newHttpsEndpoints.length, "new HTTP functions, testing setIamPolicy permission...");
|
|
42
45
|
let passed = true;
|
|
43
46
|
try {
|
|
44
|
-
const iamResult = await
|
|
47
|
+
const iamResult = await iam.testIamPermissions(context.projectId, [PERMISSION]);
|
|
45
48
|
passed = iamResult.passed;
|
|
46
49
|
}
|
|
47
50
|
catch (e) {
|
|
@@ -57,3 +60,61 @@ async function checkHttpIam(context, options, payload) {
|
|
|
57
60
|
logger_1.logger.debug("[functions] found setIamPolicy permission, proceeding with deploy");
|
|
58
61
|
}
|
|
59
62
|
exports.checkHttpIam = checkHttpIam;
|
|
63
|
+
function reduceEventsToServices(services, endpoint) {
|
|
64
|
+
const service = services_1.serviceForEndpoint(endpoint);
|
|
65
|
+
if (service.requiredProjectBindings && !services.find((s) => s.name === service.name)) {
|
|
66
|
+
services.push(service);
|
|
67
|
+
}
|
|
68
|
+
return services;
|
|
69
|
+
}
|
|
70
|
+
function mergeBindings(policy, allRequiredBindings) {
|
|
71
|
+
for (const requiredBindings of allRequiredBindings) {
|
|
72
|
+
if (requiredBindings.length === 0) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const requiredBinding of requiredBindings) {
|
|
76
|
+
const ndx = policy.bindings.findIndex((policyBinding) => policyBinding.role === requiredBinding.role);
|
|
77
|
+
if (ndx === -1) {
|
|
78
|
+
policy.bindings.push(requiredBinding);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
requiredBinding.members.forEach((updatedMember) => {
|
|
82
|
+
if (!policy.bindings[ndx].members.find((member) => member === updatedMember)) {
|
|
83
|
+
policy.bindings[ndx].members.push(updatedMember);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.mergeBindings = mergeBindings;
|
|
90
|
+
async function ensureServiceAgentRoles(projectId, want, have) {
|
|
91
|
+
const wantServices = backend.allEndpoints(want).reduce(reduceEventsToServices, []);
|
|
92
|
+
const haveServices = backend.allEndpoints(have).reduce(reduceEventsToServices, []);
|
|
93
|
+
const newServices = wantServices.filter((wantS) => !haveServices.find((haveS) => wantS.name === haveS.name));
|
|
94
|
+
if (newServices.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let policy;
|
|
98
|
+
try {
|
|
99
|
+
policy = await resourceManager_1.getIamPolicy(projectId);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
utils.logLabeledBullet("functions", "Could not verify the necessary IAM configuration for the following newly-integrated services: " +
|
|
103
|
+
`${newServices.map((service) => service.api).join(", ")}` +
|
|
104
|
+
". Deployment may fail.", "warn");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const findRequiredBindings = [];
|
|
108
|
+
newServices.forEach((service) => findRequiredBindings.push(service.requiredProjectBindings(projectId, policy)));
|
|
109
|
+
const allRequiredBindings = await Promise.all(findRequiredBindings);
|
|
110
|
+
mergeBindings(policy, allRequiredBindings);
|
|
111
|
+
try {
|
|
112
|
+
await resourceManager_1.setIamPolicy(projectId, policy, "bindings");
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw new error_1.FirebaseError("We failed to modify the IAM policy for the project. The functions " +
|
|
116
|
+
"deployment requires specific roles to be granted to service agents," +
|
|
117
|
+
" otherwise the deployment will fail.", { original: err });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.ensureServiceAgentRoles = ensureServiceAgentRoles;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DockerHelper = exports.deleteGcfArtifacts = exports.listGcfPaths = exports.ContainerRegistryCleaner = exports.
|
|
3
|
+
exports.DockerHelper = exports.deleteGcfArtifacts = exports.listGcfPaths = exports.ContainerRegistryCleaner = exports.NoopArtifactRegistryCleaner = exports.ArtifactRegistryCleaner = exports.cleanupBuildImages = void 0;
|
|
4
4
|
const clc = require("cli-color");
|
|
5
5
|
const error_1 = require("../../error");
|
|
6
6
|
const api_1 = require("../../api");
|
|
@@ -10,31 +10,6 @@ const backend = require("./backend");
|
|
|
10
10
|
const docker = require("../../gcp/docker");
|
|
11
11
|
const utils = require("../../utils");
|
|
12
12
|
const poller = require("../../operation-poller");
|
|
13
|
-
exports.SUBDOMAIN_MAPPING = {
|
|
14
|
-
"us-west2": "us",
|
|
15
|
-
"us-west3": "us",
|
|
16
|
-
"us-west4": "us",
|
|
17
|
-
"us-central1": "us",
|
|
18
|
-
"us-central2": "us",
|
|
19
|
-
"us-east1": "us",
|
|
20
|
-
"us-east4": "us",
|
|
21
|
-
"northamerica-northeast1": "us",
|
|
22
|
-
"southamerica-east1": "us",
|
|
23
|
-
"europe-west1": "eu",
|
|
24
|
-
"europe-west2": "eu",
|
|
25
|
-
"europe-west3": "eu",
|
|
26
|
-
"europe-west5": "eu",
|
|
27
|
-
"europe-west6": "eu",
|
|
28
|
-
"europe-central2": "eu",
|
|
29
|
-
"asia-east1": "asia",
|
|
30
|
-
"asia-east2": "asia",
|
|
31
|
-
"asia-northeast1": "asia",
|
|
32
|
-
"asia-northeast2": "asia",
|
|
33
|
-
"asia-northeast3": "asia",
|
|
34
|
-
"asia-south1": "asia",
|
|
35
|
-
"asia-southeast2": "asia",
|
|
36
|
-
"australia-southeast1": "asia",
|
|
37
|
-
};
|
|
38
13
|
async function retry(func) {
|
|
39
14
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
15
|
const MAX_RETRIES = 3;
|
|
@@ -87,7 +62,7 @@ async function cleanupBuildImages(haveFunctions, deletedFunctions, cleaners = {}
|
|
|
87
62
|
await gcrCleaner.cleanupFunction(func);
|
|
88
63
|
}
|
|
89
64
|
catch (err) {
|
|
90
|
-
const path = `${func.project}/${
|
|
65
|
+
const path = `${func.project}/${docker.GCR_SUBDOMAIN_MAPPING[func.region]}/gcf`;
|
|
91
66
|
failedDomains.add(`https://console.cloud.google.com/gcr/images/${path}`);
|
|
92
67
|
}
|
|
93
68
|
}));
|
|
@@ -108,7 +83,12 @@ async function cleanupBuildImages(haveFunctions, deletedFunctions, cleaners = {}
|
|
|
108
83
|
exports.cleanupBuildImages = cleanupBuildImages;
|
|
109
84
|
class ArtifactRegistryCleaner {
|
|
110
85
|
static packagePath(func) {
|
|
111
|
-
|
|
86
|
+
const encodedId = func.id
|
|
87
|
+
.replace(/_/g, "__")
|
|
88
|
+
.replace(/-/g, "--")
|
|
89
|
+
.replace(/^[A-Z]/, (first) => `${first.toLowerCase()}-${first.toLowerCase()}`)
|
|
90
|
+
.replace(/[A-Z]/g, (upper) => `_${upper.toLowerCase()}`);
|
|
91
|
+
return `projects/${func.project}/locations/${func.region}/repositories/gcf-artifacts/packages/${encodedId}`;
|
|
112
92
|
}
|
|
113
93
|
async cleanupFunction(func) {
|
|
114
94
|
let op;
|
|
@@ -140,12 +120,21 @@ ArtifactRegistryCleaner.POLLER_OPTIONS = {
|
|
|
140
120
|
apiVersion: artifactregistry.API_VERSION,
|
|
141
121
|
masterTimeout: 5 * 60 * 1000,
|
|
142
122
|
};
|
|
123
|
+
class NoopArtifactRegistryCleaner extends ArtifactRegistryCleaner {
|
|
124
|
+
cleanupFunction() {
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}
|
|
127
|
+
cleanupFunctionCache() {
|
|
128
|
+
return Promise.resolve();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.NoopArtifactRegistryCleaner = NoopArtifactRegistryCleaner;
|
|
143
132
|
class ContainerRegistryCleaner {
|
|
144
133
|
constructor() {
|
|
145
134
|
this.helpers = {};
|
|
146
135
|
}
|
|
147
136
|
helper(location) {
|
|
148
|
-
const subdomain =
|
|
137
|
+
const subdomain = docker.GCR_SUBDOMAIN_MAPPING[location] || "us";
|
|
149
138
|
if (!this.helpers[subdomain]) {
|
|
150
139
|
const origin = `https://${subdomain}.${api_1.containerRegistryDomain}`;
|
|
151
140
|
this.helpers[subdomain] = new DockerHelper(origin);
|
|
@@ -185,14 +174,14 @@ function getHelper(cache, subdomain) {
|
|
|
185
174
|
}
|
|
186
175
|
async function listGcfPaths(projectId, locations, dockerHelpers = {}) {
|
|
187
176
|
if (!locations) {
|
|
188
|
-
locations = Object.keys(
|
|
177
|
+
locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
|
|
189
178
|
}
|
|
190
|
-
const invalidRegion = locations.find((loc) => !
|
|
179
|
+
const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
|
|
191
180
|
if (invalidRegion) {
|
|
192
181
|
throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
|
|
193
182
|
}
|
|
194
183
|
const locationsSet = new Set(locations);
|
|
195
|
-
const subdomains = new Set(Object.values(
|
|
184
|
+
const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
|
|
196
185
|
const failedSubdomains = [];
|
|
197
186
|
const listAll = [];
|
|
198
187
|
for (const subdomain of subdomains) {
|
|
@@ -223,26 +212,27 @@ async function listGcfPaths(projectId, locations, dockerHelpers = {}) {
|
|
|
223
212
|
throw new error_1.FirebaseError(`Failed to search the following subdomains: ${failedSubdomains.join(",")}`);
|
|
224
213
|
}
|
|
225
214
|
return gcfDirs.map((loc) => {
|
|
226
|
-
return `${
|
|
215
|
+
return `${docker.GCR_SUBDOMAIN_MAPPING[loc]}.${api_1.containerRegistryDomain}/${projectId}/gcf/${loc}`;
|
|
227
216
|
});
|
|
228
217
|
}
|
|
229
218
|
exports.listGcfPaths = listGcfPaths;
|
|
230
219
|
async function deleteGcfArtifacts(projectId, locations, dockerHelpers = {}) {
|
|
231
220
|
if (!locations) {
|
|
232
|
-
locations = Object.keys(
|
|
221
|
+
locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
|
|
233
222
|
}
|
|
234
|
-
const invalidRegion = locations.find((loc) => !
|
|
223
|
+
const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
|
|
235
224
|
if (invalidRegion) {
|
|
236
225
|
throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
|
|
237
226
|
}
|
|
238
|
-
const subdomains = new Set(Object.values(
|
|
227
|
+
const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
|
|
239
228
|
const failedSubdomains = [];
|
|
240
229
|
const deleteLocations = locations.map((loc) => {
|
|
230
|
+
const subdomain = docker.GCR_SUBDOMAIN_MAPPING[loc];
|
|
241
231
|
try {
|
|
242
|
-
return getHelper(dockerHelpers,
|
|
232
|
+
return getHelper(dockerHelpers, subdomain).rm(`${projectId}/gcf/${loc}`);
|
|
243
233
|
}
|
|
244
234
|
catch (err) {
|
|
245
|
-
failedSubdomains.push(
|
|
235
|
+
failedSubdomains.push(subdomain);
|
|
246
236
|
logger_1.logger.debug(err);
|
|
247
237
|
}
|
|
248
238
|
});
|
|
@@ -5,18 +5,16 @@ const tmp_1 = require("tmp");
|
|
|
5
5
|
const clc = require("cli-color");
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const checkIam_1 = require("./checkIam");
|
|
8
|
-
const api_1 = require("../../api");
|
|
9
8
|
const utils_1 = require("../../utils");
|
|
10
9
|
const gcs = require("../../gcp/storage");
|
|
11
10
|
const gcf = require("../../gcp/cloudfunctions");
|
|
12
11
|
const gcfv2 = require("../../gcp/cloudfunctionsv2");
|
|
13
12
|
const utils = require("../../utils");
|
|
14
13
|
const backend = require("./backend");
|
|
15
|
-
const GCP_REGION = api_1.functionsUploadRegion;
|
|
16
14
|
tmp_1.setGracefulCleanup();
|
|
17
|
-
async function uploadSourceV1(context) {
|
|
18
|
-
const uploadUrl = await gcf.generateUploadUrl(context.projectId,
|
|
19
|
-
context.
|
|
15
|
+
async function uploadSourceV1(context, region) {
|
|
16
|
+
const uploadUrl = await gcf.generateUploadUrl(context.projectId, region);
|
|
17
|
+
context.sourceUrl = uploadUrl;
|
|
20
18
|
const uploadOpts = {
|
|
21
19
|
file: context.functionsSourceV1,
|
|
22
20
|
stream: fs.createReadStream(context.functionsSourceV1),
|
|
@@ -45,8 +43,9 @@ async function deploy(context, options, payload) {
|
|
|
45
43
|
try {
|
|
46
44
|
const want = payload.functions.backend;
|
|
47
45
|
const uploads = [];
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const v1Endpoints = backend.allEndpoints(want).filter((e) => e.platform === "gcfv1");
|
|
47
|
+
if (v1Endpoints.length > 0) {
|
|
48
|
+
uploads.push(uploadSourceV1(context, v1Endpoints[0].region));
|
|
50
49
|
}
|
|
51
50
|
for (const region of Object.keys(want.endpoints)) {
|
|
52
51
|
if (backend.regionalEndpoints(want, region).some((e) => e.platform === "gcfv2")) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PUBSUB_V2_EVENT = exports.STORAGE_V2_EVENTS = void 0;
|
|
4
|
+
exports.STORAGE_V2_EVENTS = [
|
|
5
|
+
"google.cloud.storage.object.v1.finalized",
|
|
6
|
+
"google.cloud.storage.object.v1.archived",
|
|
7
|
+
"google.cloud.storage.object.v1.deleted",
|
|
8
|
+
"google.cloud.storage.object.v1.metadataUpdated",
|
|
9
|
+
];
|
|
10
|
+
exports.PUBSUB_V2_EVENT = "google.cloud.pubsub.topic.v1.messagePublished";
|
|
@@ -19,12 +19,20 @@ const validate = require("./validate");
|
|
|
19
19
|
const utils = require("../../utils");
|
|
20
20
|
const logger_1 = require("../../logger");
|
|
21
21
|
const triggerRegionHelper_1 = require("./triggerRegionHelper");
|
|
22
|
+
const checkIam_1 = require("./checkIam");
|
|
22
23
|
function hasUserConfig(config) {
|
|
23
24
|
return Object.keys(config).length > 1;
|
|
24
25
|
}
|
|
25
26
|
function hasDotenv(opts) {
|
|
26
27
|
return previews_1.previews.dotenv && functionsEnv.hasUserEnvs(opts);
|
|
27
28
|
}
|
|
29
|
+
async function maybeEnableAR(projectId) {
|
|
30
|
+
if (previews_1.previews.artifactregistry) {
|
|
31
|
+
return ensureApiEnabled.check(projectId, "artifactregistry.googleapis.com", "functions", true);
|
|
32
|
+
}
|
|
33
|
+
await ensureApiEnabled.ensure(projectId, "artifactregistry.googleapis.com", "functions");
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
28
36
|
async function prepare(context, options, payload) {
|
|
29
37
|
if (!options.config.src.functions) {
|
|
30
38
|
return;
|
|
@@ -39,9 +47,10 @@ async function prepare(context, options, payload) {
|
|
|
39
47
|
ensureApiEnabled.ensure(projectId, "cloudfunctions.googleapis.com", "functions"),
|
|
40
48
|
ensureApiEnabled.check(projectId, "runtimeconfig.googleapis.com", "runtimeconfig", true),
|
|
41
49
|
ensureCloudBuildEnabled_1.ensureCloudBuildEnabled(projectId),
|
|
42
|
-
|
|
50
|
+
maybeEnableAR(projectId),
|
|
43
51
|
]);
|
|
44
52
|
context.runtimeConfigEnabled = checkAPIsEnabled[1];
|
|
53
|
+
context.artifactRegistryEnabled = checkAPIsEnabled[3];
|
|
45
54
|
const firebaseConfig = await functionsConfig.getFirebaseConfig(options);
|
|
46
55
|
context.firebaseConfig = firebaseConfig;
|
|
47
56
|
const runtimeConfig = await prepareFunctionsUpload_1.getFunctionsConfig(context);
|
|
@@ -104,8 +113,9 @@ async function prepare(context, options, payload) {
|
|
|
104
113
|
return functionsDeployHelper_1.functionMatchesAnyGroup(endpoint, context.filters);
|
|
105
114
|
});
|
|
106
115
|
const haveBackend = await backend.existingBackend(context);
|
|
116
|
+
await checkIam_1.ensureServiceAgentRoles(projectId, wantBackend, haveBackend);
|
|
107
117
|
inferDetailsFromExisting(wantBackend, haveBackend, usedDotenv);
|
|
108
|
-
await triggerRegionHelper_1.
|
|
118
|
+
await triggerRegionHelper_1.ensureTriggerRegions(wantBackend);
|
|
109
119
|
await prompts_1.promptForFailurePolicies(options, matchingBackend, haveBackend);
|
|
110
120
|
await prompts_1.promptForMinInstances(options, matchingBackend, haveBackend);
|
|
111
121
|
await backend.checkAvailability(context, wantBackend);
|
|
@@ -124,7 +124,9 @@ class Fabricator {
|
|
|
124
124
|
await this.setTrigger(endpoint);
|
|
125
125
|
}
|
|
126
126
|
async updateEndpoint(update, scraper) {
|
|
127
|
-
update.
|
|
127
|
+
if (update.deleteAndRecreate || update.endpoint.platform !== "gcfv2") {
|
|
128
|
+
update.endpoint.labels = Object.assign(Object.assign({}, update.endpoint.labels), deploymentTool.labels());
|
|
129
|
+
}
|
|
128
130
|
if (update.deleteAndRecreate) {
|
|
129
131
|
await this.deleteEndpoint(update.deleteAndRecreate);
|
|
130
132
|
await this.createEndpoint(update.endpoint, scraper);
|
|
@@ -157,6 +159,9 @@ class Fabricator {
|
|
|
157
159
|
throw new Error("Precondition failed");
|
|
158
160
|
}
|
|
159
161
|
const apiFunction = gcf.functionFromEndpoint(endpoint, this.sourceUrl);
|
|
162
|
+
if (apiFunction.httpsTrigger) {
|
|
163
|
+
apiFunction.httpsTrigger.securityLevel = "SECURE_ALWAYS";
|
|
164
|
+
}
|
|
160
165
|
apiFunction.sourceToken = await scraper.tokenPromise();
|
|
161
166
|
const resultFunction = await this.functionExecutor
|
|
162
167
|
.run(async () => {
|
|
@@ -37,7 +37,7 @@ async function release(context, options, payload) {
|
|
|
37
37
|
const fab = new fabricator.Fabricator({
|
|
38
38
|
functionExecutor,
|
|
39
39
|
executor: new executor.QueueExecutor({}),
|
|
40
|
-
sourceUrl: context.
|
|
40
|
+
sourceUrl: context.sourceUrl,
|
|
41
41
|
storage: context.storage,
|
|
42
42
|
appEngineLocation: functionsConfig_1.getAppEngineLocation(context.firebaseConfig),
|
|
43
43
|
});
|
|
@@ -49,7 +49,11 @@ async function release(context, options, payload) {
|
|
|
49
49
|
const deletedEndpoints = Object.values(plan)
|
|
50
50
|
.map((r) => r.endpointsToDelete)
|
|
51
51
|
.reduce(functional_1.reduceFlat, []);
|
|
52
|
-
|
|
52
|
+
const opts = {};
|
|
53
|
+
if (!context.artifactRegistryEnabled) {
|
|
54
|
+
opts.ar = new containerCleaner.NoopArtifactRegistryCleaner();
|
|
55
|
+
}
|
|
56
|
+
await containerCleaner.cleanupBuildImages(haveEndpoints, deletedEndpoints, opts);
|
|
53
57
|
const allErrors = summary.results.filter((r) => r.error).map((r) => r.error);
|
|
54
58
|
if (allErrors.length) {
|
|
55
59
|
const opts = allErrors.length == 1 ? { original: allErrors[0] } : { children: allErrors };
|
|
@@ -29,8 +29,10 @@ async function logAndTrackDeployStats(summary) {
|
|
|
29
29
|
let totalSuccesses = 0;
|
|
30
30
|
let totalAborts = 0;
|
|
31
31
|
const reports = [];
|
|
32
|
+
const regions = new Set();
|
|
32
33
|
for (const result of summary.results) {
|
|
33
34
|
const tag = triggerTag(result.endpoint);
|
|
35
|
+
regions.add(result.endpoint.region);
|
|
34
36
|
totalTime += result.durationMs;
|
|
35
37
|
if (!result.error) {
|
|
36
38
|
totalSuccesses++;
|
|
@@ -45,6 +47,8 @@ async function logAndTrackDeployStats(summary) {
|
|
|
45
47
|
reports.push(track.track("function_deploy_failure", tag, result.durationMs));
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
const regionCountTag = regions.size < 5 ? regions.size.toString() : ">=5";
|
|
51
|
+
reports.push(track.track("functions_region_count", regionCountTag, 1));
|
|
48
52
|
const gcfv1 = summary.results.find((r) => r.endpoint.platform === "gcfv1");
|
|
49
53
|
const gcfv2 = summary.results.find((r) => r.endpoint.platform === "gcfv2");
|
|
50
54
|
const tag = gcfv1 && gcfv2 ? "v1+v2" : gcfv1 ? "v1" : "v2";
|
|
@@ -18,7 +18,7 @@ const ENGINE_RUNTIMES = {
|
|
|
18
18
|
const ENGINE_RUNTIMES_NAMES = Object.values(ENGINE_RUNTIMES);
|
|
19
19
|
exports.RUNTIME_NOT_SET = "`runtime` field is required but was not found in firebase.json.\n" +
|
|
20
20
|
"To fix this, add the following lines to the `functions` section of your firebase.json:\n" +
|
|
21
|
-
'"runtime": "
|
|
21
|
+
'"runtime": "nodejs16"\n';
|
|
22
22
|
exports.UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG = clc.bold(`functions.runtime value is unsupported. ` +
|
|
23
23
|
`Valid choices are: ${clc.bold("nodejs{10|12|14|16}")}.`);
|
|
24
24
|
exports.UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG = clc.bold(`package.json in functions directory has an engines field which is unsupported. ` +
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addResourcesToBackend = exports.discoverBackend = exports.useStrategy =
|
|
3
|
+
exports.addResourcesToBackend = exports.discoverBackend = exports.useStrategy = void 0;
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const _ = require("lodash");
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
@@ -9,13 +9,8 @@ const logger_1 = require("../../../../logger");
|
|
|
9
9
|
const backend = require("../../backend");
|
|
10
10
|
const api = require("../../../../api");
|
|
11
11
|
const proto = require("../../../../gcp/proto");
|
|
12
|
+
const eventTypes_1 = require("../../eventTypes");
|
|
12
13
|
const TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js");
|
|
13
|
-
exports.GCS_EVENTS = new Set([
|
|
14
|
-
"google.cloud.storage.object.v1.finalized",
|
|
15
|
-
"google.cloud.storage.object.v1.archived",
|
|
16
|
-
"google.cloud.storage.object.v1.deleted",
|
|
17
|
-
"google.cloud.storage.object.v1.metadataUpdated",
|
|
18
|
-
]);
|
|
19
14
|
function removeInspectOptions(options) {
|
|
20
15
|
return options.filter((opt) => !opt.startsWith("--inspect"));
|
|
21
16
|
}
|
|
@@ -64,7 +59,6 @@ async function discoverBackend(projectId, sourceDir, runtime, configValues, envs
|
|
|
64
59
|
}
|
|
65
60
|
exports.discoverBackend = discoverBackend;
|
|
66
61
|
function addResourcesToBackend(projectId, runtime, annotation, want) {
|
|
67
|
-
var _a;
|
|
68
62
|
Object.freeze(annotation);
|
|
69
63
|
for (const region of annotation.regions || [api.functionsDefaultRegion]) {
|
|
70
64
|
let triggered;
|
|
@@ -99,7 +93,7 @@ function addResourcesToBackend(projectId, runtime, annotation, want) {
|
|
|
99
93
|
retry: !!annotation.failurePolicy,
|
|
100
94
|
},
|
|
101
95
|
};
|
|
102
|
-
if (
|
|
96
|
+
if (eventTypes_1.STORAGE_V2_EVENTS.find((event) => { var _a; return event === (((_a = annotation.eventTrigger) === null || _a === void 0 ? void 0 : _a.eventType) || ""); })) {
|
|
103
97
|
triggered.eventTrigger.eventFilters = {
|
|
104
98
|
bucket: annotation.eventTrigger.resource,
|
|
105
99
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serviceForEndpoint = exports.EVENT_SERVICE_MAPPING = exports.StorageService = exports.PubSubService = exports.NoOpService = void 0;
|
|
4
|
+
const backend = require("../backend");
|
|
5
|
+
const storage_1 = require("./storage");
|
|
6
|
+
const noop = () => Promise.resolve();
|
|
7
|
+
exports.NoOpService = {
|
|
8
|
+
name: "noop",
|
|
9
|
+
api: "",
|
|
10
|
+
requiredProjectBindings: undefined,
|
|
11
|
+
ensureTriggerRegion: noop,
|
|
12
|
+
};
|
|
13
|
+
exports.PubSubService = {
|
|
14
|
+
name: "pubsub",
|
|
15
|
+
api: "pubsub.googleapis.com",
|
|
16
|
+
requiredProjectBindings: undefined,
|
|
17
|
+
ensureTriggerRegion: noop,
|
|
18
|
+
};
|
|
19
|
+
exports.StorageService = {
|
|
20
|
+
name: "storage",
|
|
21
|
+
api: "storage.googleapis.com",
|
|
22
|
+
requiredProjectBindings: storage_1.obtainStorageBindings,
|
|
23
|
+
ensureTriggerRegion: storage_1.ensureStorageTriggerRegion,
|
|
24
|
+
};
|
|
25
|
+
exports.EVENT_SERVICE_MAPPING = {
|
|
26
|
+
"google.cloud.pubsub.topic.v1.messagePublished": exports.PubSubService,
|
|
27
|
+
"google.cloud.storage.object.v1.finalized": exports.StorageService,
|
|
28
|
+
"google.cloud.storage.object.v1.archived": exports.StorageService,
|
|
29
|
+
"google.cloud.storage.object.v1.deleted": exports.StorageService,
|
|
30
|
+
"google.cloud.storage.object.v1.metadataUpdated": exports.StorageService,
|
|
31
|
+
};
|
|
32
|
+
function serviceForEndpoint(endpoint) {
|
|
33
|
+
if (!backend.isEventTriggered(endpoint)) {
|
|
34
|
+
return exports.NoOpService;
|
|
35
|
+
}
|
|
36
|
+
return exports.EVENT_SERVICE_MAPPING[endpoint.eventTrigger.eventType] || exports.NoOpService;
|
|
37
|
+
}
|
|
38
|
+
exports.serviceForEndpoint = serviceForEndpoint;
|