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.
- package/lib/apphosting/backend.js +288 -0
- package/lib/apphosting/config.js +39 -8
- package/lib/apphosting/githubConnections.js +18 -12
- package/lib/apphosting/index.js +3 -278
- package/lib/apphosting/rollout.js +12 -9
- package/lib/apphosting/secrets/index.js +66 -1
- package/lib/apphosting/utils.js +41 -0
- package/lib/apphosting/yaml.js +83 -0
- package/lib/bin/firebase.js +2 -2
- package/lib/commands/apphosting-backends-create.js +4 -4
- package/lib/commands/apphosting-backends-delete.js +4 -14
- package/lib/commands/apphosting-config-export.js +51 -0
- package/lib/commands/apphosting-repos-create.js +4 -4
- package/lib/commands/apphosting-rollouts-create.js +3 -4
- package/lib/commands/apphosting-secrets-grantaccess.js +2 -2
- package/lib/commands/functions-secrets-access.js +2 -4
- package/lib/commands/index.js +4 -0
- package/lib/commands/init.js +6 -6
- package/lib/emulator/apphosting/config.js +11 -39
- package/lib/emulator/apphosting/serve.js +6 -2
- package/lib/emulator/commandUtils.js +2 -2
- package/lib/emulator/downloadableEmulators.js +9 -9
- package/lib/emulator/loggingEmulator.js +2 -2
- package/lib/frameworks/next/index.js +10 -2
- package/lib/frameworks/next/utils.js +7 -4
- package/lib/frameworks/vite/index.js +2 -2
- package/lib/fsutils.js +13 -1
- package/lib/gcp/apphosting.js +1 -1
- package/lib/init/features/apphosting.js +3 -0
- package/lib/utils.js +2 -2
- package/package.json +1 -2
package/lib/apphosting/index.js
CHANGED
|
@@ -1,280 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
const
|
|
5
|
-
|
|
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
|
-
stages: [
|
|
225
|
-
{
|
|
226
|
-
progression: "IMMEDIATE",
|
|
227
|
-
targetPercent: 100,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
|
|
233
|
-
await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
|
|
234
|
-
}
|
|
235
|
-
exports.setDefaultTrafficPolicy = setDefaultTrafficPolicy;
|
|
236
|
-
async function deleteBackendAndPoll(projectId, location, backendId) {
|
|
237
|
-
const op = await apphosting.deleteBackend(projectId, location, backendId);
|
|
238
|
-
await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `delete-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
|
|
239
|
-
}
|
|
240
|
-
exports.deleteBackendAndPoll = deleteBackendAndPoll;
|
|
241
|
-
async function promptLocation(projectId, prompt = "Please select a location:") {
|
|
242
|
-
const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
|
|
243
|
-
if (allowedLocations.length === 1) {
|
|
244
|
-
return allowedLocations[0];
|
|
245
|
-
}
|
|
246
|
-
const location = (await (0, prompt_1.promptOnce)({
|
|
247
|
-
name: "location",
|
|
248
|
-
type: "list",
|
|
249
|
-
default: constants_1.DEFAULT_LOCATION,
|
|
250
|
-
message: prompt,
|
|
251
|
-
choices: allowedLocations,
|
|
252
|
-
}));
|
|
253
|
-
(0, utils_1.logSuccess)(`Location set to ${location}.\n`);
|
|
254
|
-
return location;
|
|
255
|
-
}
|
|
256
|
-
exports.promptLocation = promptLocation;
|
|
257
|
-
async function getBackendForAmbiguousLocation(projectId, backendId, locationDisambugationPrompt) {
|
|
258
|
-
let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
|
|
259
|
-
if (unreachable && unreachable.length !== 0) {
|
|
260
|
-
(0, utils_1.logWarning)(`The following locations are currently unreachable: ${unreachable}.\n` +
|
|
261
|
-
"If your backend is in one of these regions, please try again later.");
|
|
262
|
-
}
|
|
263
|
-
backends = backends.filter((backend) => apphosting.parseBackendName(backend.name).id === backendId);
|
|
264
|
-
if (backends.length === 0) {
|
|
265
|
-
throw new error_1.FirebaseError(`No backend named "${backendId}" found.`);
|
|
266
|
-
}
|
|
267
|
-
if (backends.length === 1) {
|
|
268
|
-
return backends[0];
|
|
269
|
-
}
|
|
270
|
-
const backendsByLocation = new Map();
|
|
271
|
-
backends.forEach((backend) => backendsByLocation.set(apphosting.parseBackendName(backend.name).location, backend));
|
|
272
|
-
const location = await (0, prompt_1.promptOnce)({
|
|
273
|
-
name: "location",
|
|
274
|
-
type: "list",
|
|
275
|
-
message: locationDisambugationPrompt,
|
|
276
|
-
choices: [...backendsByLocation.keys()],
|
|
277
|
-
});
|
|
278
|
-
return backendsByLocation.get(location);
|
|
279
|
-
}
|
|
280
|
-
exports.getBackendForAmbiguousLocation = getBackendForAmbiguousLocation;
|
|
3
|
+
exports.setupBackend = void 0;
|
|
4
|
+
const backend_1 = require("./backend");
|
|
5
|
+
Object.defineProperty(exports, "setupBackend", { enumerable: true, get: function () { return backend_1.doSetup; } });
|
|
@@ -7,9 +7,9 @@ const ora = require("ora");
|
|
|
7
7
|
const devConnect_1 = require("../gcp/devConnect");
|
|
8
8
|
const githubConnections_1 = require("../apphosting/githubConnections");
|
|
9
9
|
const poller = require("../operation-poller");
|
|
10
|
-
const prompt_1 = require("../prompt");
|
|
11
10
|
const utils_1 = require("../utils");
|
|
12
11
|
const api_1 = require("../api");
|
|
12
|
+
const backend_1 = require("./backend");
|
|
13
13
|
const apphostingPollerOptions = {
|
|
14
14
|
apiOrigin: (0, api_1.apphostingOrigin)(),
|
|
15
15
|
apiVersion: apphosting.API_VERSION,
|
|
@@ -18,7 +18,14 @@ const apphostingPollerOptions = {
|
|
|
18
18
|
};
|
|
19
19
|
const GIT_COMMIT_SHA_REGEX = /^(?:[0-9a-f]{40}|[0-9a-f]{7})$/;
|
|
20
20
|
async function createRollout(backendId, projectId, location, branch, commit, force) {
|
|
21
|
-
|
|
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
|
+
}
|
|
22
29
|
if (!backend.codebase.repository) {
|
|
23
30
|
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'.`);
|
|
24
31
|
}
|
|
@@ -48,18 +55,14 @@ async function createRollout(backendId, projectId, location, branch, commit, for
|
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
else {
|
|
58
|
+
if (force) {
|
|
59
|
+
throw new error_1.FirebaseError(`Failed to create rollout with --force option because no target branch or commit was specified. Please specify which branch or commit to roll out with the --git-branch or --git-commit flag.`);
|
|
60
|
+
}
|
|
51
61
|
branch = await (0, githubConnections_1.promptGitHubBranch)(repoLink);
|
|
52
62
|
const branchInfo = await (0, githubConnections_1.getGitHubBranch)(owner, repo, branch, readToken.token);
|
|
53
63
|
targetCommit = branchInfo.commit;
|
|
54
64
|
}
|
|
55
65
|
(0, utils_1.logBullet)(`You are about to deploy [${targetCommit.sha.substring(0, 7)}]: ${targetCommit.commit.message}`);
|
|
56
|
-
const confirmRollout = await (0, prompt_1.confirm)({
|
|
57
|
-
force: !!force,
|
|
58
|
-
message: "Do you want to continue?",
|
|
59
|
-
});
|
|
60
|
-
if (!confirmRollout) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
66
|
(0, utils_1.logBullet)(`You may also track this rollout at:\n\t${(0, api_1.consoleOrigin)()}/project/${projectId}/apphosting`);
|
|
64
67
|
const createRolloutSpinner = ora("Starting a new rollout; this may take a few minutes. It's safe to exit now.").start();
|
|
65
68
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.upsertSecret = exports.grantSecretAccess = exports.serviceAccountsForBackend = exports.toMulti = void 0;
|
|
3
|
+
exports.getSecretNameParts = exports.loadConfigToExport = exports.fetchSecrets = exports.upsertSecret = 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");
|
|
@@ -10,6 +10,10 @@ const secretManager_1 = require("../../gcp/secretManager");
|
|
|
10
10
|
const secretManager_2 = require("../../gcp/secretManager");
|
|
11
11
|
const utils = require("../../utils");
|
|
12
12
|
const prompt = require("../../prompt");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const config_1 = require("../config");
|
|
15
|
+
const utils_1 = require("../utils");
|
|
16
|
+
const yaml_1 = require("../yaml");
|
|
13
17
|
function toMulti(accounts) {
|
|
14
18
|
const m = {
|
|
15
19
|
buildServiceAccounts: [accounts.buildServiceAccount],
|
|
@@ -102,3 +106,64 @@ async function upsertSecret(project, secret, location) {
|
|
|
102
106
|
return false;
|
|
103
107
|
}
|
|
104
108
|
exports.upsertSecret = upsertSecret;
|
|
109
|
+
async function fetchSecrets(projectId, secrets) {
|
|
110
|
+
let secretsKeyValuePairs;
|
|
111
|
+
try {
|
|
112
|
+
const secretPromises = secrets.map(async (secretConfig) => {
|
|
113
|
+
const [name, version] = getSecretNameParts(secretConfig.secret);
|
|
114
|
+
const value = await gcsm.accessSecretVersion(projectId, name, version);
|
|
115
|
+
return [secretConfig.variable, value];
|
|
116
|
+
});
|
|
117
|
+
const secretEntries = await Promise.all(secretPromises);
|
|
118
|
+
secretsKeyValuePairs = new Map(secretEntries);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
throw new error_1.FirebaseError(`Error exporting secrets`, {
|
|
122
|
+
original: e,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return secretsKeyValuePairs;
|
|
126
|
+
}
|
|
127
|
+
exports.fetchSecrets = fetchSecrets;
|
|
128
|
+
async function loadConfigToExport(cwd, userGivenConfigFile) {
|
|
129
|
+
if (userGivenConfigFile && !config_1.APPHOSTING_YAML_FILE_REGEX.test(userGivenConfigFile)) {
|
|
130
|
+
throw new error_1.FirebaseError("Invalid apphosting yaml config file provided. File must be in format: 'apphosting.yaml' or 'apphosting.<environment>.yaml'");
|
|
131
|
+
}
|
|
132
|
+
const allConfigs = discoverConfigs(cwd);
|
|
133
|
+
let userGivenConfigFilePath;
|
|
134
|
+
if (userGivenConfigFile) {
|
|
135
|
+
if (!allConfigs.has(userGivenConfigFile)) {
|
|
136
|
+
throw new error_1.FirebaseError(`The provided app hosting config file "${userGivenConfigFile}" does not exist`);
|
|
137
|
+
}
|
|
138
|
+
userGivenConfigFilePath = allConfigs.get(userGivenConfigFile);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
userGivenConfigFilePath = await (0, utils_1.promptForAppHostingYaml)(allConfigs, "Which environment would you like to export secrets from Secret Manager for?");
|
|
142
|
+
}
|
|
143
|
+
if (userGivenConfigFile === config_1.APPHOSTING_BASE_YAML_FILE) {
|
|
144
|
+
return yaml_1.AppHostingYamlConfig.loadFromFile(allConfigs.get(config_1.APPHOSTING_BASE_YAML_FILE));
|
|
145
|
+
}
|
|
146
|
+
const baseFilePath = allConfigs.get(config_1.APPHOSTING_BASE_YAML_FILE);
|
|
147
|
+
return await (0, config_1.loadConfigForEnvironment)(userGivenConfigFilePath, baseFilePath);
|
|
148
|
+
}
|
|
149
|
+
exports.loadConfigToExport = loadConfigToExport;
|
|
150
|
+
function discoverConfigs(cwd) {
|
|
151
|
+
const appHostingConfigPaths = (0, config_1.discoverConfigsAtBackendRoot)(cwd).filter((path) => !path.endsWith(config_1.APPHOSTING_LOCAL_YAML_FILE));
|
|
152
|
+
if (appHostingConfigPaths.length === 0) {
|
|
153
|
+
throw new error_1.FirebaseError("No apphosting.*.yaml configs found");
|
|
154
|
+
}
|
|
155
|
+
const fileNameToPathMap = new Map();
|
|
156
|
+
for (const path of appHostingConfigPaths) {
|
|
157
|
+
const fileName = (0, path_1.basename)(path);
|
|
158
|
+
fileNameToPathMap.set(fileName, path);
|
|
159
|
+
}
|
|
160
|
+
return fileNameToPathMap;
|
|
161
|
+
}
|
|
162
|
+
function getSecretNameParts(secret) {
|
|
163
|
+
let [name, version] = secret.split("@");
|
|
164
|
+
if (!version) {
|
|
165
|
+
version = "latest";
|
|
166
|
+
}
|
|
167
|
+
return [name, version];
|
|
168
|
+
}
|
|
169
|
+
exports.getSecretNameParts = getSecretNameParts;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promptForAppHostingYaml = exports.getEnvironmentName = void 0;
|
|
4
|
+
const error_1 = require("../error");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const prompt = require("../prompt");
|
|
7
|
+
function getEnvironmentName(apphostingYamlFileName) {
|
|
8
|
+
const found = apphostingYamlFileName.match(config_1.APPHOSTING_YAML_FILE_REGEX);
|
|
9
|
+
if (!found || found.length < 2 || !found[1]) {
|
|
10
|
+
throw new error_1.FirebaseError("Invalid apphosting environment file");
|
|
11
|
+
}
|
|
12
|
+
return found[1].replaceAll(".", "");
|
|
13
|
+
}
|
|
14
|
+
exports.getEnvironmentName = getEnvironmentName;
|
|
15
|
+
async function promptForAppHostingYaml(apphostingFileNameToPathMap, promptMessage = "Please select an App Hosting config:") {
|
|
16
|
+
const fileNames = Array.from(apphostingFileNameToPathMap.keys());
|
|
17
|
+
const baseFilePath = apphostingFileNameToPathMap.get(config_1.APPHOSTING_BASE_YAML_FILE);
|
|
18
|
+
const listOptions = fileNames.map((fileName) => {
|
|
19
|
+
if (fileName === config_1.APPHOSTING_BASE_YAML_FILE) {
|
|
20
|
+
return {
|
|
21
|
+
name: `base (${config_1.APPHOSTING_BASE_YAML_FILE})`,
|
|
22
|
+
value: baseFilePath,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const environment = getEnvironmentName(fileName);
|
|
26
|
+
return {
|
|
27
|
+
name: baseFilePath
|
|
28
|
+
? `${environment} (${config_1.APPHOSTING_BASE_YAML_FILE} + ${fileName})`
|
|
29
|
+
: `${environment} (${fileName})`,
|
|
30
|
+
value: apphostingFileNameToPathMap.get(fileName),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const fileToExportPath = await prompt.promptOnce({
|
|
34
|
+
name: "apphosting-yaml",
|
|
35
|
+
type: "list",
|
|
36
|
+
message: promptMessage,
|
|
37
|
+
choices: listOptions,
|
|
38
|
+
});
|
|
39
|
+
return fileToExportPath;
|
|
40
|
+
}
|
|
41
|
+
exports.promptForAppHostingYaml = promptForAppHostingYaml;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppHostingYamlConfig = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const yaml = require("yaml");
|
|
8
|
+
const jsYaml = require("js-yaml");
|
|
9
|
+
const fsutils_1 = require("../fsutils");
|
|
10
|
+
const error_1 = require("../error");
|
|
11
|
+
class AppHostingYamlConfig {
|
|
12
|
+
static async loadFromFile(filePath) {
|
|
13
|
+
var _a;
|
|
14
|
+
const config = new AppHostingYamlConfig();
|
|
15
|
+
if (!(0, fsutils_1.fileExistsSync)(filePath)) {
|
|
16
|
+
throw new error_1.FirebaseError("Cannot load AppHostingYamlConfig from given path, it doesn't exist");
|
|
17
|
+
}
|
|
18
|
+
const file = await (0, utils_1.readFileFromDirectory)((0, path_1.dirname)(filePath), (0, path_1.basename)(filePath));
|
|
19
|
+
const loadedAppHostingYaml = (_a = (await (0, utils_1.wrappedSafeLoad)(file.source))) !== null && _a !== void 0 ? _a : {};
|
|
20
|
+
if (loadedAppHostingYaml.env) {
|
|
21
|
+
const parsedEnvs = parseEnv(loadedAppHostingYaml.env);
|
|
22
|
+
config._environmentVariables = parsedEnvs.environmentVariables;
|
|
23
|
+
config._secrets = parsedEnvs.secrets;
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
static empty() {
|
|
28
|
+
return new AppHostingYamlConfig();
|
|
29
|
+
}
|
|
30
|
+
constructor() {
|
|
31
|
+
this._environmentVariables = new Map();
|
|
32
|
+
this._secrets = new Map();
|
|
33
|
+
}
|
|
34
|
+
get environmentVariables() {
|
|
35
|
+
return mapToArray(this._environmentVariables);
|
|
36
|
+
}
|
|
37
|
+
get secrets() {
|
|
38
|
+
return mapToArray(this._secrets);
|
|
39
|
+
}
|
|
40
|
+
addEnvironmentVariable(env) {
|
|
41
|
+
this._environmentVariables.set(env.variable, env);
|
|
42
|
+
}
|
|
43
|
+
addSecret(secret) {
|
|
44
|
+
this._secrets.set(secret.variable, secret);
|
|
45
|
+
}
|
|
46
|
+
merge(other) {
|
|
47
|
+
for (const [key, value] of other._environmentVariables) {
|
|
48
|
+
this._environmentVariables.set(key, value);
|
|
49
|
+
}
|
|
50
|
+
for (const [key, value] of other._secrets) {
|
|
51
|
+
this._secrets.set(key, value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async upsertFile(filePath) {
|
|
55
|
+
let yamlConfigToWrite = {};
|
|
56
|
+
if ((0, fsutils_1.fileExistsSync)(filePath)) {
|
|
57
|
+
const file = await (0, utils_1.readFileFromDirectory)((0, path_1.dirname)(filePath), (0, path_1.basename)(filePath));
|
|
58
|
+
yamlConfigToWrite = await (0, utils_1.wrappedSafeLoad)(file.source);
|
|
59
|
+
}
|
|
60
|
+
yamlConfigToWrite.env = [...this.environmentVariables, ...this.secrets];
|
|
61
|
+
(0, config_1.store)(filePath, yaml.parseDocument(jsYaml.dump(yamlConfigToWrite)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.AppHostingYamlConfig = AppHostingYamlConfig;
|
|
65
|
+
function parseEnv(envs) {
|
|
66
|
+
const environmentVariables = new Map();
|
|
67
|
+
const secrets = new Map();
|
|
68
|
+
for (const env of envs) {
|
|
69
|
+
if (env.value) {
|
|
70
|
+
environmentVariables.set(env.variable, env);
|
|
71
|
+
}
|
|
72
|
+
if (env.secret) {
|
|
73
|
+
secrets.set(env.variable, env);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
environmentVariables,
|
|
78
|
+
secrets,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function mapToArray(map) {
|
|
82
|
+
return Array.from(map.values());
|
|
83
|
+
}
|
package/lib/bin/firebase.js
CHANGED
|
@@ -16,7 +16,7 @@ const marked_1 = require("marked");
|
|
|
16
16
|
marked_1.marked.use((0, marked_terminal_1.markedTerminal)());
|
|
17
17
|
const node_path_1 = require("node:path");
|
|
18
18
|
const triple_beam_1 = require("triple-beam");
|
|
19
|
-
const
|
|
19
|
+
const node_util_1 = require("node:util");
|
|
20
20
|
const fs = require("node:fs");
|
|
21
21
|
const configstore_1 = require("../configstore");
|
|
22
22
|
const errorOut_1 = require("../errorOut");
|
|
@@ -58,7 +58,7 @@ logger_1.logger.add(new winston.transports.File({
|
|
|
58
58
|
filename: logFilename,
|
|
59
59
|
format: winston.format.printf((info) => {
|
|
60
60
|
const segments = [info.message, ...(info[triple_beam_1.SPLAT] || [])].map(utils.tryStringify);
|
|
61
|
-
return `[${info.level}] ${
|
|
61
|
+
return `[${info.level}] ${(0, node_util_1.stripVTControlCharacters)(segments.join(" "))}`;
|
|
62
62
|
}),
|
|
63
63
|
}));
|
|
64
64
|
logger_1.logger.debug("-".repeat(70));
|
|
@@ -4,8 +4,8 @@ exports.command = void 0;
|
|
|
4
4
|
const command_1 = require("../command");
|
|
5
5
|
const projectUtils_1 = require("../projectUtils");
|
|
6
6
|
const requireInteractive_1 = require("../requireInteractive");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const backend_1 = require("../apphosting/backend");
|
|
8
|
+
const apphosting_1 = require("../gcp/apphosting");
|
|
9
9
|
const firedata_1 = require("../gcp/firedata");
|
|
10
10
|
const requireTosAcceptance_1 = require("../requireTosAcceptance");
|
|
11
11
|
exports.command = new command_1.Command("apphosting:backends:create")
|
|
@@ -13,7 +13,7 @@ exports.command = new command_1.Command("apphosting:backends:create")
|
|
|
13
13
|
.option("-a, --app <webAppId>", "specify an existing Firebase web app's ID to associate your App Hosting backend with")
|
|
14
14
|
.option("-l, --location <location>", "specify the location of the backend", "")
|
|
15
15
|
.option("-s, --service-account <serviceAccount>", "specify the service account used to run the server", "")
|
|
16
|
-
.before(
|
|
16
|
+
.before(apphosting_1.ensureApiEnabled)
|
|
17
17
|
.before(requireInteractive_1.default)
|
|
18
18
|
.before((0, requireTosAcceptance_1.requireTosAcceptance)(firedata_1.APPHOSTING_TOS_ID))
|
|
19
19
|
.action(async (options) => {
|
|
@@ -21,5 +21,5 @@ exports.command = new command_1.Command("apphosting:backends:create")
|
|
|
21
21
|
const webAppId = options.app;
|
|
22
22
|
const location = options.location;
|
|
23
23
|
const serviceAccount = options.serviceAccount;
|
|
24
|
-
await (0,
|
|
24
|
+
await (0, backend_1.doSetup)(projectId, webAppId, location, serviceAccount);
|
|
25
25
|
});
|
|
@@ -8,7 +8,7 @@ const prompt_1 = require("../prompt");
|
|
|
8
8
|
const utils = require("../utils");
|
|
9
9
|
const apphosting = require("../gcp/apphosting");
|
|
10
10
|
const apphosting_backends_list_1 = require("./apphosting-backends-list");
|
|
11
|
-
const
|
|
11
|
+
const backend_1 = require("../apphosting/backend");
|
|
12
12
|
const ora = require("ora");
|
|
13
13
|
exports.command = new command_1.Command("apphosting:backends:delete <backend>")
|
|
14
14
|
.description("delete a Firebase App Hosting backend")
|
|
@@ -20,11 +20,11 @@ exports.command = new command_1.Command("apphosting:backends:delete <backend>")
|
|
|
20
20
|
let location = options.location;
|
|
21
21
|
let backend;
|
|
22
22
|
if (location === "-" || location === "") {
|
|
23
|
-
backend = await (0,
|
|
23
|
+
backend = await (0, backend_1.getBackendForAmbiguousLocation)(projectId, backendId, "Please select the location of the backend you'd like to delete:");
|
|
24
24
|
location = apphosting.parseBackendName(backend.name).location;
|
|
25
25
|
}
|
|
26
26
|
else {
|
|
27
|
-
backend = await getBackendForLocation(projectId, location, backendId);
|
|
27
|
+
backend = await (0, backend_1.getBackendForLocation)(projectId, location, backendId);
|
|
28
28
|
}
|
|
29
29
|
utils.logWarning("You are about to permanently delete this backend:");
|
|
30
30
|
(0, apphosting_backends_list_1.printBackendsTable)([backend]);
|
|
@@ -39,7 +39,7 @@ exports.command = new command_1.Command("apphosting:backends:delete <backend>")
|
|
|
39
39
|
}
|
|
40
40
|
const spinner = ora("Deleting backend...").start();
|
|
41
41
|
try {
|
|
42
|
-
await (0,
|
|
42
|
+
await (0, backend_1.deleteBackendAndPoll)(projectId, location, backendId);
|
|
43
43
|
spinner.succeed(`Successfully deleted the backend: ${backendId}`);
|
|
44
44
|
}
|
|
45
45
|
catch (err) {
|
|
@@ -47,13 +47,3 @@ exports.command = new command_1.Command("apphosting:backends:delete <backend>")
|
|
|
47
47
|
throw new error_1.FirebaseError(`Failed to delete backend: ${backendId}.`, { original: err });
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
|
-
async function getBackendForLocation(projectId, location, backendId) {
|
|
51
|
-
try {
|
|
52
|
-
return await apphosting.getBackend(projectId, location, backendId);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
throw new error_1.FirebaseError(`No backend named "${backendId}" found in ${location}.`, {
|
|
56
|
-
original: err,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|