firebase-tools 9.22.0 → 9.23.3
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 +1 -3
- package/lib/commands/functions-delete.js +7 -1
- 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/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 +5 -1
- package/lib/deploy/functions/release/reporter.js +4 -0
- 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/downloadableEmulators.js +5 -5
- package/lib/emulator/functionsEmulator.js +15 -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 +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
-
|
|
2
|
-
- Fixes issue where `init` would crash with multiple Hosting items selected (#3742).
|
|
3
|
-
- Adds a command (`crashlytics:symbols:upload`) to upload native symbol files, used in Android NDK crash symbolication.
|
|
1
|
+
- Upgrades Database Emulator to v4.7.3, removing log4j dependency.
|
|
@@ -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
|
});
|
|
@@ -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
|
});
|
|
@@ -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,8 +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),
|
|
50
|
+
maybeEnableAR(projectId),
|
|
42
51
|
]);
|
|
43
52
|
context.runtimeConfigEnabled = checkAPIsEnabled[1];
|
|
53
|
+
context.artifactRegistryEnabled = checkAPIsEnabled[3];
|
|
44
54
|
const firebaseConfig = await functionsConfig.getFirebaseConfig(options);
|
|
45
55
|
context.firebaseConfig = firebaseConfig;
|
|
46
56
|
const runtimeConfig = await prepareFunctionsUpload_1.getFunctionsConfig(context);
|
|
@@ -103,8 +113,9 @@ async function prepare(context, options, payload) {
|
|
|
103
113
|
return functionsDeployHelper_1.functionMatchesAnyGroup(endpoint, context.filters);
|
|
104
114
|
});
|
|
105
115
|
const haveBackend = await backend.existingBackend(context);
|
|
116
|
+
await checkIam_1.ensureServiceAgentRoles(projectId, wantBackend, haveBackend);
|
|
106
117
|
inferDetailsFromExisting(wantBackend, haveBackend, usedDotenv);
|
|
107
|
-
await triggerRegionHelper_1.
|
|
118
|
+
await triggerRegionHelper_1.ensureTriggerRegions(wantBackend);
|
|
108
119
|
await prompts_1.promptForFailurePolicies(options, matchingBackend, haveBackend);
|
|
109
120
|
await prompts_1.promptForMinInstances(options, matchingBackend, haveBackend);
|
|
110
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 () => {
|
|
@@ -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";
|
|
@@ -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
|
};
|