firebase-tools 9.22.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -3
- 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/extensions/params.js +39 -0
- package/lib/deploy/extensions/planner.js +11 -12
- package/lib/deploy/extensions/prepare.js +9 -1
- package/lib/deploy/functions/checkIam.js +65 -4
- package/lib/deploy/functions/containerCleaner.js +68 -77
- package/lib/deploy/functions/deploy.js +6 -7
- package/lib/deploy/functions/eventTypes.js +10 -0
- package/lib/deploy/functions/prepare.js +12 -1
- 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/deploy/index.js +9 -1
- 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/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 +8 -6
- 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/projectUtils.js +10 -1
- package/package.json +5 -5
- 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
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
-
|
|
2
|
-
-
|
|
3
|
-
-
|
|
1
|
+
- **BREAKING** Drops support for running the CLI on Node 10.
|
|
2
|
+
- **BREAKING** Replaces all usages of `-y`, `--yes`, or `--confirm` with `-f` and `--force`.
|
|
3
|
+
- **BREAKING** Function deploys upload source to the deployed region instead of us-central1.
|
|
4
|
+
- Requires firebase-functions >= 3.13.1 in Functions emulator to include bug fixes (#3851).
|
|
5
|
+
- Updates default functions runtime to Node.js 16.
|
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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readParams = void 0;
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const logger_1 = require("../../logger");
|
|
6
|
+
const paramHelper_1 = require("../../extensions/paramHelper");
|
|
7
|
+
const error_1 = require("../../error");
|
|
8
|
+
const ENV_DIRECTORY = "extensions";
|
|
9
|
+
function readParams(args) {
|
|
10
|
+
const filesToCheck = [
|
|
11
|
+
`${args.instanceId}.env`,
|
|
12
|
+
...args.aliases.map((alias) => `${args.instanceId}.env.${alias}`),
|
|
13
|
+
`${args.instanceId}.env.${args.projectNumber}`,
|
|
14
|
+
`${args.instanceId}.env.${args.projectId}`,
|
|
15
|
+
];
|
|
16
|
+
let noFilesFound = true;
|
|
17
|
+
const combinedParams = {};
|
|
18
|
+
for (const fileToCheck of filesToCheck) {
|
|
19
|
+
try {
|
|
20
|
+
const params = readParamsFile(args.projectDir, fileToCheck);
|
|
21
|
+
logger_1.logger.debug(`Successfully read params from ${fileToCheck}`);
|
|
22
|
+
noFilesFound = false;
|
|
23
|
+
Object.assign(combinedParams, params);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
logger_1.logger.debug(`${err}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (noFilesFound) {
|
|
30
|
+
throw new error_1.FirebaseError(`No params file found for ${args.instanceId}`);
|
|
31
|
+
}
|
|
32
|
+
return combinedParams;
|
|
33
|
+
}
|
|
34
|
+
exports.readParams = readParams;
|
|
35
|
+
function readParamsFile(projectDir, fileName) {
|
|
36
|
+
const paramPath = path.join(projectDir, ENV_DIRECTORY, fileName);
|
|
37
|
+
const params = paramHelper_1.readEnvFile(paramPath);
|
|
38
|
+
return params;
|
|
39
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveVersion = exports.want = exports.have = exports.getExtension = exports.getExtensionVersion = void 0;
|
|
4
|
-
const path = require("path");
|
|
5
4
|
const semver = require("semver");
|
|
6
5
|
const error_1 = require("../../error");
|
|
7
6
|
const extensionsApi = require("../../extensions/extensionsApi");
|
|
8
7
|
const extensionsHelper_1 = require("../../extensions/extensionsHelper");
|
|
9
8
|
const refs = require("../../extensions/refs");
|
|
10
|
-
const
|
|
9
|
+
const params_1 = require("./params");
|
|
11
10
|
const logger_1 = require("../../logger");
|
|
12
11
|
async function getExtensionVersion(i) {
|
|
13
12
|
if (!i.extensionVersion) {
|
|
@@ -29,7 +28,6 @@ async function getExtension(i) {
|
|
|
29
28
|
return i.extension;
|
|
30
29
|
}
|
|
31
30
|
exports.getExtension = getExtension;
|
|
32
|
-
const ENV_DIRECTORY = "extensions";
|
|
33
31
|
async function have(projectId) {
|
|
34
32
|
const instances = await extensionsApi.listInstances(projectId);
|
|
35
33
|
return instances.map((i) => {
|
|
@@ -46,16 +44,22 @@ async function have(projectId) {
|
|
|
46
44
|
});
|
|
47
45
|
}
|
|
48
46
|
exports.have = have;
|
|
49
|
-
async function want(
|
|
47
|
+
async function want(args) {
|
|
50
48
|
const instanceSpecs = [];
|
|
51
49
|
const errors = [];
|
|
52
|
-
for (const e of Object.entries(extensions)) {
|
|
50
|
+
for (const e of Object.entries(args.extensions)) {
|
|
53
51
|
try {
|
|
54
52
|
const instanceId = e[0];
|
|
55
53
|
const ref = refs.parse(e[1]);
|
|
56
54
|
ref.version = await resolveVersion(ref);
|
|
57
|
-
const params = readParams(
|
|
58
|
-
|
|
55
|
+
const params = params_1.readParams({
|
|
56
|
+
projectDir: args.projectDir,
|
|
57
|
+
instanceId,
|
|
58
|
+
projectId: args.projectId,
|
|
59
|
+
projectNumber: args.projectNumber,
|
|
60
|
+
aliases: args.aliases,
|
|
61
|
+
});
|
|
62
|
+
const autoPopulatedParams = await extensionsHelper_1.getFirebaseProjectParams(args.projectId);
|
|
59
63
|
const subbedParams = extensionsHelper_1.substituteParams(params, autoPopulatedParams);
|
|
60
64
|
instanceSpecs.push({
|
|
61
65
|
instanceId,
|
|
@@ -88,8 +92,3 @@ async function resolveVersion(ref) {
|
|
|
88
92
|
return maxSatisfying;
|
|
89
93
|
}
|
|
90
94
|
exports.resolveVersion = resolveVersion;
|
|
91
|
-
function readParams(projectDir, instanceId) {
|
|
92
|
-
const paramPath = path.join(projectDir, ENV_DIRECTORY, `${instanceId}.env`);
|
|
93
|
-
const params = paramHelper_1.readEnvFile(paramPath);
|
|
94
|
-
return params;
|
|
95
|
-
}
|
|
@@ -16,10 +16,18 @@ const warnings_1 = require("../../extensions/warnings");
|
|
|
16
16
|
async function prepare(context, options, payload) {
|
|
17
17
|
var _a;
|
|
18
18
|
const projectId = projectUtils_1.needProjectId(options);
|
|
19
|
+
const projectNumber = await projectUtils_1.needProjectNumber(options);
|
|
20
|
+
const aliases = projectUtils_1.getAliases(options, projectId);
|
|
19
21
|
await extensionsHelper_1.ensureExtensionsApiEnabled(options);
|
|
20
22
|
await requirePermissions_1.requirePermissions(options, ["firebaseextensions.instances.list"]);
|
|
21
23
|
context.have = await planner.have(projectId);
|
|
22
|
-
context.want = await planner.want(
|
|
24
|
+
context.want = await planner.want({
|
|
25
|
+
projectId,
|
|
26
|
+
projectNumber,
|
|
27
|
+
aliases,
|
|
28
|
+
projectDir: options.config.projectDir,
|
|
29
|
+
extensions: options.config.get("extensions"),
|
|
30
|
+
});
|
|
23
31
|
const usingSecrets = await Promise.all((_a = context.have) === null || _a === void 0 ? void 0 : _a.map(secrets_1.checkSpecForSecrets));
|
|
24
32
|
if (usingSecrets.some((i) => i)) {
|
|
25
33
|
await secretsUtils_1.ensureSecretManagerApiEnabled(options);
|
|
@@ -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,9 +1,8 @@
|
|
|
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
|
-
const previews_1 = require("../../previews");
|
|
7
6
|
const api_1 = require("../../api");
|
|
8
7
|
const logger_1 = require("../../logger");
|
|
9
8
|
const artifactregistry = require("../../gcp/artifactregistry");
|
|
@@ -11,31 +10,6 @@ const backend = require("./backend");
|
|
|
11
10
|
const docker = require("../../gcp/docker");
|
|
12
11
|
const utils = require("../../utils");
|
|
13
12
|
const poller = require("../../operation-poller");
|
|
14
|
-
exports.SUBDOMAIN_MAPPING = {
|
|
15
|
-
"us-west2": "us",
|
|
16
|
-
"us-west3": "us",
|
|
17
|
-
"us-west4": "us",
|
|
18
|
-
"us-central1": "us",
|
|
19
|
-
"us-central2": "us",
|
|
20
|
-
"us-east1": "us",
|
|
21
|
-
"us-east4": "us",
|
|
22
|
-
"northamerica-northeast1": "us",
|
|
23
|
-
"southamerica-east1": "us",
|
|
24
|
-
"europe-west1": "eu",
|
|
25
|
-
"europe-west2": "eu",
|
|
26
|
-
"europe-west3": "eu",
|
|
27
|
-
"europe-west5": "eu",
|
|
28
|
-
"europe-west6": "eu",
|
|
29
|
-
"europe-central2": "eu",
|
|
30
|
-
"asia-east1": "asia",
|
|
31
|
-
"asia-east2": "asia",
|
|
32
|
-
"asia-northeast1": "asia",
|
|
33
|
-
"asia-northeast2": "asia",
|
|
34
|
-
"asia-northeast3": "asia",
|
|
35
|
-
"asia-south1": "asia",
|
|
36
|
-
"asia-southeast2": "asia",
|
|
37
|
-
"australia-southeast1": "asia",
|
|
38
|
-
};
|
|
39
13
|
async function retry(func) {
|
|
40
14
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
15
|
const MAX_RETRIES = 3;
|
|
@@ -62,44 +36,37 @@ async function retry(func) {
|
|
|
62
36
|
async function cleanupBuildImages(haveFunctions, deletedFunctions, cleaners = {}) {
|
|
63
37
|
utils.logBullet(clc.bold.cyan("functions: ") + "cleaning up build files...");
|
|
64
38
|
const failedDomains = new Set();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
const path = `${func.project}/${exports.SUBDOMAIN_MAPPING[func.region]}/gcf`;
|
|
99
|
-
failedDomains.add(`https://console.cloud.google.com/gcr/images/${path}`);
|
|
100
|
-
}
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
39
|
+
const cleanup = [];
|
|
40
|
+
const arCleaner = cleaners.ar || new ArtifactRegistryCleaner();
|
|
41
|
+
cleanup.push(...haveFunctions.map(async (func) => {
|
|
42
|
+
try {
|
|
43
|
+
await arCleaner.cleanupFunction(func);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const path = `${func.project}/${func.region}/gcf-artifacts`;
|
|
47
|
+
failedDomains.add(`https://console.cloud.google.com/artifacts/docker/${path}`);
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
cleanup.push(...deletedFunctions.map(async (func) => {
|
|
51
|
+
try {
|
|
52
|
+
await Promise.all([arCleaner.cleanupFunction(func), arCleaner.cleanupFunctionCache(func)]);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const path = `${func.project}/${func.region}/gcf-artifacts`;
|
|
56
|
+
failedDomains.add(`https://console.cloud.google.com/artifacts/docker/${path}`);
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
const gcrCleaner = cleaners.gcr || new ContainerRegistryCleaner();
|
|
60
|
+
cleanup.push(...[...haveFunctions, ...deletedFunctions].map(async (func) => {
|
|
61
|
+
try {
|
|
62
|
+
await gcrCleaner.cleanupFunction(func);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const path = `${func.project}/${docker.GCR_SUBDOMAIN_MAPPING[func.region]}/gcf`;
|
|
66
|
+
failedDomains.add(`https://console.cloud.google.com/gcr/images/${path}`);
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
await Promise.all(cleanup);
|
|
103
70
|
if (failedDomains.size) {
|
|
104
71
|
let message = "Unhandled error cleaning up build images. This could result in a small monthly bill if not corrected. ";
|
|
105
72
|
message +=
|
|
@@ -116,10 +83,24 @@ async function cleanupBuildImages(haveFunctions, deletedFunctions, cleaners = {}
|
|
|
116
83
|
exports.cleanupBuildImages = cleanupBuildImages;
|
|
117
84
|
class ArtifactRegistryCleaner {
|
|
118
85
|
static packagePath(func) {
|
|
119
|
-
|
|
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}`;
|
|
120
92
|
}
|
|
121
93
|
async cleanupFunction(func) {
|
|
122
|
-
|
|
94
|
+
let op;
|
|
95
|
+
try {
|
|
96
|
+
op = await artifactregistry.deletePackage(ArtifactRegistryCleaner.packagePath(func));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (err.status === 404) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
123
104
|
if (op.done) {
|
|
124
105
|
return;
|
|
125
106
|
}
|
|
@@ -139,12 +120,21 @@ ArtifactRegistryCleaner.POLLER_OPTIONS = {
|
|
|
139
120
|
apiVersion: artifactregistry.API_VERSION,
|
|
140
121
|
masterTimeout: 5 * 60 * 1000,
|
|
141
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;
|
|
142
132
|
class ContainerRegistryCleaner {
|
|
143
133
|
constructor() {
|
|
144
134
|
this.helpers = {};
|
|
145
135
|
}
|
|
146
136
|
helper(location) {
|
|
147
|
-
const subdomain =
|
|
137
|
+
const subdomain = docker.GCR_SUBDOMAIN_MAPPING[location] || "us";
|
|
148
138
|
if (!this.helpers[subdomain]) {
|
|
149
139
|
const origin = `https://${subdomain}.${api_1.containerRegistryDomain}`;
|
|
150
140
|
this.helpers[subdomain] = new DockerHelper(origin);
|
|
@@ -184,14 +174,14 @@ function getHelper(cache, subdomain) {
|
|
|
184
174
|
}
|
|
185
175
|
async function listGcfPaths(projectId, locations, dockerHelpers = {}) {
|
|
186
176
|
if (!locations) {
|
|
187
|
-
locations = Object.keys(
|
|
177
|
+
locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
|
|
188
178
|
}
|
|
189
|
-
const invalidRegion = locations.find((loc) => !
|
|
179
|
+
const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
|
|
190
180
|
if (invalidRegion) {
|
|
191
181
|
throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
|
|
192
182
|
}
|
|
193
183
|
const locationsSet = new Set(locations);
|
|
194
|
-
const subdomains = new Set(Object.values(
|
|
184
|
+
const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
|
|
195
185
|
const failedSubdomains = [];
|
|
196
186
|
const listAll = [];
|
|
197
187
|
for (const subdomain of subdomains) {
|
|
@@ -222,26 +212,27 @@ async function listGcfPaths(projectId, locations, dockerHelpers = {}) {
|
|
|
222
212
|
throw new error_1.FirebaseError(`Failed to search the following subdomains: ${failedSubdomains.join(",")}`);
|
|
223
213
|
}
|
|
224
214
|
return gcfDirs.map((loc) => {
|
|
225
|
-
return `${
|
|
215
|
+
return `${docker.GCR_SUBDOMAIN_MAPPING[loc]}.${api_1.containerRegistryDomain}/${projectId}/gcf/${loc}`;
|
|
226
216
|
});
|
|
227
217
|
}
|
|
228
218
|
exports.listGcfPaths = listGcfPaths;
|
|
229
219
|
async function deleteGcfArtifacts(projectId, locations, dockerHelpers = {}) {
|
|
230
220
|
if (!locations) {
|
|
231
|
-
locations = Object.keys(
|
|
221
|
+
locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
|
|
232
222
|
}
|
|
233
|
-
const invalidRegion = locations.find((loc) => !
|
|
223
|
+
const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
|
|
234
224
|
if (invalidRegion) {
|
|
235
225
|
throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
|
|
236
226
|
}
|
|
237
|
-
const subdomains = new Set(Object.values(
|
|
227
|
+
const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
|
|
238
228
|
const failedSubdomains = [];
|
|
239
229
|
const deleteLocations = locations.map((loc) => {
|
|
230
|
+
const subdomain = docker.GCR_SUBDOMAIN_MAPPING[loc];
|
|
240
231
|
try {
|
|
241
|
-
return getHelper(dockerHelpers,
|
|
232
|
+
return getHelper(dockerHelpers, subdomain).rm(`${projectId}/gcf/${loc}`);
|
|
242
233
|
}
|
|
243
234
|
catch (err) {
|
|
244
|
-
failedSubdomains.push(
|
|
235
|
+
failedSubdomains.push(subdomain);
|
|
245
236
|
logger_1.logger.debug(err);
|
|
246
237
|
}
|
|
247
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")) {
|