firebase-tools 13.6.0 → 13.6.1

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 (49) hide show
  1. package/lib/apphosting/config.js +31 -0
  2. package/lib/apphosting/githubConnections.js +261 -0
  3. package/lib/{init/features/apphosting → apphosting}/index.js +21 -17
  4. package/lib/{init/features/apphosting → apphosting}/repo.js +9 -9
  5. package/lib/apphosting/secrets/dialogs.js +169 -0
  6. package/lib/apphosting/secrets/index.js +98 -0
  7. package/lib/commands/apphosting-backends-create.js +4 -2
  8. package/lib/commands/apphosting-backends-delete.js +1 -1
  9. package/lib/commands/apphosting-secrets-describe.js +29 -0
  10. package/lib/commands/apphosting-secrets-grantaccess.js +45 -0
  11. package/lib/commands/apphosting-secrets-set.js +102 -0
  12. package/lib/commands/functions-secrets-access.js +2 -2
  13. package/lib/commands/functions-secrets-describe.js +14 -0
  14. package/lib/commands/functions-secrets-destroy.js +2 -2
  15. package/lib/commands/functions-secrets-get.js +3 -17
  16. package/lib/commands/functions-secrets-prune.js +2 -1
  17. package/lib/commands/functions-secrets-set.js +2 -2
  18. package/lib/commands/index.js +5 -0
  19. package/lib/deploy/functions/checkIam.js +3 -6
  20. package/lib/deploy/functions/containerCleaner.js +1 -11
  21. package/lib/deploy/functions/params.js +2 -2
  22. package/lib/deploy/functions/prepare.js +12 -3
  23. package/lib/deploy/functions/release/fabricator.js +5 -5
  24. package/lib/deploy/functions/runtimes/index.js +6 -43
  25. package/lib/deploy/functions/runtimes/node/index.js +3 -2
  26. package/lib/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.js +15 -34
  27. package/lib/deploy/functions/runtimes/python/index.js +11 -7
  28. package/lib/deploy/functions/runtimes/supported.js +135 -0
  29. package/lib/emulator/controller.js +8 -1
  30. package/lib/emulator/functionsEmulator.js +2 -2
  31. package/lib/emulator/hub.js +5 -0
  32. package/lib/extensions/emulator/specHelper.js +4 -3
  33. package/lib/functional.js +2 -2
  34. package/lib/functions/secrets.js +40 -22
  35. package/lib/gcp/apphosting.js +15 -2
  36. package/lib/gcp/cloudbuild.js +7 -3
  37. package/lib/gcp/cloudfunctions.js +5 -5
  38. package/lib/gcp/cloudfunctionsv2.js +3 -3
  39. package/lib/gcp/cloudscheduler.js +2 -2
  40. package/lib/gcp/computeEngine.js +7 -0
  41. package/lib/gcp/devConnect.js +24 -11
  42. package/lib/gcp/iam.js +9 -1
  43. package/lib/gcp/secretManager.js +53 -13
  44. package/lib/gcp/serviceusage.js +21 -5
  45. package/lib/init/features/functions/python.js +4 -3
  46. package/lib/init/features/index.js +1 -1
  47. package/package.json +1 -1
  48. package/schema/firebase-config.json +12 -2
  49. /package/lib/{init/features/apphosting → apphosting}/constants.js +0 -0
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.store = exports.load = exports.yamlPath = void 0;
4
+ const path = require("path");
5
+ const fs_1 = require("fs");
6
+ const yaml = require("js-yaml");
7
+ const fs = require("../fsutils");
8
+ function yamlPath(cwd) {
9
+ let dir = cwd;
10
+ while (!fs.fileExistsSync(path.resolve(dir, "apphosting.yaml"))) {
11
+ if (fs.fileExistsSync(path.resolve(dir, "firebase.json"))) {
12
+ return null;
13
+ }
14
+ const parent = path.dirname(dir);
15
+ if (parent === dir) {
16
+ return null;
17
+ }
18
+ dir = parent;
19
+ }
20
+ return path.resolve(dir, "apphosting.yaml");
21
+ }
22
+ exports.yamlPath = yamlPath;
23
+ function load(yamlPath) {
24
+ const raw = fs.readFile(yamlPath);
25
+ return yaml.load(raw, yaml.DEFAULT_FULL_SCHEMA);
26
+ }
27
+ exports.load = load;
28
+ function store(yamlPath, config) {
29
+ (0, fs_1.writeFileSync)(yamlPath, yaml.dump(config));
30
+ }
31
+ exports.store = store;
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchAllRepositories = exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.ensureSecretManagerAdminGrant = exports.getOrCreateOauthConnection = exports.linkGitHubRepository = exports.generateRepositoryId = exports.extractRepoSlugFromUri = exports.parseConnectionName = void 0;
4
+ const clc = require("colorette");
5
+ const devConnect = require("../gcp/devConnect");
6
+ const rm = require("../gcp/resourceManager");
7
+ const poller = require("../operation-poller");
8
+ const utils = require("../utils");
9
+ const error_1 = require("../error");
10
+ const prompt_1 = require("../prompt");
11
+ const getProjectNumber_1 = require("../getProjectNumber");
12
+ const api_1 = require("../api");
13
+ const fuzzy = require("fuzzy");
14
+ const inquirer = require("inquirer");
15
+ const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/;
16
+ const APPHOSTING_OAUTH_CONN_NAME = "apphosting-github-oauth";
17
+ const CONNECTION_NAME_REGEX = /^projects\/(?<projectId>[^\/]+)\/locations\/(?<location>[^\/]+)\/connections\/(?<id>[^\/]+)$/;
18
+ function parseConnectionName(name) {
19
+ const match = CONNECTION_NAME_REGEX.exec(name);
20
+ if (!match || typeof match.groups === undefined) {
21
+ return;
22
+ }
23
+ const { projectId, location, id } = match.groups;
24
+ return {
25
+ projectId,
26
+ location,
27
+ id,
28
+ };
29
+ }
30
+ exports.parseConnectionName = parseConnectionName;
31
+ const devConnectPollerOptions = {
32
+ apiOrigin: (0, api_1.developerConnectOrigin)(),
33
+ apiVersion: "v1",
34
+ masterTimeout: 25 * 60 * 1000,
35
+ maxBackoff: 10000,
36
+ };
37
+ function extractRepoSlugFromUri(cloneUri) {
38
+ const match = /github.com\/(.+).git/.exec(cloneUri);
39
+ if (!match) {
40
+ return undefined;
41
+ }
42
+ return match[1];
43
+ }
44
+ exports.extractRepoSlugFromUri = extractRepoSlugFromUri;
45
+ function generateRepositoryId(remoteUri) {
46
+ var _a;
47
+ return (_a = extractRepoSlugFromUri(remoteUri)) === null || _a === void 0 ? void 0 : _a.replaceAll("/", "-");
48
+ }
49
+ exports.generateRepositoryId = generateRepositoryId;
50
+ function generateConnectionId() {
51
+ const randomHash = Math.random().toString(36).slice(6);
52
+ return `apphosting-github-conn-${randomHash}`;
53
+ }
54
+ const ADD_CONN_CHOICE = "@ADD_CONN";
55
+ async function linkGitHubRepository(projectId, location) {
56
+ var _a, _b;
57
+ utils.logBullet(clc.bold(`${clc.yellow("===")} Set up a GitHub connection`));
58
+ const oauthConn = await getOrCreateOauthConnection(projectId, location);
59
+ const existingConns = await listAppHostingConnections(projectId);
60
+ if (existingConns.length === 0) {
61
+ utils.logBullet("no connections exist");
62
+ existingConns.push(await createFullyInstalledConnection(projectId, location, generateConnectionId(), oauthConn));
63
+ }
64
+ let repoCloneUri;
65
+ let connection;
66
+ do {
67
+ if (repoCloneUri === ADD_CONN_CHOICE) {
68
+ existingConns.push(await createFullyInstalledConnection(projectId, location, generateConnectionId(), oauthConn));
69
+ }
70
+ const selection = await promptCloneUri(projectId, existingConns);
71
+ repoCloneUri = selection.cloneUri;
72
+ connection = selection.connection;
73
+ } while (repoCloneUri === ADD_CONN_CHOICE);
74
+ const { id: connectionId } = parseConnectionName(connection.name);
75
+ await getOrCreateConnection(projectId, location, connectionId, {
76
+ authorizerCredential: (_a = connection.githubConfig) === null || _a === void 0 ? void 0 : _a.authorizerCredential,
77
+ appInstallationId: (_b = connection.githubConfig) === null || _b === void 0 ? void 0 : _b.appInstallationId,
78
+ });
79
+ const repo = await getOrCreateRepository(projectId, location, connectionId, repoCloneUri);
80
+ utils.logSuccess(`Successfully linked GitHub repository at remote URI`);
81
+ utils.logSuccess(`\t${repo.cloneUri}`);
82
+ return repo;
83
+ }
84
+ exports.linkGitHubRepository = linkGitHubRepository;
85
+ async function createFullyInstalledConnection(projectId, location, connectionId, oauthConn) {
86
+ var _a, _b;
87
+ let conn = await createConnection(projectId, location, connectionId, {
88
+ appInstallationId: (_a = oauthConn.githubConfig) === null || _a === void 0 ? void 0 : _a.appInstallationId,
89
+ authorizerCredential: (_b = oauthConn.githubConfig) === null || _b === void 0 ? void 0 : _b.authorizerCredential,
90
+ });
91
+ while (conn.installationState.stage !== "COMPLETE") {
92
+ utils.logBullet("Install the Firebase GitHub app to enable access to GitHub repositories");
93
+ const targetUri = conn.installationState.actionUri.replace("install_v2", "direct_install_v2");
94
+ utils.logBullet(targetUri);
95
+ await utils.openInBrowser(targetUri);
96
+ await (0, prompt_1.promptOnce)({
97
+ type: "input",
98
+ message: "Press Enter once you have installed or configured the Firebase GitHub app to access your GitHub repo.",
99
+ });
100
+ conn = await devConnect.getConnection(projectId, location, connectionId);
101
+ }
102
+ return conn;
103
+ }
104
+ async function getOrCreateOauthConnection(projectId, location) {
105
+ let conn;
106
+ try {
107
+ conn = await devConnect.getConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
108
+ }
109
+ catch (err) {
110
+ if (err.status === 404) {
111
+ await ensureSecretManagerAdminGrant(projectId);
112
+ conn = await createConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
113
+ }
114
+ else {
115
+ throw err;
116
+ }
117
+ }
118
+ while (conn.installationState.stage === "PENDING_USER_OAUTH") {
119
+ utils.logBullet("You must authorize the Firebase GitHub app.");
120
+ utils.logBullet("Sign in to GitHub and authorize Firebase GitHub app:");
121
+ const { url, cleanup } = await utils.openInBrowserPopup(conn.installationState.actionUri, "Authorize the GitHub app");
122
+ utils.logBullet(`\t${url}`);
123
+ await (0, prompt_1.promptOnce)({
124
+ type: "input",
125
+ message: "Press Enter once you have authorized the app",
126
+ });
127
+ cleanup();
128
+ const { projectId, location, id } = parseConnectionName(conn.name);
129
+ conn = await devConnect.getConnection(projectId, location, id);
130
+ }
131
+ return conn;
132
+ }
133
+ exports.getOrCreateOauthConnection = getOrCreateOauthConnection;
134
+ async function promptCloneUri(projectId, connections) {
135
+ const { cloneUris, cloneUriToConnection } = await fetchAllRepositories(projectId, connections);
136
+ const cloneUri = await (0, prompt_1.promptOnce)({
137
+ type: "autocomplete",
138
+ name: "cloneUri",
139
+ message: "Which of the following repositories would you like to deploy?",
140
+ source: (_, input = "") => {
141
+ return new Promise((resolve) => resolve([
142
+ new inquirer.Separator(),
143
+ {
144
+ name: "Missing a repo? Select this option to configure your GitHub connection settings",
145
+ value: ADD_CONN_CHOICE,
146
+ },
147
+ new inquirer.Separator(),
148
+ ...fuzzy
149
+ .filter(input, cloneUris, {
150
+ extract: (uri) => extractRepoSlugFromUri(uri) || "",
151
+ })
152
+ .map((result) => {
153
+ return {
154
+ name: extractRepoSlugFromUri(result.original) || "",
155
+ value: result.original,
156
+ };
157
+ }),
158
+ ]));
159
+ },
160
+ });
161
+ return { cloneUri, connection: cloneUriToConnection[cloneUri] };
162
+ }
163
+ async function ensureSecretManagerAdminGrant(projectId) {
164
+ const projectNumber = await (0, getProjectNumber_1.getProjectNumber)({ projectId });
165
+ const dcsaEmail = devConnect.serviceAgentEmail(projectNumber);
166
+ const alreadyGranted = await rm.serviceAccountHasRoles(projectId, dcsaEmail, ["roles/secretmanager.admin"], true);
167
+ if (alreadyGranted) {
168
+ utils.logBullet("secret manager admin role already granted");
169
+ return;
170
+ }
171
+ utils.logBullet("To create a new GitHub connection, Secret Manager Admin role (roles/secretmanager.admin) is required on the Developer Connect Service Agent.");
172
+ const grant = await (0, prompt_1.promptOnce)({
173
+ type: "confirm",
174
+ message: "Grant the required role to the Developer Connect Service Agent?",
175
+ });
176
+ if (!grant) {
177
+ utils.logBullet("You, or your project administrator, should run the following command to grant the required role:\n\n" +
178
+ "You, or your project adminstrator, can run the following command to grant the required role manually:\n\n" +
179
+ `\tgcloud projects add-iam-policy-binding ${projectId} \\\n` +
180
+ `\t --member="serviceAccount:${dcsaEmail} \\\n` +
181
+ `\t --role="roles/secretmanager.admin\n`);
182
+ throw new error_1.FirebaseError("Insufficient IAM permissions to create a new connection to GitHub");
183
+ }
184
+ try {
185
+ await rm.addServiceAccountToRoles(projectId, dcsaEmail, ["roles/secretmanager.admin"], true);
186
+ }
187
+ catch (e) {
188
+ if ((e === null || e === void 0 ? void 0 : e.code) === 400 || (e === null || e === void 0 ? void 0 : e.status) === 400) {
189
+ await devConnect.generateP4SA(projectNumber);
190
+ await rm.addServiceAccountToRoles(projectId, dcsaEmail, ["roles/secretmanager.admin"], true);
191
+ }
192
+ else {
193
+ throw e;
194
+ }
195
+ }
196
+ utils.logSuccess("Successfully granted the required role to the Developer Connect Service Agent!");
197
+ }
198
+ exports.ensureSecretManagerAdminGrant = ensureSecretManagerAdminGrant;
199
+ async function createConnection(projectId, location, connectionId, githubConfig) {
200
+ const op = await devConnect.createConnection(projectId, location, connectionId, githubConfig);
201
+ const conn = await poller.pollOperation(Object.assign(Object.assign({}, devConnectPollerOptions), { pollerName: `create-${location}-${connectionId}`, operationResourceName: op.name }));
202
+ return conn;
203
+ }
204
+ exports.createConnection = createConnection;
205
+ async function getOrCreateConnection(projectId, location, connectionId, githubConfig) {
206
+ let conn;
207
+ try {
208
+ conn = await devConnect.getConnection(projectId, location, connectionId);
209
+ }
210
+ catch (err) {
211
+ if (err.status === 404) {
212
+ utils.logBullet("creating connection");
213
+ conn = await createConnection(projectId, location, connectionId, githubConfig);
214
+ }
215
+ else {
216
+ throw err;
217
+ }
218
+ }
219
+ return conn;
220
+ }
221
+ exports.getOrCreateConnection = getOrCreateConnection;
222
+ async function getOrCreateRepository(projectId, location, connectionId, cloneUri) {
223
+ const repositoryId = generateRepositoryId(cloneUri);
224
+ if (!repositoryId) {
225
+ throw new error_1.FirebaseError(`Failed to generate repositoryId for URI "${cloneUri}".`);
226
+ }
227
+ let repo;
228
+ try {
229
+ repo = await devConnect.getGitRepositoryLink(projectId, location, connectionId, repositoryId);
230
+ }
231
+ catch (err) {
232
+ if (err.status === 404) {
233
+ const op = await devConnect.createGitRepositoryLink(projectId, location, connectionId, repositoryId, cloneUri);
234
+ repo = await poller.pollOperation(Object.assign(Object.assign({}, devConnectPollerOptions), { pollerName: `create-${location}-${connectionId}-${repositoryId}`, operationResourceName: op.name }));
235
+ }
236
+ else {
237
+ throw err;
238
+ }
239
+ }
240
+ return repo;
241
+ }
242
+ exports.getOrCreateRepository = getOrCreateRepository;
243
+ async function listAppHostingConnections(projectId) {
244
+ const conns = await devConnect.listAllConnections(projectId, "-");
245
+ return conns.filter((conn) => APPHOSTING_CONN_PATTERN.test(conn.name) &&
246
+ conn.installationState.stage === "COMPLETE" &&
247
+ !conn.disabled);
248
+ }
249
+ exports.listAppHostingConnections = listAppHostingConnections;
250
+ async function fetchAllRepositories(projectId, connections) {
251
+ const cloneUriToConnection = {};
252
+ for (const conn of connections) {
253
+ const { location, id } = parseConnectionName(conn.name);
254
+ const connectionRepos = await devConnect.listAllLinkableGitRepositories(projectId, location, id);
255
+ connectionRepos.forEach((repo) => {
256
+ cloneUriToConnection[repo.cloneUri] = conn;
257
+ });
258
+ }
259
+ return { cloneUris: Object.keys(cloneUriToConnection), cloneUriToConnection };
260
+ }
261
+ exports.fetchAllRepositories = fetchAllRepositories;
@@ -3,18 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.doSetup = void 0;
4
4
  const clc = require("colorette");
5
5
  const repo = require("./repo");
6
- const poller = require("../../../operation-poller");
7
- const apphosting = require("../../../gcp/apphosting");
8
- const utils_1 = require("../../../utils");
9
- const api_1 = require("../../../api");
10
- const apphosting_1 = require("../../../gcp/apphosting");
11
- const resourceManager_1 = require("../../../gcp/resourceManager");
12
- const iam_1 = require("../../../gcp/iam");
13
- const error_1 = require("../../../error");
14
- const prompt_1 = require("../../../prompt");
6
+ const poller = require("../operation-poller");
7
+ const apphosting = require("../gcp/apphosting");
8
+ const githubConnections = require("./githubConnections");
9
+ const utils_1 = require("../utils");
10
+ const api_1 = require("../api");
11
+ const apphosting_1 = require("../gcp/apphosting");
12
+ const resourceManager_1 = require("../gcp/resourceManager");
13
+ const iam = require("../gcp/iam");
14
+ const error_1 = require("../error");
15
+ const prompt_1 = require("../prompt");
15
16
  const constants_1 = require("./constants");
16
- const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
17
- const deploymentTool = require("../../../deploymentTool");
17
+ const ensureApiEnabled_1 = require("../ensureApiEnabled");
18
+ const deploymentTool = require("../deploymentTool");
18
19
  const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
19
20
  const apphostingPollerOptions = {
20
21
  apiOrigin: (0, api_1.apphostingOrigin)(),
@@ -22,8 +23,9 @@ const apphostingPollerOptions = {
22
23
  masterTimeout: 25 * 60 * 1000,
23
24
  maxBackoff: 10000,
24
25
  };
25
- async function doSetup(projectId, location, serviceAccount) {
26
+ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
26
27
  await Promise.all([
28
+ ...(withDevConnect ? [(0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true)] : []),
27
29
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudbuildOrigin)(), "apphosting", true),
28
30
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.secretManagerOrigin)(), "apphosting", true),
29
31
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudRunApiOrigin)(), "apphosting", true),
@@ -53,8 +55,10 @@ async function doSetup(projectId, location, serviceAccount) {
53
55
  default: "my-web-app",
54
56
  message: "Create a name for your backend [1-30 characters]",
55
57
  });
56
- const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
57
- const backend = await createBackend(projectId, location, backendId, cloudBuildConnRepo, serviceAccount);
58
+ const gitRepositoryConnection = withDevConnect
59
+ ? await githubConnections.linkGitHubRepository(projectId, location)
60
+ : await repo.linkGitHubRepository(projectId, location);
61
+ const backend = await createBackend(projectId, location, backendId, gitRepositoryConnection, serviceAccount);
58
62
  const branch = await (0, prompt_1.promptOnce)({
59
63
  name: "branch",
60
64
  type: "input",
@@ -111,9 +115,9 @@ async function createBackend(projectId, location, backendId, repository, service
111
115
  rootDirectory: "/",
112
116
  },
113
117
  labels: deploymentTool.labels(),
114
- computeServiceAccount: serviceAccount || defaultServiceAccount,
118
+ serviceAccount: serviceAccount || defaultServiceAccount,
115
119
  };
116
- delete backendReqBody.computeServiceAccount;
120
+ delete backendReqBody.serviceAccount;
117
121
  async function createBackendAndPoll() {
118
122
  const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
119
123
  return await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
@@ -137,7 +141,7 @@ async function createBackend(projectId, location, backendId, repository, service
137
141
  exports.createBackend = createBackend;
138
142
  async function provisionDefaultComputeServiceAccount(projectId) {
139
143
  try {
140
- await (0, iam_1.createServiceAccount)(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Firebase App Hosting compute service account", "Default service account used to run builds and deploys for Firebase App Hosting");
144
+ await iam.createServiceAccount(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Firebase App Hosting compute service account", "Default service account used to run builds and deploys for Firebase App Hosting");
141
145
  }
142
146
  catch (err) {
143
147
  if (err.status !== 409) {
@@ -2,14 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fetchAllRepositories = exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.getOrCreateOauthConnection = exports.linkGitHubRepository = exports.parseConnectionName = void 0;
4
4
  const clc = require("colorette");
5
- const gcb = require("../../../gcp/cloudbuild");
6
- const rm = require("../../../gcp/resourceManager");
7
- const poller = require("../../../operation-poller");
8
- const utils = require("../../../utils");
9
- const api_1 = require("../../../api");
10
- const error_1 = require("../../../error");
11
- const prompt_1 = require("../../../prompt");
12
- const getProjectNumber_1 = require("../../../getProjectNumber");
5
+ const gcb = require("../gcp/cloudbuild");
6
+ const rm = require("../gcp/resourceManager");
7
+ const poller = require("../operation-poller");
8
+ const utils = require("../utils");
9
+ const api_1 = require("../api");
10
+ const error_1 = require("../error");
11
+ const prompt_1 = require("../prompt");
12
+ const getProjectNumber_1 = require("../getProjectNumber");
13
13
  const fuzzy = require("fuzzy");
14
14
  const inquirer = require("inquirer");
15
15
  const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/;
@@ -158,7 +158,7 @@ async function promptRepositoryUri(projectId, connections) {
158
158
  }
159
159
  async function ensureSecretManagerAdminGrant(projectId) {
160
160
  const projectNumber = await (0, getProjectNumber_1.getProjectNumber)({ projectId });
161
- const cbsaEmail = gcb.serviceAgentEmail(projectNumber);
161
+ const cbsaEmail = gcb.getDefaultServiceAgent(projectNumber);
162
162
  const alreadyGranted = await rm.serviceAccountHasRoles(projectId, cbsaEmail, ["roles/secretmanager.admin"], true);
163
163
  if (alreadyGranted) {
164
164
  return;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.envVarForSecret = exports.selectBackendServiceAccounts = exports.GRANT_ACCESS_IN_FUTURE = exports.WARN_NO_BACKENDS = exports.selectFromMetadata = exports.tableForBackends = exports.serviceAccountDisplay = exports.toMetadata = void 0;
4
+ const clc = require("colorette");
5
+ const Table = require("cli-table");
6
+ const _1 = require(".");
7
+ const apphosting = require("../../gcp/apphosting");
8
+ const prompt = require("../../prompt");
9
+ const utils = require("../../utils");
10
+ const logger_1 = require("../../logger");
11
+ const env = require("../../functions/env");
12
+ function toMetadata(projectNumber, backends) {
13
+ const metadata = [];
14
+ for (const backend of backends) {
15
+ const [, , , location, , id] = backend.name.split("/");
16
+ metadata.push(Object.assign({ location, id }, (0, _1.serviceAccountsForBackend)(projectNumber, backend)));
17
+ }
18
+ return metadata.sort((left, right) => {
19
+ const cmplocation = left.location.localeCompare(right.location);
20
+ if (cmplocation) {
21
+ return cmplocation;
22
+ }
23
+ return left.id.localeCompare(right.id);
24
+ });
25
+ }
26
+ exports.toMetadata = toMetadata;
27
+ function serviceAccountDisplay(metadata) {
28
+ if (sameServiceAccount(metadata)) {
29
+ return metadata.runServiceAccount;
30
+ }
31
+ return `${metadata.buildServiceAccount}, ${metadata.runServiceAccount}`;
32
+ }
33
+ exports.serviceAccountDisplay = serviceAccountDisplay;
34
+ function sameServiceAccount(metadata) {
35
+ return metadata.buildServiceAccount === metadata.runServiceAccount;
36
+ }
37
+ const matchesServiceAccounts = (target) => (test) => {
38
+ return (target.buildServiceAccount === test.buildServiceAccount &&
39
+ target.runServiceAccount === test.runServiceAccount);
40
+ };
41
+ function tableForBackends(metadata) {
42
+ const headers = [
43
+ "location",
44
+ "backend",
45
+ metadata.every(sameServiceAccount) ? "service account" : "service accounts",
46
+ ];
47
+ const rows = metadata.map((m) => [m.location, m.id, serviceAccountDisplay(m)]);
48
+ return [headers, rows];
49
+ }
50
+ exports.tableForBackends = tableForBackends;
51
+ function selectFromMetadata(input, selected) {
52
+ const buildAccounts = new Set();
53
+ const runAccounts = new Set();
54
+ for (const sa of selected) {
55
+ if (input.find((m) => m.buildServiceAccount === sa)) {
56
+ buildAccounts.add(sa);
57
+ }
58
+ else {
59
+ runAccounts.add(sa);
60
+ }
61
+ }
62
+ return {
63
+ buildServiceAccounts: [...buildAccounts],
64
+ runServiceAccounts: [...runAccounts],
65
+ };
66
+ }
67
+ exports.selectFromMetadata = selectFromMetadata;
68
+ exports.WARN_NO_BACKENDS = "To use this secret, your backend's service account must have secret accessor permission. " +
69
+ "It does not look like you have a backend yet. After creating a backend, grant access with " +
70
+ clc.bold("firebase apphosting:secrets:grantAccess");
71
+ exports.GRANT_ACCESS_IN_FUTURE = `To grant access in the future, run ${clc.bold("firebase apphosting:secrets:grantaccess")}`;
72
+ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
73
+ const listBackends = await apphosting.listBackends(projectId, "-");
74
+ if (listBackends.unreachable.length) {
75
+ utils.logLabeledWarning("apphosting", `Could not reach location(s) ${listBackends.unreachable.join(", ")}. You may need to run ` +
76
+ `${clc.bold("firebase apphosting:secrets:grantAccess")} at a later time if you have backends in these locations`);
77
+ }
78
+ if (!listBackends.backends.length) {
79
+ utils.logLabeledWarning("apphosting", exports.WARN_NO_BACKENDS);
80
+ return { buildServiceAccounts: [], runServiceAccounts: [] };
81
+ }
82
+ if (listBackends.backends.length === 1) {
83
+ const grant = await prompt.confirm({
84
+ nonInteractive: options.nonInteractive,
85
+ default: true,
86
+ message: "To use this secret, your backend's service account must have secret accessor permission. Would you like to grant it now?",
87
+ });
88
+ if (grant) {
89
+ return (0, _1.toMulti)((0, _1.serviceAccountsForBackend)(projectNumber, listBackends.backends[0]));
90
+ }
91
+ utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
92
+ return { buildServiceAccounts: [], runServiceAccounts: [] };
93
+ }
94
+ const metadata = toMetadata(projectNumber, listBackends.backends);
95
+ if (metadata.every(matchesServiceAccounts(metadata[0]))) {
96
+ utils.logLabeledBullet("apphosting", "To use this secret, your backend's service account must have secret accessor permission. All of your backends use " +
97
+ (sameServiceAccount(metadata[0]) ? "service account " : "service accounts ") +
98
+ serviceAccountDisplay(metadata[0]) +
99
+ ". Granting access to one backend will grant access to all backends.");
100
+ const grant = await prompt.confirm({
101
+ nonInteractive: options.nonInteractive,
102
+ default: true,
103
+ message: "Would you like to grant it now?",
104
+ });
105
+ if (grant) {
106
+ return selectFromMetadata(metadata, [
107
+ metadata[0].buildServiceAccount,
108
+ metadata[0].runServiceAccount,
109
+ ]);
110
+ }
111
+ utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
112
+ return { buildServiceAccounts: [], runServiceAccounts: [] };
113
+ }
114
+ utils.logLabeledBullet("apphosting", "To use this secret, your backend's service account must have secret accessor permission. Your backends use the following service accounts:");
115
+ const tableData = tableForBackends(metadata);
116
+ const table = new Table({
117
+ head: tableData[0],
118
+ style: { head: ["green"] },
119
+ rows: tableData[1],
120
+ });
121
+ logger_1.logger.info(table.toString());
122
+ const allAccounts = metadata.reduce((accum, row) => {
123
+ accum.add(row.buildServiceAccount);
124
+ accum.add(row.runServiceAccount);
125
+ return accum;
126
+ }, new Set());
127
+ const chosen = await prompt.promptOnce({
128
+ type: "checkbox",
129
+ message: "Which service accounts would you like to grant access? " +
130
+ "Press Space to select accounts, then Enter to confirm your choices.",
131
+ choices: [...allAccounts.values()].sort(),
132
+ });
133
+ if (!chosen.length) {
134
+ utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
135
+ }
136
+ return selectFromMetadata(metadata, chosen);
137
+ }
138
+ exports.selectBackendServiceAccounts = selectBackendServiceAccounts;
139
+ function toUpperSnakeCase(key) {
140
+ return key
141
+ .replace(/[.-]/g, "_")
142
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
143
+ .toUpperCase();
144
+ }
145
+ async function envVarForSecret(secret) {
146
+ const upper = toUpperSnakeCase(secret);
147
+ if (upper === secret) {
148
+ try {
149
+ env.validateKey(secret);
150
+ return secret;
151
+ }
152
+ catch (_a) {
153
+ }
154
+ }
155
+ do {
156
+ const test = await prompt.promptOnce({
157
+ message: "What environment variable name would you like to use?",
158
+ default: upper,
159
+ });
160
+ try {
161
+ env.validateKey(test);
162
+ return test;
163
+ }
164
+ catch (err) {
165
+ utils.logLabeledError("apphosting", err.message);
166
+ }
167
+ } while (true);
168
+ }
169
+ exports.envVarForSecret = envVarForSecret;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upsertSecret = exports.grantSecretAccess = exports.serviceAccountsForBackend = exports.toMulti = void 0;
4
+ const error_1 = require("../../error");
5
+ const gcsm = require("../../gcp/secretManager");
6
+ const gcb = require("../../gcp/cloudbuild");
7
+ const gce = require("../../gcp/computeEngine");
8
+ const secretManager_1 = require("../../gcp/secretManager");
9
+ const secretManager_2 = require("../../gcp/secretManager");
10
+ const utils = require("../../utils");
11
+ const prompt = require("../../prompt");
12
+ function toMulti(accounts) {
13
+ const m = {
14
+ buildServiceAccounts: [accounts.buildServiceAccount],
15
+ runServiceAccounts: [],
16
+ };
17
+ if (accounts.buildServiceAccount !== accounts.runServiceAccount) {
18
+ m.runServiceAccounts.push(accounts.runServiceAccount);
19
+ }
20
+ return m;
21
+ }
22
+ exports.toMulti = toMulti;
23
+ function serviceAccountsForBackend(projectNumber, backend) {
24
+ if (backend.serviceAccount) {
25
+ return {
26
+ buildServiceAccount: backend.serviceAccount,
27
+ runServiceAccount: backend.serviceAccount,
28
+ };
29
+ }
30
+ return {
31
+ buildServiceAccount: gcb.getDefaultServiceAccount(projectNumber),
32
+ runServiceAccount: gce.getDefaultServiceAccount(projectNumber),
33
+ };
34
+ }
35
+ exports.serviceAccountsForBackend = serviceAccountsForBackend;
36
+ async function grantSecretAccess(projectId, secretName, accounts) {
37
+ const newBindings = [
38
+ {
39
+ role: "roles/secretmanager.secretAccessor",
40
+ members: [...accounts.buildServiceAccounts, ...accounts.runServiceAccounts].map((sa) => `serviceAccount:${sa}`),
41
+ },
42
+ {
43
+ role: "roles/secretmanager.viewer",
44
+ members: accounts.buildServiceAccounts.map((sa) => `serviceAccount:${sa}`),
45
+ },
46
+ ];
47
+ let existingBindings;
48
+ try {
49
+ existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || [];
50
+ }
51
+ catch (err) {
52
+ 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: err });
53
+ }
54
+ try {
55
+ const updatedBindings = existingBindings.concat(newBindings);
56
+ await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings);
57
+ }
58
+ catch (err) {
59
+ 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: err });
60
+ }
61
+ utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`);
62
+ }
63
+ exports.grantSecretAccess = grantSecretAccess;
64
+ async function upsertSecret(project, secret, location) {
65
+ var _a, _b, _c, _d;
66
+ let existing;
67
+ try {
68
+ existing = await gcsm.getSecret(project, secret);
69
+ }
70
+ catch (err) {
71
+ if (err.status !== 404) {
72
+ throw new error_1.FirebaseError("Unexpected error loading secret", { original: err });
73
+ }
74
+ await gcsm.createSecret(project, secret, gcsm.labels("apphosting"), location);
75
+ return true;
76
+ }
77
+ const replication = (_a = existing.replication) === null || _a === void 0 ? void 0 : _a.userManaged;
78
+ if (location &&
79
+ (((_b = replication === null || replication === void 0 ? void 0 : replication.replicas) === null || _b === void 0 ? void 0 : _b.length) !== 1 || ((_d = (_c = replication === null || replication === void 0 ? void 0 : replication.replicas) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.location) !== location)) {
80
+ utils.logLabeledError("apphosting", "Secret replication policies cannot be changed after creation");
81
+ return null;
82
+ }
83
+ if ((0, secretManager_2.isFunctionsManaged)(existing)) {
84
+ utils.logLabeledWarning("apphosting", `Cloud Functions for Firebase currently manages versions of ${secret}. Continuing will disable ` +
85
+ "automatic deletion of old versions.");
86
+ const stopTracking = await prompt.confirm({
87
+ message: "Do you wish to continue?",
88
+ default: false,
89
+ });
90
+ if (!stopTracking) {
91
+ return null;
92
+ }
93
+ delete existing.labels[secretManager_1.FIREBASE_MANAGED];
94
+ await gcsm.patchSecret(project, secret, existing.labels);
95
+ }
96
+ return false;
97
+ }
98
+ exports.upsertSecret = upsertSecret;