firebase-tools 13.7.1 → 13.7.3

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 (34) hide show
  1. package/lib/apphosting/app.js +92 -0
  2. package/lib/apphosting/config.js +76 -10
  3. package/lib/apphosting/githubConnections.js +4 -4
  4. package/lib/apphosting/index.js +29 -11
  5. package/lib/apphosting/repo.js +1 -1
  6. package/lib/apphosting/secrets/dialogs.js +15 -14
  7. package/lib/commands/apphosting-backends-create.js +4 -2
  8. package/lib/commands/apphosting-backends-delete.js +4 -3
  9. package/lib/commands/apphosting-backends-get.js +8 -8
  10. package/lib/commands/apphosting-backends-list.js +2 -2
  11. package/lib/commands/apphosting-secrets-grantaccess.js +2 -2
  12. package/lib/commands/apphosting-secrets-set.js +9 -58
  13. package/lib/commands/firestore-databases-create.js +1 -0
  14. package/lib/commands/firestore-databases-restore.js +1 -0
  15. package/lib/commands/functions-secrets-set.js +1 -17
  16. package/lib/commands/index.js +8 -6
  17. package/lib/deploy/firestore/prepare.js +4 -2
  18. package/lib/deploy/functions/deploy.js +2 -2
  19. package/lib/deploy/functions/prepare.js +1 -0
  20. package/lib/deploy/functions/pricing.js +2 -2
  21. package/lib/deploy/functions/runtimes/discovery/index.js +3 -3
  22. package/lib/deploy/functions/runtimes/{supported.js → supported/index.js} +26 -81
  23. package/lib/deploy/functions/runtimes/supported/types.js +74 -0
  24. package/lib/downloadUtils.js +1 -0
  25. package/lib/extensions/emulator/specHelper.js +3 -3
  26. package/lib/extensions/extensionsApi.js +2 -2
  27. package/lib/extensions/localHelper.js +3 -3
  28. package/lib/firestore/pretty-print.js +5 -0
  29. package/lib/frameworks/astro/index.js +1 -1
  30. package/lib/frameworks/flutter/index.js +2 -2
  31. package/lib/init/features/hosting/github.js +4 -5
  32. package/lib/utils.js +17 -0
  33. package/package.json +3 -3
  34. package/templates/init/functions/typescript/_eslintrc +1 -0
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ const fuzzy = require("fuzzy");
3
+ const inquirer = require("inquirer");
4
+ const apps_1 = require("../management/apps");
5
+ const prompt_1 = require("../prompt");
6
+ const error_1 = require("../error");
7
+ const CREATE_NEW_FIREBASE_WEB_APP = "CREATE_NEW_WEB_APP";
8
+ const CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP = "CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP";
9
+ const webApps = {
10
+ CREATE_NEW_FIREBASE_WEB_APP,
11
+ CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP,
12
+ getOrCreateWebApp,
13
+ promptFirebaseWebApp,
14
+ };
15
+ async function getOrCreateWebApp(projectId, firebaseWebAppName, backendId) {
16
+ const webAppsInProject = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.WEB);
17
+ if (webAppsInProject.length === 0) {
18
+ const { displayName, appId } = await createFirebaseWebApp(projectId, {
19
+ displayName: backendId,
20
+ });
21
+ return { name: displayName, id: appId };
22
+ }
23
+ const existingUserProjectWebApps = new Map(webAppsInProject.map((obj) => {
24
+ var _a;
25
+ return [
26
+ (_a = obj.displayName) !== null && _a !== void 0 ? _a : obj.appId,
27
+ obj.appId,
28
+ ];
29
+ }));
30
+ if (firebaseWebAppName) {
31
+ if (existingUserProjectWebApps.get(firebaseWebAppName) === undefined) {
32
+ throw new error_1.FirebaseError(`The web app '${firebaseWebAppName}' does not exist in project ${projectId}`);
33
+ }
34
+ return { name: firebaseWebAppName, id: existingUserProjectWebApps.get(firebaseWebAppName) };
35
+ }
36
+ return await webApps.promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps);
37
+ }
38
+ async function promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps) {
39
+ const existingWebAppKeys = Array.from(existingUserProjectWebApps.keys());
40
+ const firebaseWebAppName = await (0, prompt_1.promptOnce)({
41
+ type: "autocomplete",
42
+ name: "app",
43
+ message: "Which of the following Firebase web apps would you like to associate your backend with?",
44
+ source: (_, input = "") => {
45
+ return new Promise((resolve) => resolve([
46
+ new inquirer.Separator(),
47
+ {
48
+ name: "Create a new Firebase web app.",
49
+ value: CREATE_NEW_FIREBASE_WEB_APP,
50
+ },
51
+ {
52
+ name: "Continue without a Firebase web app.",
53
+ value: CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP,
54
+ },
55
+ new inquirer.Separator(),
56
+ ...fuzzy.filter(input, existingWebAppKeys).map((result) => {
57
+ return result.original;
58
+ }),
59
+ ]));
60
+ },
61
+ });
62
+ if (firebaseWebAppName === CREATE_NEW_FIREBASE_WEB_APP) {
63
+ const newFirebaseWebApp = await createFirebaseWebApp(projectId, { displayName: backendId });
64
+ return { name: newFirebaseWebApp.displayName, id: newFirebaseWebApp.appId };
65
+ }
66
+ else if (firebaseWebAppName === CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP) {
67
+ return;
68
+ }
69
+ return { name: firebaseWebAppName, id: existingUserProjectWebApps.get(firebaseWebAppName) };
70
+ }
71
+ async function createFirebaseWebApp(projectId, options) {
72
+ try {
73
+ return await (0, apps_1.createWebApp)(projectId, options);
74
+ }
75
+ catch (e) {
76
+ if (isQuotaError(e)) {
77
+ throw new error_1.FirebaseError("Unable to create a new web app, the project has reached the quota for Firebase apps. Navigate to your Firebase console to manage or delete a Firebase app to continue. ", { original: e instanceof Error ? e : undefined });
78
+ }
79
+ throw new error_1.FirebaseError("Unable to create a Firebase web app", {
80
+ original: e instanceof Error ? e : undefined,
81
+ });
82
+ }
83
+ }
84
+ function isQuotaError(error) {
85
+ var _a, _b, _c, _d, _e;
86
+ const original = error.original;
87
+ const code = (original === null || original === void 0 ? void 0 : original.status) ||
88
+ ((_b = (_a = original === null || original === void 0 ? void 0 : original.context) === null || _a === void 0 ? void 0 : _a.response) === null || _b === void 0 ? void 0 : _b.statusCode) ||
89
+ ((_e = (_d = (_c = original === null || original === void 0 ? void 0 : original.context) === null || _c === void 0 ? void 0 : _c.body) === null || _d === void 0 ? void 0 : _d.error) === null || _e === void 0 ? void 0 : _e.code);
90
+ return code === 429;
91
+ }
92
+ module.exports = webApps;
@@ -1,31 +1,97 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.store = exports.load = exports.yamlPath = void 0;
4
- const path = require("path");
3
+ exports.maybeAddSecretToYaml = exports.upsertEnv = exports.findEnv = exports.store = exports.load = exports.yamlPath = void 0;
4
+ const path_1 = require("path");
5
5
  const fs_1 = require("fs");
6
- const yaml = require("js-yaml");
6
+ const yaml = require("yaml");
7
7
  const fs = require("../fsutils");
8
+ const prompt = require("../prompt");
9
+ const dialogs = require("./secrets/dialogs");
8
10
  function yamlPath(cwd) {
9
11
  let dir = cwd;
10
- while (!fs.fileExistsSync(path.resolve(dir, "apphosting.yaml"))) {
11
- if (fs.fileExistsSync(path.resolve(dir, "firebase.json"))) {
12
+ while (!fs.fileExistsSync((0, path_1.resolve)(dir, "apphosting.yaml"))) {
13
+ if (fs.fileExistsSync((0, path_1.resolve)(dir, "firebase.json"))) {
12
14
  return null;
13
15
  }
14
- const parent = path.dirname(dir);
16
+ const parent = (0, path_1.dirname)(dir);
15
17
  if (parent === dir) {
16
18
  return null;
17
19
  }
18
20
  dir = parent;
19
21
  }
20
- return path.resolve(dir, "apphosting.yaml");
22
+ return (0, path_1.resolve)(dir, "apphosting.yaml");
21
23
  }
22
24
  exports.yamlPath = yamlPath;
23
25
  function load(yamlPath) {
24
26
  const raw = fs.readFile(yamlPath);
25
- return yaml.load(raw, yaml.DEFAULT_FULL_SCHEMA);
27
+ return yaml.parseDocument(raw);
26
28
  }
27
29
  exports.load = load;
28
- function store(yamlPath, config) {
29
- (0, fs_1.writeFileSync)(yamlPath, yaml.dump(config));
30
+ function store(yamlPath, document) {
31
+ (0, fs_1.writeFileSync)(yamlPath, document.toString());
30
32
  }
31
33
  exports.store = store;
34
+ function findEnv(document, variable) {
35
+ if (!document.has("env")) {
36
+ return undefined;
37
+ }
38
+ const envs = document.get("env");
39
+ for (const env of envs.items) {
40
+ if (env.get("variable") === variable) {
41
+ return env.toJSON();
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+ exports.findEnv = findEnv;
47
+ function upsertEnv(document, env) {
48
+ if (!document.has("env")) {
49
+ document.set("env", document.createNode([env]));
50
+ return;
51
+ }
52
+ const envs = document.get("env");
53
+ const envYaml = document.createNode(env);
54
+ for (let i = 0; i < envs.items.length; i++) {
55
+ if (envs.items[i].get("variable") === env.variable) {
56
+ envs.set(i, envYaml);
57
+ return;
58
+ }
59
+ }
60
+ envs.add(envYaml);
61
+ }
62
+ exports.upsertEnv = upsertEnv;
63
+ async function maybeAddSecretToYaml(secretName) {
64
+ const dynamicDispatch = exports;
65
+ let path = dynamicDispatch.yamlPath(process.cwd());
66
+ let projectYaml;
67
+ if (path) {
68
+ projectYaml = dynamicDispatch.load(path);
69
+ }
70
+ else {
71
+ projectYaml = new yaml.Document();
72
+ }
73
+ if (dynamicDispatch.findEnv(projectYaml, secretName)) {
74
+ return;
75
+ }
76
+ const addToYaml = await prompt.confirm({
77
+ message: "Would you like to add this secret to apphosting.yaml?",
78
+ default: true,
79
+ });
80
+ if (!addToYaml) {
81
+ return;
82
+ }
83
+ if (!path) {
84
+ path = await prompt.promptOnce({
85
+ message: "It looks like you don't have an apphosting.yaml yet. Where would you like to store it?",
86
+ default: process.cwd(),
87
+ });
88
+ path = (0, path_1.join)(path, "apphosting.yaml");
89
+ }
90
+ const envName = await dialogs.envVarForSecret(secretName);
91
+ dynamicDispatch.upsertEnv(projectYaml, {
92
+ variable: envName,
93
+ secret: secretName,
94
+ });
95
+ dynamicDispatch.store(path, projectYaml);
96
+ }
97
+ exports.maybeAddSecretToYaml = maybeAddSecretToYaml;
@@ -64,7 +64,7 @@ async function linkGitHubRepository(projectId, location) {
64
64
  let connection;
65
65
  do {
66
66
  if (repoCloneUri === ADD_CONN_CHOICE) {
67
- existingConns.push(await createFullyInstalledConnection(projectId, location, generateConnectionId(), oauthConn));
67
+ existingConns.push(await createFullyInstalledConnection(projectId, location, generateConnectionId(), oauthConn, true));
68
68
  }
69
69
  const selection = await promptCloneUri(projectId, existingConns);
70
70
  repoCloneUri = selection.cloneUri;
@@ -81,10 +81,10 @@ async function linkGitHubRepository(projectId, location) {
81
81
  return repo;
82
82
  }
83
83
  exports.linkGitHubRepository = linkGitHubRepository;
84
- async function createFullyInstalledConnection(projectId, location, connectionId, oauthConn) {
84
+ async function createFullyInstalledConnection(projectId, location, connectionId, oauthConn, withNewInstallation = false) {
85
85
  var _a, _b;
86
86
  let conn = await createConnection(projectId, location, connectionId, {
87
- appInstallationId: (_a = oauthConn.githubConfig) === null || _a === void 0 ? void 0 : _a.appInstallationId,
87
+ appInstallationId: withNewInstallation ? undefined : (_a = oauthConn.githubConfig) === null || _a === void 0 ? void 0 : _a.appInstallationId,
88
88
  authorizerCredential: (_b = oauthConn.githubConfig) === null || _b === void 0 ? void 0 : _b.authorizerCredential,
89
89
  });
90
90
  while (conn.installationState.stage !== "COMPLETE") {
@@ -135,7 +135,7 @@ async function promptCloneUri(projectId, connections) {
135
135
  const cloneUri = await (0, prompt_1.promptOnce)({
136
136
  type: "autocomplete",
137
137
  name: "cloneUri",
138
- message: "Which of the following repositories would you like to deploy?",
138
+ message: "Which repository would you like to deploy?",
139
139
  source: (_, input = "") => {
140
140
  return new Promise((resolve) => resolve([
141
141
  new inquirer.Separator(),
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.doSetup = void 0;
4
- const clc = require("colorette");
3
+ exports.deleteBackendAndPoll = exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.doSetup = void 0;
5
4
  const repo = require("./repo");
6
5
  const poller = require("../operation-poller");
7
6
  const apphosting = require("../gcp/apphosting");
@@ -16,6 +15,7 @@ const prompt_1 = require("../prompt");
16
15
  const constants_1 = require("./constants");
17
16
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
18
17
  const deploymentTool = require("../deploymentTool");
18
+ const apps = require("./app");
19
19
  const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
20
20
  const apphostingPollerOptions = {
21
21
  apiOrigin: (0, api_1.apphostingOrigin)(),
@@ -23,7 +23,7 @@ const apphostingPollerOptions = {
23
23
  masterTimeout: 25 * 60 * 1000,
24
24
  maxBackoff: 10000,
25
25
  };
26
- async function doSetup(projectId, location, serviceAccount, withDevConnect) {
26
+ async function doSetup(projectId, webAppName, location, serviceAccount, withDevConnect) {
27
27
  await Promise.all([
28
28
  ...(withDevConnect ? [(0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.developerConnectOrigin)(), "apphosting", true)] : []),
29
29
  (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.cloudbuildOrigin)(), "apphosting", true),
@@ -37,15 +37,14 @@ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
37
37
  throw new error_1.FirebaseError(`Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`);
38
38
  }
39
39
  }
40
- (0, utils_1.logBullet)("First we need a few details to create your backend.");
40
+ (0, utils_1.logBullet)("First we need a few details to create your backend.\n");
41
41
  location =
42
42
  location ||
43
43
  (await (0, prompt_1.promptOnce)({
44
44
  name: "region",
45
45
  type: "list",
46
46
  default: constants_1.DEFAULT_REGION,
47
- message: "Please select a region " +
48
- `(${clc.yellow("info")}: Your region determines where your backend is located):\n`,
47
+ message: "Select a region to host your backend:\n",
49
48
  choices: allowedLocations.map((loc) => ({ value: loc })),
50
49
  }));
51
50
  (0, utils_1.logSuccess)(`Region set to ${location}.\n`);
@@ -55,10 +54,23 @@ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
55
54
  default: "my-web-app",
56
55
  message: "Create a name for your backend [1-30 characters]",
57
56
  });
57
+ const webApp = await apps.getOrCreateWebApp(projectId, webAppName, backendId);
58
+ if (webApp) {
59
+ (0, utils_1.logSuccess)(`Firebase web app set to ${webApp.name}.\n`);
60
+ }
61
+ else {
62
+ (0, utils_1.logWarning)(`Firebase web app not set`);
63
+ }
58
64
  const gitRepositoryConnection = withDevConnect
59
65
  ? await githubConnections.linkGitHubRepository(projectId, location)
60
66
  : await repo.linkGitHubRepository(projectId, location);
61
- const backend = await createBackend(projectId, location, backendId, gitRepositoryConnection, serviceAccount);
67
+ const rootDir = await (0, prompt_1.promptOnce)({
68
+ name: "rootDir",
69
+ type: "input",
70
+ default: "/",
71
+ message: "Specify your app's root directory relative to your repository",
72
+ });
73
+ const backend = await createBackend(projectId, location, backendId, gitRepositoryConnection, serviceAccount, webApp === null || webApp === void 0 ? void 0 : webApp.id, rootDir);
62
74
  const branch = await (0, prompt_1.promptOnce)({
63
75
  name: "branch",
64
76
  type: "input",
@@ -74,7 +86,7 @@ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
74
86
  });
75
87
  if (!confirmRollout) {
76
88
  (0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
77
- (0, utils_1.logSuccess)(`Your site will be deployed at:\n\thttps://${backend.uri}`);
89
+ (0, utils_1.logSuccess)(`Your backend will be deployed at:\n\thttps://${backend.uri}`);
78
90
  return;
79
91
  }
80
92
  await orchestrateRollout(projectId, location, backendId, {
@@ -85,7 +97,7 @@ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
85
97
  },
86
98
  });
87
99
  (0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
88
- (0, utils_1.logSuccess)(`Your site is now deployed at:\n\thttps://${backend.uri}`);
100
+ (0, utils_1.logSuccess)(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
89
101
  }
90
102
  exports.doSetup = doSetup;
91
103
  async function promptNewBackendId(projectId, location, prompt) {
@@ -106,16 +118,17 @@ async function promptNewBackendId(projectId, location, prompt) {
106
118
  function defaultComputeServiceAccountEmail(projectId) {
107
119
  return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
108
120
  }
109
- async function createBackend(projectId, location, backendId, repository, serviceAccount) {
121
+ async function createBackend(projectId, location, backendId, repository, serviceAccount, webAppId, rootDir = "/") {
110
122
  const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
111
123
  const backendReqBody = {
112
124
  servingLocality: "GLOBAL_ACCESS",
113
125
  codebase: {
114
126
  repository: `${repository.name}`,
115
- rootDirectory: "/",
127
+ rootDirectory: rootDir,
116
128
  },
117
129
  labels: deploymentTool.labels(),
118
130
  serviceAccount: serviceAccount || defaultServiceAccount,
131
+ appId: webAppId,
119
132
  };
120
133
  delete backendReqBody.serviceAccount;
121
134
  async function createBackendAndPoll() {
@@ -215,3 +228,8 @@ async function orchestrateRollout(projectId, location, backendId, buildInput) {
215
228
  return { rollout, build };
216
229
  }
217
230
  exports.orchestrateRollout = orchestrateRollout;
231
+ async function deleteBackendAndPoll(projectId, location, backendId) {
232
+ const op = await apphosting.deleteBackend(projectId, location, backendId);
233
+ await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `delete-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
234
+ }
235
+ exports.deleteBackendAndPoll = deleteBackendAndPoll;
@@ -132,7 +132,7 @@ async function promptRepositoryUri(projectId, connections) {
132
132
  const remoteUri = await (0, prompt_1.promptOnce)({
133
133
  type: "autocomplete",
134
134
  name: "remoteUri",
135
- message: "Which of the following repositories would you like to deploy?",
135
+ message: "Which repository would you like to deploy?",
136
136
  source: (_, input = "") => {
137
137
  return new Promise((resolve) => resolve([
138
138
  new inquirer.Separator(),
@@ -65,42 +65,43 @@ function selectFromMetadata(input, selected) {
65
65
  };
66
66
  }
67
67
  exports.selectFromMetadata = selectFromMetadata;
68
- exports.WARN_NO_BACKENDS = "To use this secret, your backend's service account must have secret accessor permission. " +
68
+ exports.WARN_NO_BACKENDS = "To use this secret, your backend's service account must be granted access." +
69
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");
70
+ clc.bold("firebase apphosting:secrets:grantaccess");
71
71
  exports.GRANT_ACCESS_IN_FUTURE = `To grant access in the future, run ${clc.bold("firebase apphosting:secrets:grantaccess")}`;
72
72
  async function selectBackendServiceAccounts(projectNumber, projectId, options) {
73
73
  const listBackends = await apphosting.listBackends(projectId, "-");
74
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`);
75
+ utils.logWarning(`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
77
  }
78
78
  if (!listBackends.backends.length) {
79
- utils.logLabeledWarning("apphosting", exports.WARN_NO_BACKENDS);
79
+ utils.logWarning(exports.WARN_NO_BACKENDS);
80
80
  return { buildServiceAccounts: [], runServiceAccounts: [] };
81
81
  }
82
82
  if (listBackends.backends.length === 1) {
83
83
  const grant = await prompt.confirm({
84
84
  nonInteractive: options.nonInteractive,
85
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?",
86
+ message: "To use this secret, your backend's service account must be granted access. Would you like to grant access now?",
87
87
  });
88
88
  if (grant) {
89
89
  return (0, _1.toMulti)((0, _1.serviceAccountsForBackend)(projectNumber, listBackends.backends[0]));
90
90
  }
91
- utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
91
+ utils.logBullet(exports.GRANT_ACCESS_IN_FUTURE);
92
92
  return { buildServiceAccounts: [], runServiceAccounts: [] };
93
93
  }
94
94
  const metadata = toMetadata(projectNumber, listBackends.backends);
95
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 ") +
96
+ utils.logBullet("To use this secret, your backend's service account must be granted access.");
97
+ utils.logBullet("All of your backends share the following " +
98
+ (sameServiceAccount(metadata[0]) ? "service account: " : "service accounts: ") +
98
99
  serviceAccountDisplay(metadata[0]) +
99
- ". Granting access to one backend will grant access to all backends.");
100
+ ".\nGranting access to one backend will grant access to all backends.");
100
101
  const grant = await prompt.confirm({
101
102
  nonInteractive: options.nonInteractive,
102
103
  default: true,
103
- message: "Would you like to grant it now?",
104
+ message: "Would you like to grant access to all backends now?",
104
105
  });
105
106
  if (grant) {
106
107
  return selectFromMetadata(metadata, [
@@ -108,10 +109,10 @@ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
108
109
  metadata[0].runServiceAccount,
109
110
  ]);
110
111
  }
111
- utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
112
+ utils.logBullet(exports.GRANT_ACCESS_IN_FUTURE);
112
113
  return { buildServiceAccounts: [], runServiceAccounts: [] };
113
114
  }
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
+ utils.logBullet("To use this secret, your backend's service account must be granted access. Your backends use the following service accounts:");
115
116
  const tableData = tableForBackends(metadata);
116
117
  const table = new Table({
117
118
  head: tableData[0],
@@ -131,7 +132,7 @@ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
131
132
  choices: [...allAccounts.values()].sort(),
132
133
  });
133
134
  if (!chosen.length) {
134
- utils.logLabeledBullet("apphosting", exports.GRANT_ACCESS_IN_FUTURE);
135
+ utils.logBullet(exports.GRANT_ACCESS_IN_FUTURE);
135
136
  }
136
137
  return selectFromMetadata(metadata, chosen);
137
138
  }
@@ -7,7 +7,8 @@ const requireInteractive_1 = require("../requireInteractive");
7
7
  const apphosting_1 = require("../apphosting");
8
8
  const apphosting_2 = require("../gcp/apphosting");
9
9
  exports.command = new command_1.Command("apphosting:backends:create")
10
- .description("create a backend in a Firebase project")
10
+ .description("create a Firebase App Hosting backend")
11
+ .option("-a, --app <webApp>", "specify an existing Firebase web app to associate your App Hosting backend with")
11
12
  .option("-l, --location <location>", "specify the region of the backend", "")
12
13
  .option("-s, --service-account <serviceAccount>", "specify the service account used to run the server", "")
13
14
  .option("-w, --with-dev-connect", "use the Developer Connect flow insetad of Cloud Build Repositories (testing)", false)
@@ -15,8 +16,9 @@ exports.command = new command_1.Command("apphosting:backends:create")
15
16
  .before(requireInteractive_1.default)
16
17
  .action(async (options) => {
17
18
  const projectId = (0, projectUtils_1.needProjectId)(options);
19
+ const webApp = options.app;
18
20
  const location = options.location;
19
21
  const serviceAccount = options.serviceAccount;
20
22
  const withDevConnect = options.withDevConnect;
21
- await (0, apphosting_1.doSetup)(projectId, location, serviceAccount, withDevConnect);
23
+ await (0, apphosting_1.doSetup)(projectId, webApp, location, serviceAccount, withDevConnect);
22
24
  });
@@ -9,9 +9,10 @@ const constants_1 = require("../apphosting/constants");
9
9
  const utils = require("../utils");
10
10
  const apphosting = require("../gcp/apphosting");
11
11
  const apphosting_backends_list_1 = require("./apphosting-backends-list");
12
+ const apphosting_1 = require("../apphosting");
12
13
  exports.command = new command_1.Command("apphosting:backends:delete <backend>")
13
- .description("delete a backend from a Firebase project")
14
- .option("-l, --location <location>", "App Backend location", "")
14
+ .description("delete a Firebase App Hosting backend")
15
+ .option("-l, --location <location>", "specify the region of the backend", "")
15
16
  .withForce()
16
17
  .before(apphosting.ensureApiEnabled)
17
18
  .action(async (backendId, options) => {
@@ -52,7 +53,7 @@ exports.command = new command_1.Command("apphosting:backends:delete <backend>")
52
53
  throw new error_1.FirebaseError("Deletion Aborted");
53
54
  }
54
55
  try {
55
- await apphosting.deleteBackend(projectId, location, backendId);
56
+ await (0, apphosting_1.deleteBackendAndPoll)(projectId, location, backendId);
56
57
  utils.logSuccess(`Successfully deleted the backend: ${backendId}`);
57
58
  }
58
59
  catch (err) {
@@ -7,30 +7,30 @@ const error_1 = require("../error");
7
7
  const utils_1 = require("../utils");
8
8
  const apphosting = require("../gcp/apphosting");
9
9
  const apphosting_backends_list_1 = require("./apphosting-backends-list");
10
- exports.command = new command_1.Command("apphosting:backends:get <backendId>")
11
- .description("get backend details of a Firebase project")
12
- .option("-l, --location <location>", "app backend location", "-")
10
+ exports.command = new command_1.Command("apphosting:backends:get <backend>")
11
+ .description("print info about a Firebase App Hosting backend")
12
+ .option("-l, --location <location>", "backend location", "-")
13
13
  .before(apphosting.ensureApiEnabled)
14
- .action(async (backendId, options) => {
14
+ .action(async (backend, options) => {
15
15
  const projectId = (0, projectUtils_1.needProjectId)(options);
16
16
  const location = options.location;
17
17
  let backendsList = [];
18
18
  try {
19
19
  if (location !== "-") {
20
- const backendInRegion = await apphosting.getBackend(projectId, location, backendId);
20
+ const backendInRegion = await apphosting.getBackend(projectId, location, backend);
21
21
  backendsList.push(backendInRegion);
22
22
  }
23
23
  else {
24
24
  const resp = await apphosting.listBackends(projectId, "-");
25
25
  const allBackends = resp.backends || [];
26
- backendsList = allBackends.filter((bkd) => bkd.name.split("/").pop() === backendId);
26
+ backendsList = allBackends.filter((bkd) => bkd.name.split("/").pop() === backend);
27
27
  }
28
28
  }
29
29
  catch (err) {
30
- throw new error_1.FirebaseError(`Failed to get backend: ${backendId}. Please check the parameters you have provided.`, { original: err });
30
+ throw new error_1.FirebaseError(`Failed to get backend: ${backend}. Please check the parameters you have provided.`, { original: err });
31
31
  }
32
32
  if (backendsList.length === 0) {
33
- (0, utils_1.logWarning)(`Found no backend with id: ${backendId}`);
33
+ (0, utils_1.logWarning)(`Backend "${backend}" not found`);
34
34
  return;
35
35
  }
36
36
  (0, apphosting_backends_list_1.printBackendsTable)(backendsList);
@@ -10,8 +10,8 @@ const apphosting = require("../gcp/apphosting");
10
10
  const Table = require("cli-table");
11
11
  const TABLE_HEAD = ["Backend ID", "Repository", "Location", "URL", "Created Date", "Updated Date"];
12
12
  exports.command = new command_1.Command("apphosting:backends:list")
13
- .description("list backends of a Firebase project")
14
- .option("-l, --location <location>", "app Backend location", "-")
13
+ .description("list Firebase App Hosting backends")
14
+ .option("-l, --location <location>", "list backends in the specified location", "-")
15
15
  .before(apphosting.ensureApiEnabled)
16
16
  .action(async (options) => {
17
17
  var _a;
@@ -11,8 +11,8 @@ const apphosting = require("../gcp/apphosting");
11
11
  const secrets = require("../apphosting/secrets");
12
12
  exports.command = new command_1.Command("apphosting:secrets:grantaccess <secretName>")
13
13
  .description("grant service accounts permissions to the provided secret")
14
- .option("-l, --location <location>", "app backend location")
15
- .option("-b, --backend <backend>", "app backend name")
14
+ .option("-l, --location <location>", "backend location")
15
+ .option("-b, --backend <backend>", "backend name")
16
16
  .before(requireAuth_1.requireAuth)
17
17
  .before(secretManager.ensureApi)
18
18
  .before(apphosting.ensureApiEnabled)
@@ -1,23 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.command = void 0;
4
- const tty = require("tty");
5
4
  const clc = require("colorette");
6
- const path_1 = require("path");
7
5
  const command_1 = require("../command");
8
6
  const projectUtils_1 = require("../projectUtils");
9
7
  const requireAuth_1 = require("../requireAuth");
10
- const fs = require("fs");
11
8
  const gcsm = require("../gcp/secretManager");
12
9
  const apphosting = require("../gcp/apphosting");
13
10
  const requirePermissions_1 = require("../requirePermissions");
14
- const prompt_1 = require("../prompt");
15
11
  const secrets = require("../apphosting/secrets");
16
12
  const dialogs = require("../apphosting/secrets/dialogs");
17
13
  const config = require("../apphosting/config");
18
- const utils_1 = require("../utils");
14
+ const utils = require("../utils");
19
15
  exports.command = new command_1.Command("apphosting:secrets:set <secretName>")
20
- .description("grant service accounts permissions to the provided secret")
16
+ .description("create or update a secret for use in Firebase App Hosting")
21
17
  .option("-l, --location <location>", "optional location to retrict secret replication")
22
18
  .withForce("Automatically create a secret, grant permissions, and add to YAML.")
23
19
  .before(requireAuth_1.requireAuth)
@@ -33,73 +29,28 @@ exports.command = new command_1.Command("apphosting:secrets:set <secretName>")
33
29
  ])
34
30
  .option("--data-file <dataFile>", 'File path from which to read secret data. Set to "-" to read the secret data from stdin.')
35
31
  .action(async (secretName, options) => {
36
- var _a;
37
- const howToAccess = `You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}`)}`;
38
- const grantAccess = `To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantAccess")}`;
39
32
  const projectId = (0, projectUtils_1.needProjectId)(options);
40
33
  const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
41
34
  const created = await secrets.upsertSecret(projectId, secretName, options.location);
42
35
  if (created === null) {
43
36
  return;
44
37
  }
45
- let secretValue;
46
- if ((!options.dataFile || options.dataFile === "-") && tty.isatty(0)) {
47
- secretValue = await (0, prompt_1.promptOnce)({
48
- type: "password",
49
- message: `Enter a value for ${secretName}`,
50
- });
51
- }
52
- else {
53
- let dataFile = 0;
54
- if (options.dataFile && options.dataFile !== "-") {
55
- dataFile = options.dataFile;
56
- }
57
- secretValue = fs.readFileSync(dataFile, "utf-8");
58
- }
59
- if (created) {
60
- (0, utils_1.logSuccess)(`Created new secret projects/${projectId}/secrets/${secretName}`);
38
+ else if (created) {
39
+ utils.logSuccess(`Created new secret projects/${projectId}/secrets/${secretName}`);
61
40
  }
41
+ const secretValue = await utils.readSecretValue(`Enter a value for ${secretName}`, options.dataFile);
62
42
  const version = await gcsm.addVersion(projectId, secretName, secretValue);
63
- (0, utils_1.logSuccess)(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`);
64
- (0, utils_1.logSuccess)(howToAccess);
43
+ utils.logSuccess(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`);
44
+ utils.logBullet(`You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}\n`)}`);
65
45
  if (!created) {
66
- (0, utils_1.logWarning)(grantAccess);
67
46
  return;
68
47
  }
69
48
  const accounts = await dialogs.selectBackendServiceAccounts(projectNumber, projectId, options);
70
49
  if (!accounts.buildServiceAccounts.length && !accounts.runServiceAccounts.length) {
71
- (0, utils_1.logWarning)(grantAccess);
50
+ utils.logWarning(`To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantaccess")}`);
72
51
  }
73
52
  else {
74
53
  await secrets.grantSecretAccess(projectId, secretName, accounts);
75
54
  }
76
- let path = config.yamlPath(process.cwd());
77
- let yaml = {};
78
- if (path) {
79
- yaml = config.load(path);
80
- if ((_a = yaml.env) === null || _a === void 0 ? void 0 : _a.find((env) => env.variable === secretName)) {
81
- return;
82
- }
83
- }
84
- const addToYaml = await (0, prompt_1.confirm)({
85
- message: "Would you like to add this secret to apphosting.yaml?",
86
- default: true,
87
- });
88
- if (!addToYaml) {
89
- return;
90
- }
91
- if (!path) {
92
- path = await (0, prompt_1.promptOnce)({
93
- message: "It looks like you don't have an apphosting.yaml yet. Where would you like to store it?",
94
- default: process.cwd(),
95
- });
96
- path = (0, path_1.join)(path, "apphosting.yaml");
97
- }
98
- const envName = await dialogs.envVarForSecret(secretName);
99
- yaml.env = yaml.env || [];
100
- yaml.env.push({
101
- variable: envName,
102
- secret: secretName,
103
- });
104
- config.store(path, yaml);
55
+ await config.maybeAddSecretToYaml(secretName);
105
56
  });