firebase-tools 13.35.1 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/lib/appdistribution/client.js +4 -2
  2. package/lib/apphosting/backend.js +65 -11
  3. package/lib/apphosting/config.js +130 -101
  4. package/lib/apphosting/rollout.js +3 -9
  5. package/lib/apphosting/secrets/dialogs.js +5 -2
  6. package/lib/apphosting/secrets/index.js +45 -3
  7. package/lib/apphosting/yaml.js +19 -8
  8. package/lib/commands/appdistribution-groups-create.js +1 -1
  9. package/lib/commands/appdistribution-groups-delete.js +1 -1
  10. package/lib/commands/appdistribution-groups-list.js +1 -1
  11. package/lib/commands/appdistribution-testers-add.js +1 -1
  12. package/lib/commands/appdistribution-testers-remove.js +1 -1
  13. package/lib/commands/apphosting-backends-create.js +1 -8
  14. package/lib/commands/apphosting-backends-delete.js +16 -26
  15. package/lib/commands/apphosting-backends-get.js +10 -16
  16. package/lib/commands/apphosting-backends-list.js +4 -10
  17. package/lib/commands/apphosting-rollouts-create.js +1 -8
  18. package/lib/commands/apphosting-secrets-access.js +1 -1
  19. package/lib/commands/apphosting-secrets-describe.js +1 -1
  20. package/lib/commands/apphosting-secrets-grantaccess.js +19 -9
  21. package/lib/commands/apphosting-secrets-set.js +31 -1
  22. package/lib/commands/apps-android-sha-create.js +1 -1
  23. package/lib/commands/apps-android-sha-delete.js +1 -1
  24. package/lib/commands/apps-android-sha-list.js +1 -1
  25. package/lib/commands/apps-create.js +1 -1
  26. package/lib/commands/apps-init.js +1 -1
  27. package/lib/commands/auth-export.js +1 -1
  28. package/lib/commands/auth-import.js +1 -1
  29. package/lib/commands/database-instances-create.js +1 -1
  30. package/lib/commands/database-profile.js +1 -2
  31. package/lib/commands/database-settings-set.js +1 -1
  32. package/lib/commands/database-update.js +1 -1
  33. package/lib/commands/dataconnect-sdk-generate.js +1 -1
  34. package/lib/commands/dataconnect-services-list.js +1 -1
  35. package/lib/commands/dataconnect-sql-diff.js +1 -1
  36. package/lib/commands/dataconnect-sql-grant.js +1 -1
  37. package/lib/commands/dataconnect-sql-migrate.js +2 -2
  38. package/lib/commands/dataconnect-sql-setup.js +1 -1
  39. package/lib/commands/dataconnect-sql-shell.js +1 -1
  40. package/lib/commands/deploy.js +3 -3
  41. package/lib/commands/emulators-exec.js +1 -1
  42. package/lib/commands/ext-dev-register.js +1 -1
  43. package/lib/commands/ext-dev-usage.js +2 -2
  44. package/lib/commands/ext-install.js +2 -3
  45. package/lib/commands/firestore-backups-delete.js +2 -2
  46. package/lib/commands/firestore-backups-get.js +1 -1
  47. package/lib/commands/firestore-backups-list.js +2 -2
  48. package/lib/commands/firestore-backups-schedules-create.js +4 -4
  49. package/lib/commands/firestore-backups-schedules-delete.js +2 -2
  50. package/lib/commands/firestore-backups-schedules-list.js +2 -2
  51. package/lib/commands/firestore-backups-schedules-update.js +1 -1
  52. package/lib/commands/firestore-databases-create.js +6 -6
  53. package/lib/commands/firestore-databases-delete.js +2 -2
  54. package/lib/commands/firestore-databases-get.js +1 -1
  55. package/lib/commands/firestore-databases-list.js +1 -1
  56. package/lib/commands/firestore-databases-restore.js +5 -5
  57. package/lib/commands/firestore-databases-update.js +4 -4
  58. package/lib/commands/firestore-delete.js +7 -8
  59. package/lib/commands/firestore-indexes-list.js +4 -4
  60. package/lib/commands/firestore-locations.js +1 -1
  61. package/lib/commands/functions-artifacts-setpolicy.js +7 -7
  62. package/lib/commands/functions-config-export.js +1 -1
  63. package/lib/commands/functions-deletegcfartifacts.js +1 -1
  64. package/lib/commands/functions-secrets-access.js +1 -1
  65. package/lib/commands/functions-secrets-describe.js +1 -1
  66. package/lib/commands/functions-secrets-destroy.js +2 -2
  67. package/lib/commands/functions-secrets-get.js +1 -1
  68. package/lib/commands/functions-secrets-prune.js +2 -2
  69. package/lib/commands/functions-secrets-set.js +3 -3
  70. package/lib/commands/index.js +0 -4
  71. package/lib/commands/init.js +3 -2
  72. package/lib/commands/internaltesting-frameworks-compose.js +1 -1
  73. package/lib/commands/remoteconfig-versions-list.js +1 -1
  74. package/lib/commands/setup-emulators-database.js +1 -1
  75. package/lib/commands/setup-emulators-dataconnect.js +1 -1
  76. package/lib/commands/setup-emulators-firestore.js +1 -1
  77. package/lib/commands/setup-emulators-pubsub.js +1 -1
  78. package/lib/commands/setup-emulators-storage.js +1 -1
  79. package/lib/commands/setup-emulators-ui.js +1 -1
  80. package/lib/config.js +16 -18
  81. package/lib/dataconnect/dataplaneClient.js +3 -1
  82. package/lib/deploy/functions/build.js +1 -0
  83. package/lib/deploy/functions/prompts.js +30 -1
  84. package/lib/deploy/functions/release/index.js +27 -9
  85. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +4 -4
  86. package/lib/emulator/apphosting/config.js +15 -14
  87. package/lib/emulator/auth/operations.js +2 -1
  88. package/lib/emulator/constants.js +1 -1
  89. package/lib/emulator/dataconnect/pgliteServer.js +2 -1
  90. package/lib/emulator/dataconnectEmulator.js +2 -0
  91. package/lib/emulator/downloadableEmulators.js +9 -9
  92. package/lib/emulator/env.js +2 -1
  93. package/lib/emulator/initEmulators.js +29 -4
  94. package/lib/experiments.js +1 -13
  95. package/lib/functions/artifacts.js +88 -1
  96. package/lib/gcp/cloudfunctions.js +1 -1
  97. package/lib/gcp/cloudfunctionsv2.js +3 -3
  98. package/lib/gcp/cloudsql/permissions.js +2 -1
  99. package/lib/gcp/cloudsql/permissions_setup.js +8 -5
  100. package/lib/gcp/proto.js +4 -3
  101. package/lib/init/features/dataconnect/sdk.js +4 -5
  102. package/package.json +2 -2
  103. package/standalone/package.json +1 -1
  104. package/templates/init/dataconnect/dataconnect.yaml +1 -1
  105. package/lib/commands/apphosting-config-export.js +0 -29
  106. package/lib/commands/experimental-functions-shell.js +0 -13
@@ -99,6 +99,7 @@ class AppDistributionClient {
99
99
  utils.logSuccess("distributed to testers/groups successfully");
100
100
  }
101
101
  async listTesters(projectName, groupName) {
102
+ var _a;
102
103
  const listTestersResponse = {
103
104
  testers: [],
104
105
  };
@@ -119,7 +120,7 @@ class AppDistributionClient {
119
120
  catch (err) {
120
121
  throw new error_1.FirebaseError(`Client request failed to list testers ${err}`);
121
122
  }
122
- for (const t of apiResponse.body.testers) {
123
+ for (const t of (_a = apiResponse.body.testers) !== null && _a !== void 0 ? _a : []) {
123
124
  listTestersResponse.testers.push({
124
125
  name: t.name,
125
126
  displayName: t.displayName,
@@ -159,6 +160,7 @@ class AppDistributionClient {
159
160
  return apiResponse.body;
160
161
  }
161
162
  async listGroups(projectName) {
163
+ var _a;
162
164
  const listGroupsResponse = {
163
165
  groups: [],
164
166
  };
@@ -170,7 +172,7 @@ class AppDistributionClient {
170
172
  const apiResponse = await client.get(`${projectName}/groups`, {
171
173
  queryParams,
172
174
  });
173
- listGroupsResponse.groups.push(...(apiResponse.body.groups || []));
175
+ listGroupsResponse.groups.push(...((_a = apiResponse.body.groups) !== null && _a !== void 0 ? _a : []));
174
176
  pageToken = apiResponse.body.nextPageToken;
175
177
  }
176
178
  catch (err) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getBackendForAmbiguousLocation = exports.getBackendForLocation = exports.promptLocation = exports.deleteBackendAndPoll = exports.setDefaultTrafficPolicy = exports.createBackend = exports.ensureAppHostingComputeServiceAccount = exports.createGitRepoLink = exports.doSetup = void 0;
3
+ exports.getBackend = exports.getBackendForAmbiguousLocation = exports.chooseBackends = exports.getBackendForLocation = exports.promptLocation = exports.deleteBackendAndPoll = exports.setDefaultTrafficPolicy = exports.createBackend = exports.ensureAppHostingComputeServiceAccount = exports.createGitRepoLink = exports.doSetup = void 0;
4
4
  const clc = require("colorette");
5
5
  const poller = require("../operation-poller");
6
6
  const apphosting = require("../gcp/apphosting");
@@ -50,7 +50,7 @@ async function awaitTlsReady(url) {
50
50
  }
51
51
  } while (!ready);
52
52
  }
53
- async function doSetup(projectId, webAppName, location, serviceAccount) {
53
+ async function doSetup(projectId, webAppName, serviceAccount) {
54
54
  await Promise.all([
55
55
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true),
56
56
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudbuildOrigin)(), "apphosting", true),
@@ -60,14 +60,7 @@ async function doSetup(projectId, webAppName, location, serviceAccount) {
60
60
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.iamOrigin)(), "apphosting", true),
61
61
  ]);
62
62
  await ensureAppHostingComputeServiceAccount(projectId, serviceAccount);
63
- const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
64
- if (location) {
65
- if (!allowedLocations.includes(location)) {
66
- throw new error_1.FirebaseError(`Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`);
67
- }
68
- }
69
- location =
70
- location || (await promptLocation(projectId, "Select a location to host your backend:\n"));
63
+ const location = await promptLocation(projectId, "Select a primary region to host your backend:\n");
71
64
  const gitRepositoryLink = await githubConnections.linkGitHubRepository(projectId, location);
72
65
  const rootDir = await (0, prompt_1.promptOnce)({
73
66
  name: "rootDir",
@@ -259,10 +252,53 @@ async function getBackendForLocation(projectId, location, backendId) {
259
252
  }
260
253
  }
261
254
  exports.getBackendForLocation = getBackendForLocation;
255
+ async function chooseBackends(projectId, backendId, chooseBackendPrompt, force) {
256
+ let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
257
+ if (unreachable && unreachable.length !== 0) {
258
+ (0, utils_1.logWarning)(`The following locations are currently unreachable: ${unreachable.join(",")}.\n` +
259
+ "If your backend is in one of these regions, please try again later.");
260
+ }
261
+ backends = backends.filter((backend) => apphosting.parseBackendName(backend.name).id === backendId);
262
+ if (backends.length === 0) {
263
+ throw new error_1.FirebaseError(`No backend named "${backendId}" found.`);
264
+ }
265
+ if (backends.length === 1) {
266
+ return backends;
267
+ }
268
+ if (force) {
269
+ throw new error_1.FirebaseError(`Force cannot be used because multiple backends were found with ID ${backendId}.`);
270
+ }
271
+ const backendsByDisplay = new Map();
272
+ backends.forEach((backend) => {
273
+ const { location, id } = apphosting.parseBackendName(backend.name);
274
+ backendsByDisplay.set(`${id}(${location})`, backend);
275
+ });
276
+ const chosenBackendDisplays = await (0, prompt_1.promptOnce)({
277
+ name: "backend",
278
+ type: "checkbox",
279
+ message: chooseBackendPrompt,
280
+ choices: Array.from(backendsByDisplay.keys(), (name) => {
281
+ return {
282
+ checked: false,
283
+ name: name,
284
+ value: name,
285
+ };
286
+ }),
287
+ });
288
+ const chosenBackends = [];
289
+ chosenBackendDisplays.forEach((backendDisplay) => {
290
+ const backend = backendsByDisplay.get(backendDisplay);
291
+ if (backend !== undefined) {
292
+ chosenBackends.push(backend);
293
+ }
294
+ });
295
+ return chosenBackends;
296
+ }
297
+ exports.chooseBackends = chooseBackends;
262
298
  async function getBackendForAmbiguousLocation(projectId, backendId, locationDisambugationPrompt, force) {
263
299
  let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
264
300
  if (unreachable && unreachable.length !== 0) {
265
- (0, utils_1.logWarning)(`The following locations are currently unreachable: ${unreachable}.\n` +
301
+ (0, utils_1.logWarning)(`The following locations are currently unreachable: ${unreachable.join(", ")}.\n` +
266
302
  "If your backend is in one of these regions, please try again later.");
267
303
  }
268
304
  backends = backends.filter((backend) => apphosting.parseBackendName(backend.name).id === backendId);
@@ -286,3 +322,21 @@ async function getBackendForAmbiguousLocation(projectId, backendId, locationDisa
286
322
  return backendsByLocation.get(location);
287
323
  }
288
324
  exports.getBackendForAmbiguousLocation = getBackendForAmbiguousLocation;
325
+ async function getBackend(projectId, backendId) {
326
+ let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
327
+ backends = backends.filter((backend) => apphosting.parseBackendName(backend.name).id === backendId);
328
+ if (backends.length > 1) {
329
+ const locations = backends.map((b) => apphosting.parseBackendName(b.name).location);
330
+ throw new error_1.FirebaseError(`You have multiple backends with the same ${backendId} ID in regions: ${locations.join(", ")}. This is not allowed until we can support more locations. ` +
331
+ "Please delete and recreate any backends that share an ID with another backend.");
332
+ }
333
+ if (backends.length === 1) {
334
+ return backends[0];
335
+ }
336
+ if (unreachable && unreachable.length !== 0) {
337
+ (0, utils_1.logWarning)(`Backends with the following primary regions are unreachable: ${unreachable.join(", ")}.\n` +
338
+ "If your backend is in one of these regions, please try again later.");
339
+ }
340
+ throw new error_1.FirebaseError(`No backend named ${backendId} found.`);
341
+ }
342
+ exports.getBackend = getBackend;
@@ -1,28 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadConfigToExportSecrets = exports.loadConfigForEnvironment = exports.exportConfig = exports.maybeAddSecretToYaml = exports.upsertEnv = exports.findEnv = exports.store = exports.load = exports.listAppHostingFilesInPath = exports.discoverBackendRoot = exports.APPHOSTING_YAML_FILE_REGEX = exports.APPHOSTING_LOCAL_YAML_FILE = exports.APPHOSTING_BASE_YAML_FILE = void 0;
3
+ exports.suggestedTestKeyName = exports.overrideChosenEnv = exports.maybeGenerateEmulatorYaml = exports.maybeAddSecretToYaml = exports.upsertEnv = exports.findEnv = exports.store = exports.load = exports.listAppHostingFilesInPath = exports.discoverBackendRoot = exports.APPHOSTING_YAML_FILE_REGEX = exports.APPHOSTING_LOCAL_YAML_FILE = exports.APPHOSTING_EMULATORS_YAML_FILE = exports.APPHOSTING_BASE_YAML_FILE = void 0;
4
4
  const path_1 = require("path");
5
5
  const fs_1 = require("fs");
6
6
  const yaml = require("yaml");
7
+ const clc = require("colorette");
7
8
  const fs = require("../fsutils");
8
9
  const prompt = require("../prompt");
9
10
  const dialogs = require("./secrets/dialogs");
10
11
  const yaml_1 = require("./yaml");
11
- const error_1 = require("../error");
12
- const utils_1 = require("./utils");
13
- const secrets_1 = require("./secrets");
14
12
  const logger_1 = require("../logger");
15
- const utils_2 = require("../utils");
16
- const projects_1 = require("../management/projects");
13
+ const csm = require("../gcp/secretManager");
14
+ const error_1 = require("../error");
17
15
  exports.APPHOSTING_BASE_YAML_FILE = "apphosting.yaml";
16
+ exports.APPHOSTING_EMULATORS_YAML_FILE = "apphosting.emulator.yaml";
18
17
  exports.APPHOSTING_LOCAL_YAML_FILE = "apphosting.local.yaml";
19
18
  exports.APPHOSTING_YAML_FILE_REGEX = /^apphosting(\.[a-z0-9_]+)?\.yaml$/;
20
- const SECRET_CONFIG = "Secret";
21
- const EXPORTABLE_CONFIG = [SECRET_CONFIG];
22
19
  function discoverBackendRoot(cwd) {
23
20
  let dir = cwd;
24
- while (!fs.fileExistsSync((0, path_1.resolve)(dir, exports.APPHOSTING_BASE_YAML_FILE))) {
25
- if (fs.fileExistsSync((0, path_1.resolve)(dir, "firebase.json"))) {
21
+ while (true) {
22
+ const files = fs.listFiles(dir);
23
+ if (files.some((file) => exports.APPHOSTING_YAML_FILE_REGEX.test(file))) {
24
+ return dir;
25
+ }
26
+ if (files.includes("firebase.json")) {
26
27
  return null;
27
28
  }
28
29
  const parent = (0, path_1.dirname)(dir);
@@ -31,7 +32,6 @@ function discoverBackendRoot(cwd) {
31
32
  }
32
33
  dir = parent;
33
34
  }
34
- return dir;
35
35
  }
36
36
  exports.discoverBackendRoot = discoverBackendRoot;
37
37
  function listAppHostingFilesInPath(path) {
@@ -42,7 +42,18 @@ function listAppHostingFilesInPath(path) {
42
42
  }
43
43
  exports.listAppHostingFilesInPath = listAppHostingFilesInPath;
44
44
  function load(yamlPath) {
45
- const raw = fs.readFile(yamlPath);
45
+ let raw;
46
+ try {
47
+ raw = fs.readFile(yamlPath);
48
+ }
49
+ catch (err) {
50
+ if (err.code !== "ENOENT") {
51
+ throw new error_1.FirebaseError(`Unexpected error trying to load ${yamlPath}`, {
52
+ original: (0, error_1.getError)(err),
53
+ });
54
+ }
55
+ return new yaml.Document();
56
+ }
46
57
  return yaml.parseDocument(raw);
47
58
  }
48
59
  exports.load = load;
@@ -79,13 +90,13 @@ function upsertEnv(document, env) {
79
90
  envs.add(envYaml);
80
91
  }
81
92
  exports.upsertEnv = upsertEnv;
82
- async function maybeAddSecretToYaml(secretName) {
83
- const dynamicDispatch = exports;
93
+ const dynamicDispatch = exports;
94
+ async function maybeAddSecretToYaml(secretName, fileName = exports.APPHOSTING_BASE_YAML_FILE) {
84
95
  const backendRoot = dynamicDispatch.discoverBackendRoot(process.cwd());
85
96
  let path;
86
97
  let projectYaml;
87
98
  if (backendRoot) {
88
- path = (0, path_1.join)(backendRoot, exports.APPHOSTING_BASE_YAML_FILE);
99
+ path = (0, path_1.join)(backendRoot, fileName);
89
100
  projectYaml = dynamicDispatch.load(path);
90
101
  }
91
102
  else {
@@ -95,7 +106,7 @@ async function maybeAddSecretToYaml(secretName) {
95
106
  return;
96
107
  }
97
108
  const addToYaml = await prompt.confirm({
98
- message: "Would you like to add this secret to apphosting.yaml?",
109
+ message: `Would you like to add this secret to ${fileName}?`,
99
110
  default: true,
100
111
  });
101
112
  if (!addToYaml) {
@@ -103,12 +114,12 @@ async function maybeAddSecretToYaml(secretName) {
103
114
  }
104
115
  if (!path) {
105
116
  path = await prompt.promptOnce({
106
- message: "It looks like you don't have an apphosting.yaml yet. Where would you like to store it?",
117
+ message: `It looks like you don't have an ${fileName} yet. Where would you like to store it?`,
107
118
  default: process.cwd(),
108
119
  });
109
- path = (0, path_1.join)(path, exports.APPHOSTING_BASE_YAML_FILE);
120
+ path = (0, path_1.join)(path, fileName);
110
121
  }
111
- const envName = await dialogs.envVarForSecret(secretName);
122
+ const envName = await dialogs.envVarForSecret(secretName, fileName === exports.APPHOSTING_EMULATORS_YAML_FILE);
112
123
  dynamicDispatch.upsertEnv(projectYaml, {
113
124
  variable: envName,
114
125
  secret: secretName,
@@ -116,92 +127,110 @@ async function maybeAddSecretToYaml(secretName) {
116
127
  dynamicDispatch.store(path, projectYaml);
117
128
  }
118
129
  exports.maybeAddSecretToYaml = maybeAddSecretToYaml;
119
- async function exportConfig(cwd, projectRoot, backendRoot, projectId, userGivenConfigFile) {
120
- const choices = await prompt.prompt({}, [
121
- {
122
- type: "checkbox",
123
- name: "configurations",
124
- message: "What configs would you like to export?",
125
- choices: EXPORTABLE_CONFIG,
126
- },
127
- ]);
128
- if (!choices.configurations.includes(SECRET_CONFIG)) {
129
- logger_1.logger.info("No configs selected to export");
130
- return;
131
- }
132
- if (!projectId) {
133
- const project = await (0, projects_1.getOrPromptProject)({});
134
- projectId = project.projectId;
135
- }
136
- let localAppHostingConfig = yaml_1.AppHostingYamlConfig.empty();
137
- const localAppHostingConfigPath = (0, path_1.resolve)(backendRoot, exports.APPHOSTING_LOCAL_YAML_FILE);
138
- if (fs.fileExistsSync(localAppHostingConfigPath)) {
139
- localAppHostingConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(localAppHostingConfigPath);
140
- }
141
- const configToExport = await loadConfigToExportSecrets(cwd, userGivenConfigFile);
142
- const secretsToExport = Object.entries(configToExport.env)
143
- .filter(([, env]) => env.secret)
144
- .map(([variable, env]) => {
145
- return Object.assign({ variable }, env);
130
+ async function maybeGenerateEmulatorYaml(projectId, repoRoot) {
131
+ const basePath = dynamicDispatch.discoverBackendRoot(repoRoot) || repoRoot;
132
+ if (fs.fileExistsSync((0, path_1.join)(basePath, exports.APPHOSTING_EMULATORS_YAML_FILE))) {
133
+ logger_1.logger.debug("apphosting.emulator.yaml already exists, skipping generation and secrets access prompt");
134
+ return null;
135
+ }
136
+ let baseConfig;
137
+ try {
138
+ baseConfig = await yaml_1.AppHostingYamlConfig.loadFromFile((0, path_1.join)(basePath, exports.APPHOSTING_BASE_YAML_FILE));
139
+ }
140
+ catch (_a) {
141
+ baseConfig = yaml_1.AppHostingYamlConfig.empty();
142
+ }
143
+ const createFile = await prompt.confirm({
144
+ message: "The App Hosting emulator uses a file called apphosting.emulator.yaml to override " +
145
+ "values in apphosting.yaml for local testing. This codebase does not have one, would you like " +
146
+ "to create it?",
147
+ default: true,
146
148
  });
147
- if (!secretsToExport) {
148
- logger_1.logger.info("No secrets found to export in the chosen App Hosting config files");
149
- return;
150
- }
151
- const secretMaterial = await (0, secrets_1.fetchSecrets)(projectId, secretsToExport);
152
- for (const [key, value] of secretMaterial) {
153
- localAppHostingConfig.env[key] = {
154
- value,
155
- availability: ["RUNTIME"],
156
- };
157
- }
158
- localAppHostingConfig.upsertFile(localAppHostingConfigPath);
159
- logger_1.logger.info(`Wrote secrets as environment variables to ${exports.APPHOSTING_LOCAL_YAML_FILE}.`);
160
- (0, utils_2.updateOrCreateGitignore)(projectRoot, [exports.APPHOSTING_LOCAL_YAML_FILE]);
161
- logger_1.logger.info(`${exports.APPHOSTING_LOCAL_YAML_FILE} has been automatically added to your .gitignore.`);
162
- }
163
- exports.exportConfig = exportConfig;
164
- async function loadConfigForEnvironment(envYamlPath, baseYamlPath) {
165
- const envYamlConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(envYamlPath);
166
- if (baseYamlPath) {
167
- const baseConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(baseYamlPath);
168
- baseConfig.merge(envYamlConfig);
169
- return baseConfig;
170
- }
171
- return envYamlConfig;
172
- }
173
- exports.loadConfigForEnvironment = loadConfigForEnvironment;
174
- async function loadConfigToExportSecrets(cwd, userGivenConfigFile) {
175
- if (userGivenConfigFile && !exports.APPHOSTING_YAML_FILE_REGEX.test(userGivenConfigFile)) {
176
- throw new error_1.FirebaseError("Invalid apphosting yaml config file provided. File must be in format: 'apphosting.yaml' or 'apphosting.<environment>.yaml'");
177
- }
178
- const allConfigs = getValidConfigs(cwd);
179
- let userGivenConfigFilePath;
180
- if (userGivenConfigFile) {
181
- if (!allConfigs.has(userGivenConfigFile)) {
182
- throw new error_1.FirebaseError(`The provided app hosting config file "${userGivenConfigFile}" does not exist`);
149
+ if (!createFile) {
150
+ return (0, yaml_1.toEnvList)(baseConfig.env);
151
+ }
152
+ const newEnv = await dynamicDispatch.overrideChosenEnv(projectId, baseConfig.env || {});
153
+ const envList = Object.entries(newEnv);
154
+ if (envList.length) {
155
+ const newYaml = new yaml.Document();
156
+ for (const [variable, env] of envList) {
157
+ dynamicDispatch.upsertEnv(newYaml, Object.assign({ variable }, env));
183
158
  }
184
- userGivenConfigFilePath = allConfigs.get(userGivenConfigFile);
159
+ dynamicDispatch.store((0, path_1.join)(basePath, exports.APPHOSTING_EMULATORS_YAML_FILE), newYaml);
185
160
  }
186
161
  else {
187
- userGivenConfigFilePath = await (0, utils_1.promptForAppHostingYaml)(allConfigs, "Which environment would you like to export secrets from Secret Manager for?");
188
- }
189
- if (userGivenConfigFile === exports.APPHOSTING_BASE_YAML_FILE) {
190
- return yaml_1.AppHostingYamlConfig.loadFromFile(allConfigs.get(exports.APPHOSTING_BASE_YAML_FILE));
162
+ const sample = "env:\n" +
163
+ "#- variable: ENV_VAR_NAME\n" +
164
+ "# value: plaintext value\n" +
165
+ "#- variable: SECRET_ENV_VAR_NAME\n" +
166
+ "# secret: cloud-secret-manager-id\n";
167
+ (0, fs_1.writeFileSync)((0, path_1.join)(basePath, exports.APPHOSTING_EMULATORS_YAML_FILE), sample);
168
+ }
169
+ return (0, yaml_1.toEnvList)(Object.assign(Object.assign({}, baseConfig.env), newEnv));
170
+ }
171
+ exports.maybeGenerateEmulatorYaml = maybeGenerateEmulatorYaml;
172
+ async function overrideChosenEnv(projectId, env) {
173
+ const names = Object.keys(env);
174
+ if (!names.length) {
175
+ return {};
176
+ }
177
+ const toOverwrite = await prompt.promptOnce({
178
+ type: "checkbox",
179
+ message: "Which environment variables would you like to override?",
180
+ choices: names.map((name) => {
181
+ return { name };
182
+ }),
183
+ });
184
+ if (!projectId && toOverwrite.some((name) => "secret" in env[name])) {
185
+ throw new error_1.FirebaseError(`Need a project ID to overwrite a secret. Either use ${clc.bold("firebase use")} or pass the ${clc.bold("--project")} flag`);
186
+ }
187
+ const newEnv = {};
188
+ for (const name of toOverwrite) {
189
+ if ("value" in env[name]) {
190
+ const newValue = await prompt.promptOnce({
191
+ type: "input",
192
+ message: `What new value would you like for plaintext ${name}?`,
193
+ });
194
+ newEnv[name] = { variable: name, value: newValue };
195
+ continue;
196
+ }
197
+ let secretRef;
198
+ let action = "pick-new";
199
+ while (action === "pick-new") {
200
+ secretRef = await prompt.promptOnce({
201
+ type: "input",
202
+ message: `What would you like to name the secret reference for ${name}?`,
203
+ default: suggestedTestKeyName(name),
204
+ });
205
+ if (await csm.secretExists(projectId, secretRef)) {
206
+ action = await prompt.promptOnce({
207
+ type: "list",
208
+ message: "This secret reference already exists, would you like to reuse it or create a new one?",
209
+ choices: [
210
+ { name: "Reuse it", value: "reuse" },
211
+ { name: "Create a new one", value: "pick-new" },
212
+ ],
213
+ });
214
+ }
215
+ else {
216
+ action = "create";
217
+ }
218
+ }
219
+ newEnv[name] = { variable: name, secret: secretRef };
220
+ if (action === "reuse") {
221
+ continue;
222
+ }
223
+ const secretValue = await prompt.promptOnce({
224
+ type: "password",
225
+ message: `What new value would you like for secret ${name} [input is hidden]?`,
226
+ });
227
+ await csm.createSecret(projectId, secretRef, { [csm.FIREBASE_MANAGED]: "apphosting" });
228
+ await csm.addVersion(projectId, secretRef, secretValue);
191
229
  }
192
- const baseFilePath = allConfigs.get(exports.APPHOSTING_BASE_YAML_FILE);
193
- return await loadConfigForEnvironment(userGivenConfigFilePath, baseFilePath);
230
+ return newEnv;
194
231
  }
195
- exports.loadConfigToExportSecrets = loadConfigToExportSecrets;
196
- function getValidConfigs(cwd) {
197
- const appHostingConfigPaths = listAppHostingFilesInPath(cwd).filter((path) => !path.endsWith(exports.APPHOSTING_LOCAL_YAML_FILE));
198
- if (appHostingConfigPaths.length === 0) {
199
- throw new error_1.FirebaseError("No apphosting.*.yaml configs found");
200
- }
201
- const fileNameToPathMap = new Map();
202
- for (const path of appHostingConfigPaths) {
203
- const fileName = (0, path_1.basename)(path);
204
- fileNameToPathMap.set(fileName, path);
205
- }
206
- return fileNameToPathMap;
232
+ exports.overrideChosenEnv = overrideChosenEnv;
233
+ function suggestedTestKeyName(variable) {
234
+ return "test-" + variable.replace(/_/g, "-").toLowerCase();
207
235
  }
236
+ exports.suggestedTestKeyName = suggestedTestKeyName;
@@ -17,18 +17,12 @@ const apphostingPollerOptions = {
17
17
  maxBackoff: 10000,
18
18
  };
19
19
  const GIT_COMMIT_SHA_REGEX = /^(?:[0-9a-f]{40}|[0-9a-f]{7})$/;
20
- async function createRollout(backendId, projectId, location, branch, commit, force) {
21
- let backend;
22
- if (location === "-" || location === "") {
23
- backend = await (0, backend_1.getBackendForAmbiguousLocation)(projectId, backendId, "Please select the location of the backend you'd like to roll out:", force);
24
- location = apphosting.parseBackendName(backend.name).location;
25
- }
26
- else {
27
- backend = await (0, backend_1.getBackendForLocation)(projectId, location, backendId);
28
- }
20
+ async function createRollout(backendId, projectId, branch, commit, force) {
21
+ const backend = await (0, backend_1.getBackend)(projectId, backendId);
29
22
  if (!backend.codebase.repository) {
30
23
  throw new error_1.FirebaseError(`Backend ${backendId} is misconfigured due to missing a connected repository. You can delete and recreate your backend using 'firebase apphosting:backends:delete' and 'firebase apphosting:backends:create'.`);
31
24
  }
25
+ const { location } = apphosting.parseBackendName(backend.name);
32
26
  const { repoLink, owner, repo, readToken } = await (0, devConnect_1.getRepoDetailsFromBackend)(projectId, location, backend.codebase.repository);
33
27
  let targetCommit;
34
28
  if (branch) {
@@ -143,8 +143,11 @@ function toUpperSnakeCase(key) {
143
143
  .replace(/([a-z])([A-Z])/g, "$1_$2")
144
144
  .toUpperCase();
145
145
  }
146
- async function envVarForSecret(secret) {
147
- const upper = toUpperSnakeCase(secret);
146
+ async function envVarForSecret(secret, trimTestPrefix = false) {
147
+ let upper = toUpperSnakeCase(secret);
148
+ if (trimTestPrefix && upper.startsWith("TEST_")) {
149
+ upper = upper.substring("TEST_".length);
150
+ }
148
151
  if (upper === secret) {
149
152
  try {
150
153
  env.validateKey(secret);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSecretNameParts = exports.fetchSecrets = exports.upsertSecret = exports.grantSecretAccess = exports.serviceAccountsForBackend = exports.toMulti = void 0;
3
+ exports.getSecretNameParts = exports.fetchSecrets = exports.upsertSecret = exports.grantEmailsSecretAccess = exports.grantSecretAccess = exports.serviceAccountsForBackend = exports.toMulti = void 0;
4
4
  const error_1 = require("../../error");
5
5
  const gcsm = require("../../gcp/secretManager");
6
6
  const gcb = require("../../gcp/cloudbuild");
@@ -57,16 +57,58 @@ async function grantSecretAccess(projectId, projectNumber, secretName, accounts)
57
57
  catch (err) {
58
58
  throw new error_1.FirebaseError(`Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, { original: (0, error_1.getError)(err) });
59
59
  }
60
+ const updatedBindings = existingBindings.concat(newBindings);
60
61
  try {
61
- const updatedBindings = existingBindings.concat(newBindings);
62
62
  await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings);
63
63
  }
64
64
  catch (err) {
65
- throw new error_1.FirebaseError(`Failed to set IAM bindings ${JSON.stringify(newBindings)} on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, { original: (0, error_1.getError)(err) });
65
+ throw new error_1.FirebaseError(`Failed to set IAM bindings ${JSON.stringify(newBindings)} on secret: ${secretName}. Ensure you have the permissions to do so and try again. ` +
66
+ "For more information visit https://cloud.google.com/secret-manager/docs/manage-access-to-secrets#required-roles", { original: (0, error_1.getError)(err) });
66
67
  }
67
68
  utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`);
68
69
  }
69
70
  exports.grantSecretAccess = grantSecretAccess;
71
+ async function grantEmailsSecretAccess(projectId, secretNames, emails) {
72
+ const typeGuesses = Object.fromEntries(emails.map((email) => [email, "user"]));
73
+ for (const secretName of secretNames) {
74
+ let existingBindings;
75
+ try {
76
+ existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || [];
77
+ }
78
+ catch (err) {
79
+ throw new error_1.FirebaseError(`Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again. ` +
80
+ "For more information visit https://cloud.google.com/secret-manager/docs/manage-access-to-secrets#required-roles", { original: (0, error_1.getError)(err) });
81
+ }
82
+ do {
83
+ try {
84
+ const newBindings = [
85
+ {
86
+ role: "roles/secretmanager.secretAccessor",
87
+ members: Object.entries(typeGuesses).map(([email, type]) => `${type}:${email}`),
88
+ },
89
+ ];
90
+ const updatedBindings = existingBindings.concat(newBindings);
91
+ await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings);
92
+ break;
93
+ }
94
+ catch (err) {
95
+ if (!(err instanceof error_1.FirebaseError)) {
96
+ throw new error_1.FirebaseError(`Unexpected error updating IAM bindings on secret: ${secretName}`, {
97
+ original: (0, error_1.getError)(err),
98
+ });
99
+ }
100
+ const match = /Principal (.*) is of type "([^"]+)"/.exec(err.message);
101
+ if (!match) {
102
+ throw new error_1.FirebaseError(`Failed to set IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, { original: (0, error_1.getError)(err) });
103
+ }
104
+ typeGuesses[match[1]] = match[2];
105
+ continue;
106
+ }
107
+ } while (true);
108
+ utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`);
109
+ }
110
+ }
111
+ exports.grantEmailsSecretAccess = grantEmailsSecretAccess;
70
112
  async function upsertSecret(project, secret, location) {
71
113
  var _a, _b, _c, _d;
72
114
  let existing;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppHostingYamlConfig = void 0;
3
+ exports.toEnvList = exports.toEnvMap = exports.AppHostingYamlConfig = void 0;
4
4
  const path_1 = require("path");
5
5
  const utils_1 = require("../utils");
6
6
  const config_1 = require("./config");
@@ -23,7 +23,7 @@ class AppHostingYamlConfig {
23
23
  config.filename = path.basename(filePath);
24
24
  const loadedAppHostingYaml = (_a = (await (0, utils_1.wrappedSafeLoad)(file.source))) !== null && _a !== void 0 ? _a : {};
25
25
  if (loadedAppHostingYaml.env) {
26
- config.env = parseEnv(loadedAppHostingYaml.env);
26
+ config.env = toEnvMap(loadedAppHostingYaml.env);
27
27
  }
28
28
  return config;
29
29
  }
@@ -31,12 +31,13 @@ class AppHostingYamlConfig {
31
31
  return new AppHostingYamlConfig();
32
32
  }
33
33
  merge(other, allowSecretsToBecomePlaintext = true) {
34
+ var _a;
34
35
  if (!allowSecretsToBecomePlaintext) {
35
36
  const wereSecrets = Object.entries(this.env)
36
37
  .filter(([, env]) => env.secret)
37
38
  .map(([key]) => key);
38
39
  if (wereSecrets.some((key) => { var _a; return (_a = other.env[key]) === null || _a === void 0 ? void 0 : _a.value; })) {
39
- throw new error_1.FirebaseError(`Cannot convert secret to plaintext in ${other.filename ? other.filename : "apphosting yaml"}`);
40
+ throw new error_1.FirebaseError(`Cannot convert secret to plaintext in ${(_a = other.filename) !== null && _a !== void 0 ? _a : "apphosting yaml"}`);
40
41
  }
41
42
  }
42
43
  this.env = Object.assign(Object.assign({}, this.env), other.env);
@@ -47,13 +48,23 @@ class AppHostingYamlConfig {
47
48
  const file = await (0, utils_1.readFileFromDirectory)((0, path_1.dirname)(filePath), (0, path_1.basename)(filePath));
48
49
  yamlConfigToWrite = await (0, utils_1.wrappedSafeLoad)(file.source);
49
50
  }
50
- yamlConfigToWrite.env = Object.entries(this.env).map(([variable, env]) => {
51
- return Object.assign({ variable }, env);
52
- });
51
+ yamlConfigToWrite.env = toEnvList(this.env);
53
52
  (0, config_1.store)(filePath, yaml.parseDocument(jsYaml.dump(yamlConfigToWrite)));
54
53
  }
55
54
  }
56
55
  exports.AppHostingYamlConfig = AppHostingYamlConfig;
57
- function parseEnv(envs) {
58
- return Object.fromEntries(envs.map((env) => [env.variable, env]));
56
+ function toEnvMap(envs) {
57
+ return Object.fromEntries(envs.map((env) => {
58
+ const variable = env.variable;
59
+ const tmp = Object.assign({}, env);
60
+ delete env.variable;
61
+ return [variable, tmp];
62
+ }));
63
+ }
64
+ exports.toEnvMap = toEnvMap;
65
+ function toEnvList(envs) {
66
+ return Object.entries(envs).map(([variable, env]) => {
67
+ return Object.assign(Object.assign({}, env), { variable });
68
+ });
59
69
  }
70
+ exports.toEnvList = toEnvList;
@@ -7,7 +7,7 @@ const requireAuth_1 = require("../requireAuth");
7
7
  const client_1 = require("../appdistribution/client");
8
8
  const options_parser_util_1 = require("../appdistribution/options-parser-util");
9
9
  exports.command = new command_1.Command("appdistribution:groups:create <displayName> [alias]")
10
- .description("create group in project")
10
+ .description("create an App Distribution group")
11
11
  .alias("appdistribution:group:create")
12
12
  .before(requireAuth_1.requireAuth)
13
13
  .action(async (displayName, alias, options) => {
@@ -8,7 +8,7 @@ const error_1 = require("../error");
8
8
  const client_1 = require("../appdistribution/client");
9
9
  const options_parser_util_1 = require("../appdistribution/options-parser-util");
10
10
  exports.command = new command_1.Command("appdistribution:groups:delete <alias>")
11
- .description("delete group from a project")
11
+ .description("delete an App Distribution group")
12
12
  .alias("appdistribution:group:delete")
13
13
  .before(requireAuth_1.requireAuth)
14
14
  .action(async (alias, options) => {
@@ -11,7 +11,7 @@ const requireAuth_1 = require("../requireAuth");
11
11
  const utils = require("../utils");
12
12
  const Table = require("cli-table3");
13
13
  exports.command = new command_1.Command("appdistribution:groups:list")
14
- .description("list groups in project")
14
+ .description("list App Distribution groups")
15
15
  .alias("appdistribution:group:list")
16
16
  .before(requireAuth_1.requireAuth)
17
17
  .action(async (options) => {
@@ -7,7 +7,7 @@ const requireAuth_1 = require("../requireAuth");
7
7
  const client_1 = require("../appdistribution/client");
8
8
  const options_parser_util_1 = require("../appdistribution/options-parser-util");
9
9
  exports.command = new command_1.Command("appdistribution:testers:add [emails...]")
10
- .description("add testers to project (and possibly group)")
10
+ .description("add testers to project (and App Distribution group, if specified via flag)")
11
11
  .option("--file <file>", "a path to a file containing a list of tester emails to be added")
12
12
  .option("--group-alias <group-alias>", "if specified, the testers are also added to the group identified by this alias")
13
13
  .before(requireAuth_1.requireAuth)