firebase-tools 13.23.0 → 13.24.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.
@@ -0,0 +1,288 @@
1
+ "use strict";
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;
4
+ const clc = require("colorette");
5
+ const poller = require("../operation-poller");
6
+ const apphosting = require("../gcp/apphosting");
7
+ const githubConnections = require("./githubConnections");
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 = require("../gcp/iam");
13
+ const error_1 = require("../error");
14
+ const prompt_1 = require("../prompt");
15
+ const constants_1 = require("./constants");
16
+ const ensureApiEnabled_1 = require("../ensureApiEnabled");
17
+ const deploymentTool = require("../deploymentTool");
18
+ const app_1 = require("./app");
19
+ const ora = require("ora");
20
+ const node_fetch_1 = require("node-fetch");
21
+ const rollout_1 = require("./rollout");
22
+ const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
23
+ const apphostingPollerOptions = {
24
+ apiOrigin: (0, api_1.apphostingOrigin)(),
25
+ apiVersion: apphosting_1.API_VERSION,
26
+ masterTimeout: 25 * 60 * 1000,
27
+ maxBackoff: 10000,
28
+ };
29
+ async function tlsReady(url) {
30
+ var _a;
31
+ try {
32
+ await (0, node_fetch_1.default)(url);
33
+ return true;
34
+ }
35
+ catch (err) {
36
+ const maybeNodeError = err;
37
+ if (/HANDSHAKE_FAILURE/.test((_a = maybeNodeError === null || maybeNodeError === void 0 ? void 0 : maybeNodeError.cause) === null || _a === void 0 ? void 0 : _a.code) ||
38
+ "EPROTO" === (maybeNodeError === null || maybeNodeError === void 0 ? void 0 : maybeNodeError.code)) {
39
+ return false;
40
+ }
41
+ return true;
42
+ }
43
+ }
44
+ async function awaitTlsReady(url) {
45
+ let ready;
46
+ do {
47
+ ready = await tlsReady(url);
48
+ if (!ready) {
49
+ await (0, utils_1.sleep)(1000);
50
+ }
51
+ } while (!ready);
52
+ }
53
+ async function doSetup(projectId, webAppName, location, serviceAccount) {
54
+ await Promise.all([
55
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true),
56
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudbuildOrigin)(), "apphosting", true),
57
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.secretManagerOrigin)(), "apphosting", true),
58
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudRunApiOrigin)(), "apphosting", true),
59
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.artifactRegistryDomain)(), "apphosting", true),
60
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.iamOrigin)(), "apphosting", true),
61
+ ]);
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"));
71
+ const gitRepositoryLink = await githubConnections.linkGitHubRepository(projectId, location);
72
+ const rootDir = await (0, prompt_1.promptOnce)({
73
+ name: "rootDir",
74
+ type: "input",
75
+ default: "/",
76
+ message: "Specify your app's root directory relative to your repository",
77
+ });
78
+ const branch = await githubConnections.promptGitHubBranch(gitRepositoryLink);
79
+ (0, utils_1.logSuccess)(`Repo linked successfully!\n`);
80
+ (0, utils_1.logBullet)(`${clc.yellow("===")} Set up your backend`);
81
+ const backendId = await promptNewBackendId(projectId, location, {
82
+ name: "backendId",
83
+ type: "input",
84
+ default: "my-web-app",
85
+ message: "Provide a name for your backend [1-30 characters]",
86
+ });
87
+ (0, utils_1.logSuccess)(`Name set to ${backendId}\n`);
88
+ const webApp = await app_1.webApps.getOrCreateWebApp(projectId, webAppName, backendId);
89
+ if (!webApp) {
90
+ (0, utils_1.logWarning)(`Firebase web app not set`);
91
+ }
92
+ const createBackendSpinner = ora("Creating your new backend...").start();
93
+ const backend = await createBackend(projectId, location, backendId, gitRepositoryLink, serviceAccount, webApp === null || webApp === void 0 ? void 0 : webApp.id, rootDir);
94
+ createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
95
+ await setDefaultTrafficPolicy(projectId, location, backendId, branch);
96
+ const confirmRollout = await (0, prompt_1.promptOnce)({
97
+ type: "confirm",
98
+ name: "rollout",
99
+ default: true,
100
+ message: "Do you want to deploy now?",
101
+ });
102
+ if (!confirmRollout) {
103
+ (0, utils_1.logSuccess)(`Your backend will be deployed at:\n\thttps://${backend.uri}`);
104
+ return;
105
+ }
106
+ const url = `https://${backend.uri}`;
107
+ (0, utils_1.logBullet)(`You may also track this rollout at:\n\t${(0, api_1.consoleOrigin)()}/project/${projectId}/apphosting`);
108
+ const createRolloutSpinner = ora("Starting a new rollout; this may take a few minutes. It's safe to exit now.").start();
109
+ await (0, rollout_1.orchestrateRollout)({
110
+ projectId,
111
+ location,
112
+ backendId,
113
+ buildInput: {
114
+ source: {
115
+ codebase: {
116
+ branch,
117
+ },
118
+ },
119
+ },
120
+ isFirstRollout: true,
121
+ });
122
+ createRolloutSpinner.succeed("Rollout complete");
123
+ if (!(await tlsReady(url))) {
124
+ const tlsSpinner = ora("Finalizing your backend's TLS certificate; this may take a few minutes.").start();
125
+ await awaitTlsReady(url);
126
+ tlsSpinner.succeed("TLS certificate ready");
127
+ }
128
+ (0, utils_1.logSuccess)(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
129
+ }
130
+ exports.doSetup = doSetup;
131
+ async function createGitRepoLink(projectId, location, connectionId) {
132
+ await Promise.all([
133
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true),
134
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.secretManagerOrigin)(), "apphosting", true),
135
+ (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.iamOrigin)(), "apphosting", true),
136
+ ]);
137
+ const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
138
+ if (location) {
139
+ if (!allowedLocations.includes(location)) {
140
+ throw new error_1.FirebaseError(`Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`);
141
+ }
142
+ }
143
+ location =
144
+ location ||
145
+ (await promptLocation(projectId, "Select a location for your GitRepoLink's connection:\n"));
146
+ await githubConnections.linkGitHubRepository(projectId, location, connectionId);
147
+ }
148
+ exports.createGitRepoLink = createGitRepoLink;
149
+ async function ensureAppHostingComputeServiceAccount(projectId, serviceAccount) {
150
+ const sa = serviceAccount || defaultComputeServiceAccountEmail(projectId);
151
+ const name = `projects/${projectId}/serviceAccounts/${sa}`;
152
+ try {
153
+ await iam.testResourceIamPermissions((0, api_1.iamOrigin)(), "v1", name, ["iam.serviceAccounts.actAs"], `projects/${projectId}`);
154
+ }
155
+ catch (err) {
156
+ if (!(err instanceof error_1.FirebaseError)) {
157
+ throw err;
158
+ }
159
+ if (err.status === 404) {
160
+ await provisionDefaultComputeServiceAccount(projectId);
161
+ }
162
+ else if (err.status === 403) {
163
+ throw new error_1.FirebaseError(`Failed to create backend due to missing delegation permissions for ${sa}. Make sure you have the iam.serviceAccounts.actAs permission.`, { original: err });
164
+ }
165
+ }
166
+ }
167
+ exports.ensureAppHostingComputeServiceAccount = ensureAppHostingComputeServiceAccount;
168
+ async function promptNewBackendId(projectId, location, prompt) {
169
+ while (true) {
170
+ const backendId = await (0, prompt_1.promptOnce)(prompt);
171
+ try {
172
+ await apphosting.getBackend(projectId, location, backendId);
173
+ }
174
+ catch (err) {
175
+ if (err.status === 404) {
176
+ return backendId;
177
+ }
178
+ throw new error_1.FirebaseError(`Failed to check if backend with id ${backendId} already exists in ${location}`, { original: err });
179
+ }
180
+ (0, utils_1.logWarning)(`Backend with id ${backendId} already exists in ${location}`);
181
+ }
182
+ }
183
+ function defaultComputeServiceAccountEmail(projectId) {
184
+ return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
185
+ }
186
+ async function createBackend(projectId, location, backendId, repository, serviceAccount, webAppId, rootDir = "/") {
187
+ const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
188
+ const backendReqBody = {
189
+ servingLocality: "GLOBAL_ACCESS",
190
+ codebase: {
191
+ repository: `${repository.name}`,
192
+ rootDirectory: rootDir,
193
+ },
194
+ labels: deploymentTool.labels(),
195
+ serviceAccount: serviceAccount || defaultServiceAccount,
196
+ appId: webAppId,
197
+ };
198
+ async function createBackendAndPoll() {
199
+ const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
200
+ return await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
201
+ }
202
+ return await createBackendAndPoll();
203
+ }
204
+ exports.createBackend = createBackend;
205
+ async function provisionDefaultComputeServiceAccount(projectId) {
206
+ try {
207
+ await iam.createServiceAccount(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Default service account used to run builds and deploys for Firebase App Hosting", "Firebase App Hosting compute service account");
208
+ }
209
+ catch (err) {
210
+ if (err.status !== 409) {
211
+ throw err;
212
+ }
213
+ }
214
+ await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), [
215
+ "roles/firebaseapphosting.computeRunner",
216
+ "roles/firebase.sdkAdminServiceAgent",
217
+ "roles/developerconnect.readTokenAccessor",
218
+ ], true);
219
+ }
220
+ async function setDefaultTrafficPolicy(projectId, location, backendId, codebaseBranch) {
221
+ const traffic = {
222
+ rolloutPolicy: {
223
+ codebaseBranch: codebaseBranch,
224
+ },
225
+ };
226
+ const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
227
+ await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
228
+ }
229
+ exports.setDefaultTrafficPolicy = setDefaultTrafficPolicy;
230
+ async function deleteBackendAndPoll(projectId, location, backendId) {
231
+ const op = await apphosting.deleteBackend(projectId, location, backendId);
232
+ await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `delete-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
233
+ }
234
+ exports.deleteBackendAndPoll = deleteBackendAndPoll;
235
+ async function promptLocation(projectId, prompt = "Please select a location:") {
236
+ const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
237
+ if (allowedLocations.length === 1) {
238
+ return allowedLocations[0];
239
+ }
240
+ const location = (await (0, prompt_1.promptOnce)({
241
+ name: "location",
242
+ type: "list",
243
+ default: constants_1.DEFAULT_LOCATION,
244
+ message: prompt,
245
+ choices: allowedLocations,
246
+ }));
247
+ (0, utils_1.logSuccess)(`Location set to ${location}.\n`);
248
+ return location;
249
+ }
250
+ exports.promptLocation = promptLocation;
251
+ async function getBackendForLocation(projectId, location, backendId) {
252
+ try {
253
+ return await apphosting.getBackend(projectId, location, backendId);
254
+ }
255
+ catch (err) {
256
+ throw new error_1.FirebaseError(`No backend named "${backendId}" found in ${location}.`, {
257
+ original: err,
258
+ });
259
+ }
260
+ }
261
+ exports.getBackendForLocation = getBackendForLocation;
262
+ async function getBackendForAmbiguousLocation(projectId, backendId, locationDisambugationPrompt, force) {
263
+ let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
264
+ if (unreachable && unreachable.length !== 0) {
265
+ (0, utils_1.logWarning)(`The following locations are currently unreachable: ${unreachable}.\n` +
266
+ "If your backend is in one of these regions, please try again later.");
267
+ }
268
+ backends = backends.filter((backend) => apphosting.parseBackendName(backend.name).id === backendId);
269
+ if (backends.length === 0) {
270
+ throw new error_1.FirebaseError(`No backend named "${backendId}" found.`);
271
+ }
272
+ if (backends.length === 1) {
273
+ return backends[0];
274
+ }
275
+ if (force) {
276
+ throw new error_1.FirebaseError(`Multiple backends found with ID ${backendId}. Please specify the region of your target backend.`);
277
+ }
278
+ const backendsByLocation = new Map();
279
+ backends.forEach((backend) => backendsByLocation.set(apphosting.parseBackendName(backend.name).location, backend));
280
+ const location = await (0, prompt_1.promptOnce)({
281
+ name: "location",
282
+ type: "list",
283
+ message: locationDisambugationPrompt,
284
+ choices: [...backendsByLocation.keys()],
285
+ });
286
+ return backendsByLocation.get(location);
287
+ }
288
+ exports.getBackendForAmbiguousLocation = getBackendForAmbiguousLocation;
@@ -1,15 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.maybeAddSecretToYaml = exports.upsertEnv = exports.findEnv = exports.store = exports.load = exports.yamlPath = void 0;
3
+ exports.loadConfigForEnvironment = exports.maybeAddSecretToYaml = exports.upsertEnv = exports.findEnv = exports.store = exports.load = exports.discoverConfigsAtBackendRoot = exports.discoverBackendRoot = exports.APPHOSTING_YAML_FILE_REGEX = exports.APPHOSTING_LOCAL_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
7
  const fs = require("../fsutils");
8
8
  const prompt = require("../prompt");
9
9
  const dialogs = require("./secrets/dialogs");
10
- function yamlPath(cwd) {
10
+ const yaml_1 = require("./yaml");
11
+ const error_1 = require("../error");
12
+ exports.APPHOSTING_BASE_YAML_FILE = "apphosting.yaml";
13
+ exports.APPHOSTING_LOCAL_YAML_FILE = "apphosting.local.yaml";
14
+ exports.APPHOSTING_YAML_FILE_REGEX = /^apphosting(\.[a-z0-9_]+)?\.yaml$/;
15
+ function discoverBackendRoot(cwd) {
11
16
  let dir = cwd;
12
- while (!fs.fileExistsSync((0, path_1.resolve)(dir, "apphosting.yaml"))) {
17
+ while (!fs.fileExistsSync((0, path_1.resolve)(dir, exports.APPHOSTING_BASE_YAML_FILE))) {
13
18
  if (fs.fileExistsSync((0, path_1.resolve)(dir, "firebase.json"))) {
14
19
  return null;
15
20
  }
@@ -19,9 +24,23 @@ function yamlPath(cwd) {
19
24
  }
20
25
  dir = parent;
21
26
  }
22
- return (0, path_1.resolve)(dir, "apphosting.yaml");
27
+ return dir;
28
+ }
29
+ exports.discoverBackendRoot = discoverBackendRoot;
30
+ function discoverConfigsAtBackendRoot(cwd) {
31
+ const backendRoot = discoverBackendRoot(cwd);
32
+ if (!backendRoot) {
33
+ throw new error_1.FirebaseError("Unable to find your project's root, ensure the apphosting.yaml config is initialized. Try 'firebase init apphosting'");
34
+ }
35
+ return listAppHostingFilesInPath(backendRoot);
36
+ }
37
+ exports.discoverConfigsAtBackendRoot = discoverConfigsAtBackendRoot;
38
+ function listAppHostingFilesInPath(path) {
39
+ return fs
40
+ .listFiles(path)
41
+ .filter((file) => exports.APPHOSTING_YAML_FILE_REGEX.test(file))
42
+ .map((file) => (0, path_1.join)(path, file));
23
43
  }
24
- exports.yamlPath = yamlPath;
25
44
  function load(yamlPath) {
26
45
  const raw = fs.readFile(yamlPath);
27
46
  return yaml.parseDocument(raw);
@@ -62,9 +81,11 @@ function upsertEnv(document, env) {
62
81
  exports.upsertEnv = upsertEnv;
63
82
  async function maybeAddSecretToYaml(secretName) {
64
83
  const dynamicDispatch = exports;
65
- let path = dynamicDispatch.yamlPath(process.cwd());
84
+ const backendRoot = dynamicDispatch.discoverBackendRoot(process.cwd());
85
+ let path;
66
86
  let projectYaml;
67
- if (path) {
87
+ if (backendRoot) {
88
+ path = (0, path_1.join)(backendRoot, exports.APPHOSTING_BASE_YAML_FILE);
68
89
  projectYaml = dynamicDispatch.load(path);
69
90
  }
70
91
  else {
@@ -85,7 +106,7 @@ async function maybeAddSecretToYaml(secretName) {
85
106
  message: "It looks like you don't have an apphosting.yaml yet. Where would you like to store it?",
86
107
  default: process.cwd(),
87
108
  });
88
- path = (0, path_1.join)(path, "apphosting.yaml");
109
+ path = (0, path_1.join)(path, exports.APPHOSTING_BASE_YAML_FILE);
89
110
  }
90
111
  const envName = await dialogs.envVarForSecret(secretName);
91
112
  dynamicDispatch.upsertEnv(projectYaml, {
@@ -95,3 +116,13 @@ async function maybeAddSecretToYaml(secretName) {
95
116
  dynamicDispatch.store(path, projectYaml);
96
117
  }
97
118
  exports.maybeAddSecretToYaml = maybeAddSecretToYaml;
119
+ async function loadConfigForEnvironment(envYamlPath, baseYamlPath) {
120
+ const envYamlConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(envYamlPath);
121
+ if (baseYamlPath) {
122
+ const baseConfig = await yaml_1.AppHostingYamlConfig.loadFromFile(baseYamlPath);
123
+ baseConfig.merge(envYamlConfig);
124
+ return baseConfig;
125
+ }
126
+ return envYamlConfig;
127
+ }
128
+ exports.loadConfigForEnvironment = loadConfigForEnvironment;
@@ -264,19 +264,25 @@ async function promptCloneUri(projectId, connection) {
264
264
  return cloneUri;
265
265
  }
266
266
  async function promptGitHubBranch(repoLink) {
267
+ var _a;
267
268
  const branches = await devConnect.listAllBranches(repoLink.name);
268
- while (true) {
269
- const branch = await (0, prompt_1.promptOnce)({
270
- name: "branch",
271
- type: "input",
272
- default: "main",
273
- message: "Pick a branch for continuous deployment",
274
- });
275
- if (branches.has(branch)) {
276
- return branch;
277
- }
278
- utils.logWarning(`The branch "${branch}" does not exist on "${extractRepoSlugFromUri(repoLink.cloneUri)}". Please enter a valid branch for this repo.`);
279
- }
269
+ const branch = await (0, prompt_1.promptOnce)({
270
+ type: "autocomplete",
271
+ name: "branch",
272
+ message: "Pick a branch for continuous deployment",
273
+ source: (_, input = "") => {
274
+ return new Promise((resolve) => resolve([
275
+ ...fuzzy.filter(input, Array.from(branches)).map((result) => {
276
+ return {
277
+ name: result.original,
278
+ value: result.original,
279
+ };
280
+ }),
281
+ ]));
282
+ },
283
+ });
284
+ utils.logWarning(`The branch "${branch}" does not exist on "${(_a = extractRepoSlugFromUri(repoLink.cloneUri)) !== null && _a !== void 0 ? _a : ""}". Please enter a valid branch for this repo.`);
285
+ return branch;
280
286
  }
281
287
  exports.promptGitHubBranch = promptGitHubBranch;
282
288
  async function ensureSecretManagerAdminGrant(projectId) {