firebase-tools 9.19.0 → 9.20.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 +1 -3
- package/lib/api.js +1 -0
- package/lib/commands/crashlytics-symbols-upload.js +146 -0
- package/lib/commands/ext-configure.js +2 -0
- package/lib/commands/ext-install.js +23 -2
- package/lib/commands/ext-uninstall.js +6 -0
- package/lib/commands/ext-update.js +9 -2
- package/lib/commands/functions-config-export.js +115 -0
- package/lib/commands/functions-delete.js +44 -35
- package/lib/commands/functions-list.js +1 -1
- package/lib/commands/index.js +8 -0
- package/lib/deploy/functions/backend.js +13 -4
- package/lib/deploy/functions/prepare.js +3 -0
- package/lib/deploy/functions/runtimes/node/parseTriggers.js +13 -1
- package/lib/deploy/functions/triggerRegionHelper.js +32 -0
- package/lib/downloadUtils.js +37 -0
- package/lib/emulator/auth/apiSpec.js +513 -1
- package/lib/emulator/auth/handlers.js +4 -3
- package/lib/emulator/auth/operations.js +144 -14
- package/lib/emulator/auth/server.js +10 -13
- package/lib/emulator/auth/state.js +132 -13
- package/lib/emulator/download.js +2 -31
- package/lib/emulator/functionsEmulatorRuntime.js +29 -7
- package/lib/extensions/askUserForConsent.js +14 -1
- package/lib/extensions/askUserForParam.js +72 -3
- package/lib/extensions/extensionsApi.js +1 -0
- package/lib/extensions/extensionsHelper.js +1 -0
- package/lib/extensions/paramHelper.js +3 -2
- package/lib/extensions/secretsUtils.js +58 -0
- package/lib/functional.js +8 -1
- package/lib/functions/env.js +10 -4
- package/lib/functions/runtimeConfigExport.js +137 -0
- package/lib/gcp/cloudfunctions.js +4 -2
- package/lib/gcp/cloudfunctionsv2.js +6 -0
- package/lib/gcp/secretManager.js +111 -0
- package/lib/gcp/storage.js +16 -0
- package/lib/previews.js +1 -1
- package/lib/requireInteractive.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
- `ext:
|
|
2
|
-
- Fixes issue where account specified by `login:use` was not being correctly loaded (#3759).
|
|
3
|
-
- Fixes minor layout issues in Auth Emulator IDP sign-in page (#3774).
|
|
1
|
+
- `ext:install`, `ext:update` and `ext:configure` now support param type `secret`.
|
package/lib/api.js
CHANGED
|
@@ -115,6 +115,7 @@ var api = {
|
|
|
115
115
|
serviceUsageOrigin: utils.envOverride("FIREBASE_SERVICE_USAGE_URL", "https://serviceusage.googleapis.com"),
|
|
116
116
|
githubOrigin: utils.envOverride("GITHUB_URL", "https://github.com"),
|
|
117
117
|
githubApiOrigin: utils.envOverride("GITHUB_API_URL", "https://api.github.com"),
|
|
118
|
+
secretManagerOrigin: utils.envOverride("CLOUD_SECRET_MANAGER_URL", "https://secretmanager.googleapis.com"),
|
|
118
119
|
githubClientId: utils.envOverride("GITHUB_CLIENT_ID", "89cf50f02ac6aaed3484"),
|
|
119
120
|
githubClientSecret: utils.envOverride("GITHUB_CLIENT_SECRET", "3330d14abc895d9a74d5f17cd7a00711fa2c5bf0"),
|
|
120
121
|
setRefreshToken: function (token) {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const spawn = require("cross-spawn");
|
|
7
|
+
const uuid = require("uuid");
|
|
8
|
+
const command_1 = require("../command");
|
|
9
|
+
const downloadUtils = require("../downloadUtils");
|
|
10
|
+
const error_1 = require("../error");
|
|
11
|
+
const logger_1 = require("../logger");
|
|
12
|
+
const rimraf = require("rimraf");
|
|
13
|
+
const utils = require("../utils");
|
|
14
|
+
var SymbolGenerator;
|
|
15
|
+
(function (SymbolGenerator) {
|
|
16
|
+
SymbolGenerator["breakpad"] = "breakpad";
|
|
17
|
+
SymbolGenerator["csym"] = "csym";
|
|
18
|
+
})(SymbolGenerator || (SymbolGenerator = {}));
|
|
19
|
+
const SYMBOL_CACHE_ROOT_DIR = process.env.FIREBASE_CRASHLYTICS_CACHE_PATH || os.tmpdir();
|
|
20
|
+
const JAR_CACHE_DIR = process.env.FIREBASE_CRASHLYTICS_BUILDTOOLS_PATH ||
|
|
21
|
+
path.join(os.homedir(), ".cache", "firebase", "crashlytics", "buildtools");
|
|
22
|
+
const JAR_VERSION = "2.8.0";
|
|
23
|
+
const JAR_URL = `https://storage.googleapis.com/firebase-preview-drop/android/crashlytics-eap/crashlytics-buildtools/firebase-crashlytics-buildtools-${JAR_VERSION}-alpha-all.jar`;
|
|
24
|
+
exports.default = new command_1.Command("crashlytics:symbols:upload <symbolFiles...>")
|
|
25
|
+
.description("Upload symbols for native code, to symbolicate stack traces.")
|
|
26
|
+
.option("--app <appID>", "the app id of your Firebase app")
|
|
27
|
+
.option("--generator [breakpad|csym]", "the symbol generator being used, defaults to breakpad.")
|
|
28
|
+
.option("--dry-run", "generate symbols without uploading them")
|
|
29
|
+
.option("--debug", "print debug output and logging from the underlying uploader tool")
|
|
30
|
+
.action(async (symbolFiles, options) => {
|
|
31
|
+
const app = getGoogleAppID(options) || "";
|
|
32
|
+
const generator = getSymbolGenerator(options);
|
|
33
|
+
const dryRun = !!options.dryRun;
|
|
34
|
+
const debug = !!options.debug;
|
|
35
|
+
let jarFile = await downloadBuiltoolsJar();
|
|
36
|
+
if (process.env.LOCAL_JAR) {
|
|
37
|
+
jarFile = process.env.LOCAL_JAR;
|
|
38
|
+
}
|
|
39
|
+
const jarOptions = {
|
|
40
|
+
jarFile,
|
|
41
|
+
app,
|
|
42
|
+
generator,
|
|
43
|
+
cachePath: path.join(SYMBOL_CACHE_ROOT_DIR, `crashlytics-${uuid.v4()}`, "nativeSymbols", app, generator),
|
|
44
|
+
symbolFile: "",
|
|
45
|
+
generate: true,
|
|
46
|
+
};
|
|
47
|
+
for (const symbolFile of symbolFiles) {
|
|
48
|
+
utils.logBullet(`Generating symbols for ${symbolFile}`);
|
|
49
|
+
const generateArgs = buildArgs(Object.assign(Object.assign({}, jarOptions), { symbolFile }));
|
|
50
|
+
const output = runJar(generateArgs, debug);
|
|
51
|
+
if (output.length > 0) {
|
|
52
|
+
utils.logBullet(output);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
utils.logBullet(`Generated symbols for ${symbolFile}`);
|
|
56
|
+
utils.logBullet(`Output Path: ${jarOptions.cachePath}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (dryRun) {
|
|
60
|
+
utils.logBullet("Skipping upload because --dry-run was passed");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
utils.logBullet(`Uploading all generated symbols`);
|
|
64
|
+
const uploadArgs = buildArgs(Object.assign(Object.assign({}, jarOptions), { generate: false }));
|
|
65
|
+
const output = runJar(uploadArgs, debug);
|
|
66
|
+
if (output.length > 0) {
|
|
67
|
+
utils.logBullet(output);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
utils.logBullet("Successfully uploaded all symbols");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
function getGoogleAppID(options) {
|
|
74
|
+
if (!options.app) {
|
|
75
|
+
throw new error_1.FirebaseError("set the --app option to a valid Firebase app id and try again");
|
|
76
|
+
}
|
|
77
|
+
return options.app;
|
|
78
|
+
}
|
|
79
|
+
function getSymbolGenerator(options) {
|
|
80
|
+
if (!options.generator) {
|
|
81
|
+
return SymbolGenerator.breakpad;
|
|
82
|
+
}
|
|
83
|
+
if (!Object.values(SymbolGenerator).includes(options.generator)) {
|
|
84
|
+
throw new error_1.FirebaseError('--symbol-generator should be set to either "breakpad" or "csym"');
|
|
85
|
+
}
|
|
86
|
+
return options.generator;
|
|
87
|
+
}
|
|
88
|
+
async function downloadBuiltoolsJar() {
|
|
89
|
+
const jarPath = path.join(JAR_CACHE_DIR, `crashlytics-buildtools-${JAR_VERSION}.jar`);
|
|
90
|
+
if (fs.existsSync(jarPath)) {
|
|
91
|
+
logger_1.logger.debug(`Buildtools Jar already downloaded at ${jarPath}`);
|
|
92
|
+
return jarPath;
|
|
93
|
+
}
|
|
94
|
+
if (fs.existsSync(JAR_CACHE_DIR)) {
|
|
95
|
+
logger_1.logger.debug(`Deleting Jar cache at ${JAR_CACHE_DIR} because the CLI was run with a newer Jar version`);
|
|
96
|
+
rimraf.sync(JAR_CACHE_DIR);
|
|
97
|
+
}
|
|
98
|
+
utils.logBullet("Downloading buildtools.jar to " + jarPath);
|
|
99
|
+
utils.logBullet("For open source licenses used by this command, look in the META-INF directory in the buildtools.jar file");
|
|
100
|
+
const tmpfile = await downloadUtils.downloadToTmp(JAR_URL);
|
|
101
|
+
fs.mkdirSync(JAR_CACHE_DIR, { recursive: true });
|
|
102
|
+
fs.copySync(tmpfile, jarPath);
|
|
103
|
+
return jarPath;
|
|
104
|
+
}
|
|
105
|
+
function buildArgs(options) {
|
|
106
|
+
const baseArgs = [
|
|
107
|
+
"-jar",
|
|
108
|
+
options.jarFile,
|
|
109
|
+
`-symbolGenerator=${options.generator}`,
|
|
110
|
+
`-symbolFileCacheDir=${options.cachePath}`,
|
|
111
|
+
"-verbose",
|
|
112
|
+
];
|
|
113
|
+
if (options.generate) {
|
|
114
|
+
return baseArgs.concat(["-generateNativeSymbols", `-unstrippedLibrary=${options.symbolFile}`]);
|
|
115
|
+
}
|
|
116
|
+
return baseArgs.concat([
|
|
117
|
+
"-uploadNativeSymbols",
|
|
118
|
+
`-googleAppId=${options.app}`,
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
function runJar(args, debug) {
|
|
122
|
+
var _a, _b, _c;
|
|
123
|
+
const outputs = spawn.sync("java", args, {
|
|
124
|
+
stdio: debug ? "inherit" : "pipe",
|
|
125
|
+
});
|
|
126
|
+
if (outputs.status || 0 > 0) {
|
|
127
|
+
if (!debug) {
|
|
128
|
+
utils.logWarning(((_a = outputs.stdout) === null || _a === void 0 ? void 0 : _a.toString()) || "An unknown error occurred");
|
|
129
|
+
}
|
|
130
|
+
throw new error_1.FirebaseError("Failed to upload symbols");
|
|
131
|
+
}
|
|
132
|
+
if (!debug) {
|
|
133
|
+
let logRegex = /(Generated symbol file.*$)/m;
|
|
134
|
+
let matched = (((_b = outputs.stdout) === null || _b === void 0 ? void 0 : _b.toString()) || "").match(logRegex);
|
|
135
|
+
if (matched) {
|
|
136
|
+
return matched[1];
|
|
137
|
+
}
|
|
138
|
+
logRegex = /(Crashlytics symbol file uploaded successfully.*$)/m;
|
|
139
|
+
matched = (((_c = outputs.stdout) === null || _c === void 0 ? void 0 : _c.toString()) || "").match(logRegex);
|
|
140
|
+
if (matched) {
|
|
141
|
+
return matched[1];
|
|
142
|
+
}
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
@@ -52,6 +52,8 @@ exports.default = new command_1.Command("ext:configure <extensionInstanceId>")
|
|
|
52
52
|
paramSpecs: paramSpecWithNewDefaults,
|
|
53
53
|
nonInteractive: options.nonInteractive,
|
|
54
54
|
paramsEnvPath: options.params,
|
|
55
|
+
instanceId,
|
|
56
|
+
reconfiguring: true,
|
|
55
57
|
});
|
|
56
58
|
if (immutableParams.length) {
|
|
57
59
|
const plural = immutableParams.length > 1;
|
|
@@ -14,6 +14,7 @@ const command_1 = require("../command");
|
|
|
14
14
|
const error_1 = require("../error");
|
|
15
15
|
const projectUtils_1 = require("../projectUtils");
|
|
16
16
|
const extensionsApi = require("../extensions/extensionsApi");
|
|
17
|
+
const secretsUtils = require("../extensions/secretsUtils");
|
|
17
18
|
const provisioningHelper = require("../extensions/provisioningHelper");
|
|
18
19
|
const refs = require("../extensions/refs");
|
|
19
20
|
const warnings_1 = require("../extensions/warnings");
|
|
@@ -37,7 +38,8 @@ async function installExtension(options) {
|
|
|
37
38
|
const spinner = ora.default();
|
|
38
39
|
try {
|
|
39
40
|
await provisioningHelper.checkProductsProvisioned(projectId, spec);
|
|
40
|
-
|
|
41
|
+
const usesSecrets = secretsUtils.usesSecrets(spec);
|
|
42
|
+
if (spec.billingRequired || usesSecrets) {
|
|
41
43
|
const enabled = await cloudbilling_1.checkBillingEnabled(projectId);
|
|
42
44
|
if (!enabled && nonInteractive) {
|
|
43
45
|
throw new error_1.FirebaseError(`This extension requires the Blaze plan, but project ${projectId} is not on the Blaze plan. ` +
|
|
@@ -51,6 +53,23 @@ async function installExtension(options) {
|
|
|
51
53
|
await billingMigrationHelper_1.displayNode10CreateBillingNotice(spec, !nonInteractive);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
56
|
+
const apis = spec.apis || [];
|
|
57
|
+
if (usesSecrets) {
|
|
58
|
+
apis.push({
|
|
59
|
+
apiName: "secretmanager.googleapis.com",
|
|
60
|
+
reason: `To access and manage secrets which are used by this extension. By using this product you agree to the terms and conditions of the following license: https://console.cloud.google.com/tos?id=cloud&project=${projectId}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (apis.length) {
|
|
64
|
+
askUserForConsent.displayApis(spec.displayName || spec.name, projectId, apis);
|
|
65
|
+
const consented = await extensionsHelper_1.confirm({ nonInteractive, force, default: true });
|
|
66
|
+
if (!consented) {
|
|
67
|
+
throw new error_1.FirebaseError("Without explicit consent for the APIs listed, we cannot deploy this extension.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (usesSecrets) {
|
|
71
|
+
await secretsUtils.ensureSecretManagerApiEnabled(options);
|
|
72
|
+
}
|
|
54
73
|
const roles = spec.roles ? spec.roles.map((role) => role.role) : [];
|
|
55
74
|
if (roles.length) {
|
|
56
75
|
await askUserForConsent.displayRoles(spec.displayName || spec.name, projectId, roles);
|
|
@@ -86,6 +105,7 @@ async function installExtension(options) {
|
|
|
86
105
|
paramSpecs: spec.params,
|
|
87
106
|
nonInteractive,
|
|
88
107
|
paramsEnvPath,
|
|
108
|
+
instanceId,
|
|
89
109
|
});
|
|
90
110
|
spinner.text = "Installing your extension instance. This usually takes 3 to 5 minutes...";
|
|
91
111
|
spinner.start();
|
|
@@ -106,6 +126,7 @@ async function installExtension(options) {
|
|
|
106
126
|
paramSpecs: spec.params,
|
|
107
127
|
nonInteractive,
|
|
108
128
|
paramsEnvPath,
|
|
129
|
+
instanceId,
|
|
109
130
|
});
|
|
110
131
|
spinner.text = "Updating your extension instance. This usually takes 3 to 5 minutes...";
|
|
111
132
|
spinner.start();
|
|
@@ -164,7 +185,7 @@ async function infoInstallByReference(extensionName) {
|
|
|
164
185
|
}
|
|
165
186
|
const extVersion = await extensionsApi.getExtensionVersion(extensionName);
|
|
166
187
|
displayExtensionInfo_1.displayExtInfo(extensionName, ref.publisherId, extVersion.spec, true);
|
|
167
|
-
warnings_1.displayWarningPrompts(ref.publisherId, extension.registryLaunchStage, extVersion);
|
|
188
|
+
await warnings_1.displayWarningPrompts(ref.publisherId, extension.registryLaunchStage, extVersion);
|
|
168
189
|
return extVersion;
|
|
169
190
|
}
|
|
170
191
|
exports.default = new command_1.Command("ext:install [extensionName]")
|
|
@@ -10,6 +10,7 @@ const command_1 = require("../command");
|
|
|
10
10
|
const error_1 = require("../error");
|
|
11
11
|
const projectUtils_1 = require("../projectUtils");
|
|
12
12
|
const extensionsApi = require("../extensions/extensionsApi");
|
|
13
|
+
const secretsUtils = require("../extensions/secretsUtils");
|
|
13
14
|
const extensionsHelper_1 = require("../extensions/extensionsHelper");
|
|
14
15
|
const prompt_1 = require("../prompt");
|
|
15
16
|
const requirePermissions_1 = require("../requirePermissions");
|
|
@@ -51,11 +52,16 @@ exports.default = new command_1.Command("ext:uninstall <extensionInstanceId>")
|
|
|
51
52
|
}
|
|
52
53
|
if (!options.force) {
|
|
53
54
|
const serviceAccountMessage = `Uninstalling deletes the service account used by this extension instance:\n${clc.bold(instance.serviceAccountEmail)}\n\n`;
|
|
55
|
+
const managedSecrets = await secretsUtils.getManagedSecrets(instance);
|
|
54
56
|
const resourcesMessage = _.get(instance, "config.source.spec.resources", []).length
|
|
55
57
|
? "Uninstalling deletes all extension resources created for this extension instance:\n" +
|
|
56
58
|
instance.config.source.spec.resources
|
|
57
59
|
.map((resource) => clc.bold(`- ${extensionsHelper_1.resourceTypeToNiceName[resource.type] || resource.type}: ${resource.name} \n`))
|
|
58
60
|
.join("") +
|
|
61
|
+
managedSecrets
|
|
62
|
+
.map(secretsUtils.prettySecretName)
|
|
63
|
+
.map((s) => clc.bold(`- Secret: ${s}\n`))
|
|
64
|
+
.join("") +
|
|
59
65
|
"\n"
|
|
60
66
|
: "";
|
|
61
67
|
const artifactsMessage = `The following ${clc.bold("will not")} be deleted:\n` +
|
|
@@ -12,6 +12,7 @@ const billingMigrationHelper_1 = require("../extensions/billingMigrationHelper")
|
|
|
12
12
|
const checkProjectBilling_1 = require("../extensions/checkProjectBilling");
|
|
13
13
|
const cloudbilling_1 = require("../gcp/cloudbilling");
|
|
14
14
|
const extensionsApi = require("../extensions/extensionsApi");
|
|
15
|
+
const secretsUtils = require("../extensions/secretsUtils");
|
|
15
16
|
const provisioningHelper = require("../extensions/provisioningHelper");
|
|
16
17
|
const extensionsHelper_1 = require("../extensions/extensionsHelper");
|
|
17
18
|
const paramHelper = require("../extensions/paramHelper");
|
|
@@ -124,7 +125,8 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
|
|
|
124
125
|
force: options.force,
|
|
125
126
|
});
|
|
126
127
|
await provisioningHelper.checkProductsProvisioned(projectId, newSpec);
|
|
127
|
-
|
|
128
|
+
const usesSecrets = secretsUtils.usesSecrets(newSpec);
|
|
129
|
+
if (newSpec.billingRequired || usesSecrets) {
|
|
128
130
|
const enabled = await cloudbilling_1.checkBillingEnabled(projectId);
|
|
129
131
|
billingMigrationHelper_1.displayNode10UpdateBillingNotice(existingSpec, newSpec);
|
|
130
132
|
if (!(await extensionsHelper_1.confirm({
|
|
@@ -144,7 +146,11 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
|
|
|
144
146
|
marked(`https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`));
|
|
145
147
|
}
|
|
146
148
|
}
|
|
149
|
+
if (usesSecrets) {
|
|
150
|
+
await secretsUtils.ensureSecretManagerApiEnabled(options);
|
|
151
|
+
}
|
|
147
152
|
}
|
|
153
|
+
const oldParamValues = Object.assign({}, existingParams);
|
|
148
154
|
const newParams = await paramHelper.getParamsForUpdate({
|
|
149
155
|
spec: existingSpec,
|
|
150
156
|
newSpec,
|
|
@@ -152,6 +158,7 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
|
|
|
152
158
|
projectId,
|
|
153
159
|
paramsEnvPath: options.params,
|
|
154
160
|
nonInteractive: options.nonInteractive,
|
|
161
|
+
instanceId,
|
|
155
162
|
});
|
|
156
163
|
spinner.start();
|
|
157
164
|
const updateOptions = {
|
|
@@ -164,7 +171,7 @@ exports.default = new command_1.Command("ext:update <extensionInstanceId> [updat
|
|
|
164
171
|
else {
|
|
165
172
|
updateOptions.source = newSource;
|
|
166
173
|
}
|
|
167
|
-
if (!_.isEqual(newParams,
|
|
174
|
+
if (!_.isEqual(newParams, oldParamValues)) {
|
|
168
175
|
updateOptions.params = newParams;
|
|
169
176
|
}
|
|
170
177
|
await updateHelper_1.update(updateOptions);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const clc = require("cli-color");
|
|
5
|
+
const requireInteractive_1 = require("../requireInteractive");
|
|
6
|
+
const command_1 = require("../command");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
const iam_1 = require("../gcp/iam");
|
|
9
|
+
const logger_1 = require("../logger");
|
|
10
|
+
const prompt_1 = require("../prompt");
|
|
11
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
12
|
+
const utils_1 = require("../utils");
|
|
13
|
+
const functional_1 = require("../functional");
|
|
14
|
+
const configExport = require("../functions/runtimeConfigExport");
|
|
15
|
+
const requireConfig = require("../requireConfig");
|
|
16
|
+
const REQUIRED_PERMISSIONS = [
|
|
17
|
+
"runtimeconfig.configs.list",
|
|
18
|
+
"runtimeconfig.configs.get",
|
|
19
|
+
"runtimeconfig.variables.list",
|
|
20
|
+
"runtimeconfig.variables.get",
|
|
21
|
+
];
|
|
22
|
+
const RESERVED_PROJECT_ALIAS = ["local"];
|
|
23
|
+
const MAX_ATTEMPTS = 3;
|
|
24
|
+
function checkReservedAliases(pInfos) {
|
|
25
|
+
for (const pInfo of pInfos) {
|
|
26
|
+
if (pInfo.alias && RESERVED_PROJECT_ALIAS.includes(pInfo.alias)) {
|
|
27
|
+
utils_1.logWarning(`Project alias (${clc.bold(pInfo.alias)}) is reserved for internal use. ` +
|
|
28
|
+
`Saving exported config in .env.${pInfo.projectId} instead.`);
|
|
29
|
+
delete pInfo.alias;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function checkRequiredPermission(pInfos) {
|
|
34
|
+
pInfos = pInfos.filter((pInfo) => !pInfo.config);
|
|
35
|
+
const testPermissions = pInfos.map((pInfo) => iam_1.testIamPermissions(pInfo.projectId, REQUIRED_PERMISSIONS));
|
|
36
|
+
const results = await Promise.all(testPermissions);
|
|
37
|
+
for (const [pInfo, result] of functional_1.zip(pInfos, results)) {
|
|
38
|
+
if (result.passed) {
|
|
39
|
+
throw new error_1.FirebaseError(`Unexpectedly failed to fetch runtime config for project ${pInfo.projectId}`);
|
|
40
|
+
}
|
|
41
|
+
utils_1.logWarning("You are missing the following permissions to read functions config on project " +
|
|
42
|
+
`${clc.bold(pInfo.projectId)}:\n\t${result.missing.join("\n\t")}`);
|
|
43
|
+
const confirm = await prompt_1.promptOnce({
|
|
44
|
+
type: "confirm",
|
|
45
|
+
name: "skip",
|
|
46
|
+
default: true,
|
|
47
|
+
message: `Continue without importing configs from project ${pInfo.projectId}?`,
|
|
48
|
+
});
|
|
49
|
+
if (!confirm) {
|
|
50
|
+
throw new error_1.FirebaseError("Command aborted!");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function promptForPrefix(errMsg) {
|
|
55
|
+
utils_1.logWarning("The following configs keys could not be exported as environment variables:\n");
|
|
56
|
+
utils_1.logWarning(errMsg);
|
|
57
|
+
return await prompt_1.promptOnce({
|
|
58
|
+
type: "input",
|
|
59
|
+
name: "prefix",
|
|
60
|
+
default: "CONFIG_",
|
|
61
|
+
message: "Enter a PREFIX to rename invalid environment variable keys:",
|
|
62
|
+
}, {});
|
|
63
|
+
}
|
|
64
|
+
function fromEntries(itr) {
|
|
65
|
+
const obj = {};
|
|
66
|
+
for (const [k, v] of itr) {
|
|
67
|
+
obj[k] = v;
|
|
68
|
+
}
|
|
69
|
+
return obj;
|
|
70
|
+
}
|
|
71
|
+
exports.default = new command_1.Command("functions:config:export")
|
|
72
|
+
.description("Export environment config as environment variables in dotenv format")
|
|
73
|
+
.before(requirePermissions_1.requirePermissions, [
|
|
74
|
+
"runtimeconfig.configs.list",
|
|
75
|
+
"runtimeconfig.configs.get",
|
|
76
|
+
"runtimeconfig.variables.list",
|
|
77
|
+
"runtimeconfig.variables.get",
|
|
78
|
+
])
|
|
79
|
+
.before(requireConfig)
|
|
80
|
+
.before(requireInteractive_1.default)
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
let pInfos = configExport.getProjectInfos(options);
|
|
83
|
+
checkReservedAliases(pInfos);
|
|
84
|
+
utils_1.logBullet("Importing functions configs from projects [" +
|
|
85
|
+
pInfos.map(({ projectId }) => `${clc.bold(projectId)}`).join(", ") +
|
|
86
|
+
"]");
|
|
87
|
+
await configExport.hydrateConfigs(pInfos);
|
|
88
|
+
await checkRequiredPermission(pInfos);
|
|
89
|
+
pInfos = pInfos.filter((pInfo) => pInfo.config);
|
|
90
|
+
logger_1.logger.debug(`Loaded function configs: ${JSON.stringify(pInfos)}`);
|
|
91
|
+
utils_1.logBullet(`Importing configs from projects: [${pInfos.map((p) => p.projectId).join(", ")}]`);
|
|
92
|
+
let attempts = 0;
|
|
93
|
+
let prefix = "";
|
|
94
|
+
while (true) {
|
|
95
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
96
|
+
throw new error_1.FirebaseError("Exceeded max attempts to fix invalid config keys.");
|
|
97
|
+
}
|
|
98
|
+
const errMsg = configExport.hydrateEnvs(pInfos, prefix);
|
|
99
|
+
if (errMsg.length == 0) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
prefix = await promptForPrefix(errMsg);
|
|
103
|
+
attempts += 1;
|
|
104
|
+
}
|
|
105
|
+
const header = `# Exported firebase functions:config:export command on ${new Date().toLocaleDateString()}`;
|
|
106
|
+
const dotEnvs = pInfos.map((pInfo) => configExport.toDotenvFormat(pInfo.envs, header));
|
|
107
|
+
const filenames = pInfos.map(configExport.generateDotenvFilename);
|
|
108
|
+
const filesToWrite = fromEntries(functional_1.zip(filenames, dotEnvs));
|
|
109
|
+
filesToWrite[".env.local"] = `${header}\n# .env.local file contains environment variables for the Functions Emulator.\n`;
|
|
110
|
+
filesToWrite[".env"] = `${header}# .env file contains environment variables that applies to all projects.\n`;
|
|
111
|
+
const functionsDir = options.config.get("functions.source", ".");
|
|
112
|
+
for (const [filename, content] of Object.entries(filesToWrite)) {
|
|
113
|
+
await options.config.askWriteProjectFile(path.join(functionsDir, filename), content);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -10,6 +10,7 @@ const helper = require("../deploy/functions/functionsDeployHelper");
|
|
|
10
10
|
const requirePermissions_1 = require("../requirePermissions");
|
|
11
11
|
const utils = require("../utils");
|
|
12
12
|
const backend = require("../deploy/functions/backend");
|
|
13
|
+
const error_1 = require("../error");
|
|
13
14
|
exports.default = new command_1.Command("functions:delete [filters...]")
|
|
14
15
|
.description("delete one or more Cloud Functions by name or group name.")
|
|
15
16
|
.option("--region <region>", "Specify region of the function to be deleted. " +
|
|
@@ -26,41 +27,49 @@ exports.default = new command_1.Command("functions:delete [filters...]")
|
|
|
26
27
|
const filterChunks = filters.map((filter) => {
|
|
27
28
|
return filter.split(".");
|
|
28
29
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
try {
|
|
31
|
+
const [config, existingBackend] = await Promise.all([
|
|
32
|
+
functionsConfig.getFirebaseConfig(options),
|
|
33
|
+
backend.existingBackend(context),
|
|
34
|
+
]);
|
|
35
|
+
await backend.checkAvailability(context, backend.empty());
|
|
36
|
+
const appEngineLocation = functionsConfig.getAppEngineLocation(config);
|
|
37
|
+
const functionsToDelete = existingBackend.cloudFunctions.filter((fn) => {
|
|
38
|
+
const regionMatches = options.region ? fn.region === options.region : true;
|
|
39
|
+
const nameMatches = helper.functionMatchesAnyGroup(fn, filterChunks);
|
|
40
|
+
return regionMatches && nameMatches;
|
|
41
|
+
});
|
|
42
|
+
if (functionsToDelete.length === 0) {
|
|
43
|
+
throw new Error(`The specified filters do not match any existing functions in project ${clc.bold(context.projectId)}.`);
|
|
44
|
+
}
|
|
45
|
+
const schedulesToDelete = existingBackend.schedules.filter((schedule) => {
|
|
46
|
+
functionsToDelete.some(backend.sameFunctionName(schedule.targetService));
|
|
47
|
+
});
|
|
48
|
+
const topicsToDelete = existingBackend.topics.filter((topic) => {
|
|
49
|
+
functionsToDelete.some(backend.sameFunctionName(topic.targetService));
|
|
50
|
+
});
|
|
51
|
+
const deleteList = functionsToDelete
|
|
52
|
+
.map((func) => {
|
|
53
|
+
return "\t" + helper.getFunctionLabel(func);
|
|
54
|
+
})
|
|
55
|
+
.join("\n");
|
|
56
|
+
const confirmDeletion = await prompt_1.promptOnce({
|
|
57
|
+
type: "confirm",
|
|
58
|
+
name: "force",
|
|
59
|
+
default: false,
|
|
60
|
+
message: "You are about to delete the following Cloud Functions:\n" +
|
|
61
|
+
deleteList +
|
|
62
|
+
"\n Are you sure?",
|
|
63
|
+
}, options);
|
|
64
|
+
if (!confirmDeletion) {
|
|
65
|
+
throw new Error("Command aborted.");
|
|
66
|
+
}
|
|
67
|
+
return await functionsDelete_1.deleteFunctions(functionsToDelete, schedulesToDelete, topicsToDelete, appEngineLocation);
|
|
42
68
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
const deleteList = functionsToDelete
|
|
50
|
-
.map((func) => {
|
|
51
|
-
return "\t" + helper.getFunctionLabel(func);
|
|
52
|
-
})
|
|
53
|
-
.join("\n");
|
|
54
|
-
const confirmDeletion = await prompt_1.promptOnce({
|
|
55
|
-
type: "confirm",
|
|
56
|
-
name: "force",
|
|
57
|
-
default: false,
|
|
58
|
-
message: "You are about to delete the following Cloud Functions:\n" +
|
|
59
|
-
deleteList +
|
|
60
|
-
"\n Are you sure?",
|
|
61
|
-
}, options);
|
|
62
|
-
if (!confirmDeletion) {
|
|
63
|
-
return utils.reject("Command aborted.", { exit: 1 });
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new error_1.FirebaseError("Failed to delete functions", {
|
|
71
|
+
original: err,
|
|
72
|
+
exit: 1,
|
|
73
|
+
});
|
|
64
74
|
}
|
|
65
|
-
return await functionsDelete_1.deleteFunctions(functionsToDelete, schedulesToDelete, topicsToDelete, appEngineLocation);
|
|
66
75
|
});
|
|
@@ -46,7 +46,7 @@ exports.default = new command_1.Command("functions:list")
|
|
|
46
46
|
return functionList;
|
|
47
47
|
}
|
|
48
48
|
catch (err) {
|
|
49
|
-
throw new error_1.FirebaseError(
|
|
49
|
+
throw new error_1.FirebaseError("Failed to list functions", {
|
|
50
50
|
exit: 1,
|
|
51
51
|
original: err,
|
|
52
52
|
});
|
package/lib/commands/index.js
CHANGED
|
@@ -26,6 +26,11 @@ module.exports = function (client) {
|
|
|
26
26
|
client.auth = {};
|
|
27
27
|
client.auth.export = loadCommand("auth-export");
|
|
28
28
|
client.auth.upload = loadCommand("auth-import");
|
|
29
|
+
if (previews.crashlyticsSymbolsUpload) {
|
|
30
|
+
client.crashlytics = {};
|
|
31
|
+
client.crashlytics.symbols = {};
|
|
32
|
+
client.crashlytics.symbols.upload = loadCommand("crashlytics-symbols-upload");
|
|
33
|
+
}
|
|
29
34
|
client.database = {};
|
|
30
35
|
client.database.get = loadCommand("database-get");
|
|
31
36
|
client.database.instances = {};
|
|
@@ -84,6 +89,9 @@ module.exports = function (client) {
|
|
|
84
89
|
client.functions = {};
|
|
85
90
|
client.functions.config = {};
|
|
86
91
|
client.functions.config.clone = loadCommand("functions-config-clone");
|
|
92
|
+
if (previews.dotenv) {
|
|
93
|
+
client.functions.config.export = loadCommand("functions-config-export");
|
|
94
|
+
}
|
|
87
95
|
client.functions.config.get = loadCommand("functions-config-get");
|
|
88
96
|
client.functions.config.set = loadCommand("functions-config-set");
|
|
89
97
|
client.functions.config.unset = loadCommand("functions-config-unset");
|
|
@@ -104,7 +104,7 @@ async function existingBackend(context, forceRefresh) {
|
|
|
104
104
|
}
|
|
105
105
|
exports.existingBackend = existingBackend;
|
|
106
106
|
async function loadExistingBackend(ctx) {
|
|
107
|
-
var _a, _b, _c;
|
|
107
|
+
var _a, _b, _c, _d;
|
|
108
108
|
ctx.loadedExistingBackend = true;
|
|
109
109
|
ctx.existingBackend = Object.assign({}, empty());
|
|
110
110
|
ctx.unreachableRegions = {
|
|
@@ -144,12 +144,21 @@ async function loadExistingBackend(ctx) {
|
|
|
144
144
|
if (!previews_1.previews.functionsv2) {
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
let gcfV2Results;
|
|
148
|
+
try {
|
|
149
|
+
gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (err.status === 404 && ((_b = err.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes("method not found"))) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
148
157
|
for (const apiFunction of gcfV2Results.functions) {
|
|
149
158
|
const specFunction = gcfV2.specFromFunction(apiFunction);
|
|
150
159
|
ctx.existingBackend.cloudFunctions.push(specFunction);
|
|
151
|
-
const pubsubScheduled = ((
|
|
152
|
-
const httpsScheduled = ((
|
|
160
|
+
const pubsubScheduled = ((_c = apiFunction.labels) === null || _c === void 0 ? void 0 : _c["deployment-scheduled"]) === "true";
|
|
161
|
+
const httpsScheduled = ((_d = apiFunction.labels) === null || _d === void 0 ? void 0 : _d["deployment-scheduled"]) === "https";
|
|
153
162
|
if (pubsubScheduled) {
|
|
154
163
|
const id = scheduleIdForFunction(specFunction);
|
|
155
164
|
ctx.existingBackend.schedules.push({
|
|
@@ -18,6 +18,7 @@ const runtimes = require("./runtimes");
|
|
|
18
18
|
const validate = require("./validate");
|
|
19
19
|
const utils = require("../../utils");
|
|
20
20
|
const logger_1 = require("../../logger");
|
|
21
|
+
const triggerRegionHelper_1 = require("./triggerRegionHelper");
|
|
21
22
|
function hasUserConfig(config) {
|
|
22
23
|
return Object.keys(config).length > 1;
|
|
23
24
|
}
|
|
@@ -70,6 +71,7 @@ async function prepare(context, options, payload) {
|
|
|
70
71
|
cloudrun: "run.googleapis.com",
|
|
71
72
|
eventarc: "eventarc.googleapis.com",
|
|
72
73
|
pubsub: "pubsub.googleapis.com",
|
|
74
|
+
storage: "storage.googleapis.com",
|
|
73
75
|
};
|
|
74
76
|
const enablements = Object.entries(V2_APIS).map(([tag, api]) => {
|
|
75
77
|
return ensureApiEnabled.ensure(context.projectId, api, tag);
|
|
@@ -100,6 +102,7 @@ async function prepare(context, options, payload) {
|
|
|
100
102
|
return functionsDeployHelper_1.functionMatchesAnyGroup(fn, context.filters);
|
|
101
103
|
});
|
|
102
104
|
const haveFunctions = (await backend.existingBackend(context)).cloudFunctions;
|
|
105
|
+
await triggerRegionHelper_1.setTriggerRegion(wantFunctions, haveFunctions);
|
|
103
106
|
await prompts_1.promptForFailurePolicies(options, wantFunctions, haveFunctions);
|
|
104
107
|
await prompts_1.promptForMinInstances(options, wantFunctions, haveFunctions);
|
|
105
108
|
await backend.checkAvailability(context, wantBackend);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addResourcesToBackend = exports.discoverBackend = exports.useStrategy = void 0;
|
|
3
|
+
exports.addResourcesToBackend = exports.discoverBackend = exports.useStrategy = exports.GCS_EVENTS = void 0;
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const _ = require("lodash");
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
@@ -10,6 +10,12 @@ const backend = require("../../backend");
|
|
|
10
10
|
const api = require("../../../../api");
|
|
11
11
|
const proto = require("../../../../gcp/proto");
|
|
12
12
|
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
|
+
]);
|
|
13
19
|
function removeInspectOptions(options) {
|
|
14
20
|
return options.filter((opt) => !opt.startsWith("--inspect"));
|
|
15
21
|
}
|
|
@@ -58,6 +64,7 @@ async function discoverBackend(projectId, sourceDir, runtime, configValues, envs
|
|
|
58
64
|
}
|
|
59
65
|
exports.discoverBackend = discoverBackend;
|
|
60
66
|
function addResourcesToBackend(projectId, runtime, annotation, want) {
|
|
67
|
+
var _a;
|
|
61
68
|
Object.freeze(annotation);
|
|
62
69
|
for (const region of annotation.regions || [api.functionsDefaultRegion]) {
|
|
63
70
|
let trigger;
|
|
@@ -79,6 +86,11 @@ function addResourcesToBackend(projectId, runtime, annotation, want) {
|
|
|
79
86
|
},
|
|
80
87
|
retry: !!annotation.failurePolicy,
|
|
81
88
|
};
|
|
89
|
+
if (exports.GCS_EVENTS.has(((_a = annotation.eventTrigger) === null || _a === void 0 ? void 0 : _a.eventType) || "")) {
|
|
90
|
+
trigger.eventFilters = {
|
|
91
|
+
bucket: annotation.eventTrigger.resource,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
82
94
|
}
|
|
83
95
|
const cloudFunctionName = {
|
|
84
96
|
id: annotation.name,
|