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.
Files changed (39) hide show
  1. package/CHANGELOG.md +1 -3
  2. package/lib/api.js +1 -0
  3. package/lib/commands/crashlytics-symbols-upload.js +146 -0
  4. package/lib/commands/ext-configure.js +2 -0
  5. package/lib/commands/ext-install.js +23 -2
  6. package/lib/commands/ext-uninstall.js +6 -0
  7. package/lib/commands/ext-update.js +9 -2
  8. package/lib/commands/functions-config-export.js +115 -0
  9. package/lib/commands/functions-delete.js +44 -35
  10. package/lib/commands/functions-list.js +1 -1
  11. package/lib/commands/index.js +8 -0
  12. package/lib/deploy/functions/backend.js +13 -4
  13. package/lib/deploy/functions/prepare.js +3 -0
  14. package/lib/deploy/functions/runtimes/node/parseTriggers.js +13 -1
  15. package/lib/deploy/functions/triggerRegionHelper.js +32 -0
  16. package/lib/downloadUtils.js +37 -0
  17. package/lib/emulator/auth/apiSpec.js +513 -1
  18. package/lib/emulator/auth/handlers.js +4 -3
  19. package/lib/emulator/auth/operations.js +144 -14
  20. package/lib/emulator/auth/server.js +10 -13
  21. package/lib/emulator/auth/state.js +132 -13
  22. package/lib/emulator/download.js +2 -31
  23. package/lib/emulator/functionsEmulatorRuntime.js +29 -7
  24. package/lib/extensions/askUserForConsent.js +14 -1
  25. package/lib/extensions/askUserForParam.js +72 -3
  26. package/lib/extensions/extensionsApi.js +1 -0
  27. package/lib/extensions/extensionsHelper.js +1 -0
  28. package/lib/extensions/paramHelper.js +3 -2
  29. package/lib/extensions/secretsUtils.js +58 -0
  30. package/lib/functional.js +8 -1
  31. package/lib/functions/env.js +10 -4
  32. package/lib/functions/runtimeConfigExport.js +137 -0
  33. package/lib/gcp/cloudfunctions.js +4 -2
  34. package/lib/gcp/cloudfunctionsv2.js +6 -0
  35. package/lib/gcp/secretManager.js +111 -0
  36. package/lib/gcp/storage.js +16 -0
  37. package/lib/previews.js +1 -1
  38. package/lib/requireInteractive.js +12 -0
  39. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1 @@
1
- - `ext:dev:publish` and `ext:update` now support --force and --non-interactive flags.
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
- if (spec.billingRequired) {
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
- if (newSpec.billingRequired) {
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, existingParams)) {
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
- const [config, existingBackend] = await Promise.all([
30
- functionsConfig.getFirebaseConfig(options),
31
- backend.existingBackend(context),
32
- ]);
33
- await backend.checkAvailability(context, backend.empty());
34
- const appEngineLocation = functionsConfig.getAppEngineLocation(config);
35
- const functionsToDelete = existingBackend.cloudFunctions.filter((fn) => {
36
- const regionMatches = options.region ? fn.region === options.region : true;
37
- const nameMatches = helper.functionMatchesAnyGroup(fn, filterChunks);
38
- return regionMatches && nameMatches;
39
- });
40
- if (functionsToDelete.length === 0) {
41
- return utils.reject(`The specified filters do not match any existing functions in project ${clc.bold(context.projectId)}.`, { exit: 1 });
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
- const schedulesToDelete = existingBackend.schedules.filter((schedule) => {
44
- functionsToDelete.some(backend.sameFunctionName(schedule.targetService));
45
- });
46
- const topicsToDelete = existingBackend.topics.filter((topic) => {
47
- functionsToDelete.some(backend.sameFunctionName(topic.targetService));
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(`Failed to list functions ${err.message}`, {
49
+ throw new error_1.FirebaseError("Failed to list functions", {
50
50
  exit: 1,
51
51
  original: err,
52
52
  });
@@ -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
- const gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
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 = ((_b = apiFunction.labels) === null || _b === void 0 ? void 0 : _b["deployment-scheduled"]) === "true";
152
- const httpsScheduled = ((_c = apiFunction.labels) === null || _c === void 0 ? void 0 : _c["deployment-scheduled"]) === "https";
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,