firebase-tools 13.7.2 → 13.7.4
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/api.js +10 -8
- package/lib/apphosting/app.js +92 -0
- package/lib/apphosting/config.js +76 -10
- package/lib/apphosting/constants.js +2 -2
- package/lib/apphosting/githubConnections.js +2 -2
- package/lib/apphosting/index.js +36 -27
- package/lib/apphosting/repo.js +2 -2
- package/lib/apphosting/secrets/dialogs.js +15 -14
- package/lib/apphosting/secrets/index.js +7 -1
- package/lib/commands/apphosting-backends-create.js +5 -3
- package/lib/commands/apphosting-backends-delete.js +7 -17
- package/lib/commands/apphosting-backends-get.js +8 -8
- package/lib/commands/apphosting-backends-list.js +4 -5
- package/lib/commands/apphosting-builds-create.js +32 -0
- package/lib/commands/apphosting-builds-get.js +18 -0
- package/lib/commands/apphosting-rollouts-create.js +26 -0
- package/lib/commands/apphosting-rollouts-list.js +21 -0
- package/lib/commands/apphosting-secrets-access.js +1 -1
- package/lib/commands/apphosting-secrets-grantaccess.js +7 -7
- package/lib/commands/apphosting-secrets-set.js +10 -59
- package/lib/commands/functions-secrets-set.js +1 -17
- package/lib/commands/index.js +8 -0
- package/lib/deploy/functions/runtimes/discovery/index.js +3 -3
- package/lib/deploy/functions/runtimes/{supported.js → supported/index.js} +26 -81
- package/lib/deploy/functions/runtimes/supported/types.js +74 -0
- package/lib/emulator/downloadableEmulators.js +15 -5
- package/lib/extensions/emulator/specHelper.js +3 -3
- package/lib/extensions/extensionsApi.js +2 -2
- package/lib/extensions/localHelper.js +3 -3
- package/lib/frameworks/flutter/index.js +2 -2
- package/lib/frameworks/next/index.js +3 -3
- package/lib/frameworks/next/utils.js +1 -0
- package/lib/functions/python.js +4 -3
- package/lib/gcp/apphosting.js +6 -1
- package/lib/gcp/devConnect.js +1 -1
- package/lib/init/features/hosting/github.js +4 -5
- package/lib/utils.js +17 -0
- package/package.json +3 -3
- package/templates/init/functions/typescript/_eslintrc +1 -0
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.setScopes = exports.getScopes = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = void 0;
|
|
3
|
+
exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsTOSOrigin = exports.extensionsPublisherOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.cloudbuildOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.eventarcOrigin = exports.dynamicLinksKey = exports.dynamicLinksOrigin = exports.consoleOrigin = exports.authOrigin = exports.apphostingP4SADomain = exports.apphostingOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.developerConnectP4SADomain = exports.developerConnectOrigin = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
|
|
4
|
+
exports.setScopes = exports.getScopes = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = void 0;
|
|
5
5
|
const constants_1 = require("./emulator/constants");
|
|
6
6
|
const logger_1 = require("./logger");
|
|
7
7
|
const scopes = require("./scopes");
|
|
@@ -21,10 +21,18 @@ const cloudMonitoringOrigin = () => utils.envOverride("CLOUD_MONITORING_URL", "h
|
|
|
21
21
|
exports.cloudMonitoringOrigin = cloudMonitoringOrigin;
|
|
22
22
|
const containerRegistryDomain = () => utils.envOverride("CONTAINER_REGISTRY_DOMAIN", "gcr.io");
|
|
23
23
|
exports.containerRegistryDomain = containerRegistryDomain;
|
|
24
|
+
const developerConnectOrigin = () => utils.envOverride("DEVELOPERCONNECT_URL", "https://developerconnect.googleapis.com");
|
|
25
|
+
exports.developerConnectOrigin = developerConnectOrigin;
|
|
26
|
+
const developerConnectP4SADomain = () => utils.envOverride("DEVELOPERCONNECT_P4SA_DOMAIN", "gcp-sa-devconnect.iam.gserviceaccount.com");
|
|
27
|
+
exports.developerConnectP4SADomain = developerConnectP4SADomain;
|
|
24
28
|
const artifactRegistryDomain = () => utils.envOverride("ARTIFACT_REGISTRY_DOMAIN", "https://artifactregistry.googleapis.com");
|
|
25
29
|
exports.artifactRegistryDomain = artifactRegistryDomain;
|
|
26
30
|
const appDistributionOrigin = () => utils.envOverride("FIREBASE_APP_DISTRIBUTION_URL", "https://firebaseappdistribution.googleapis.com");
|
|
27
31
|
exports.appDistributionOrigin = appDistributionOrigin;
|
|
32
|
+
const apphostingOrigin = () => utils.envOverride("FIREBASE_APPHOSTING_URL", "https://firebaseapphosting.googleapis.com");
|
|
33
|
+
exports.apphostingOrigin = apphostingOrigin;
|
|
34
|
+
const apphostingP4SADomain = () => utils.envOverride("FIREBASE_APPHOSTING_P4SA_DOMAIN", "gcp-sa-firebaseapphosting.iam.gserviceaccount.com");
|
|
35
|
+
exports.apphostingP4SADomain = apphostingP4SADomain;
|
|
28
36
|
const authOrigin = () => utils.envOverride("FIREBASE_AUTH_URL", "https://accounts.google.com");
|
|
29
37
|
exports.authOrigin = authOrigin;
|
|
30
38
|
const consoleOrigin = () => utils.envOverride("FIREBASE_CONSOLE_URL", "https://console.firebase.google.com");
|
|
@@ -60,10 +68,6 @@ const functionsDefaultRegion = () => utils.envOverride("FIREBASE_FUNCTIONS_DEFAU
|
|
|
60
68
|
exports.functionsDefaultRegion = functionsDefaultRegion;
|
|
61
69
|
const cloudbuildOrigin = () => utils.envOverride("FIREBASE_CLOUDBUILD_URL", "https://cloudbuild.googleapis.com");
|
|
62
70
|
exports.cloudbuildOrigin = cloudbuildOrigin;
|
|
63
|
-
const developerConnectOrigin = () => utils.envOverride("FIREBASE_DEVELOPERCONNECT_URL", "https://developerconnect.googleapis.com");
|
|
64
|
-
exports.developerConnectOrigin = developerConnectOrigin;
|
|
65
|
-
const developerConnectP4SAOrigin = () => utils.envOverride("FIREBASE_DEVELOPERCONNECT_P4SA_URL", "gcp-sa-devconnect.iam.gserviceaccount.com");
|
|
66
|
-
exports.developerConnectP4SAOrigin = developerConnectP4SAOrigin;
|
|
67
71
|
const cloudschedulerOrigin = () => utils.envOverride("FIREBASE_CLOUDSCHEDULER_URL", "https://cloudscheduler.googleapis.com");
|
|
68
72
|
exports.cloudschedulerOrigin = cloudschedulerOrigin;
|
|
69
73
|
const cloudTasksOrigin = () => utils.envOverride("FIREBASE_CLOUD_TAKS_URL", "https://cloudtasks.googleapis.com");
|
|
@@ -108,8 +112,6 @@ const cloudRunApiOrigin = () => utils.envOverride("CLOUD_RUN_API_URL", "https://
|
|
|
108
112
|
exports.cloudRunApiOrigin = cloudRunApiOrigin;
|
|
109
113
|
const serviceUsageOrigin = () => utils.envOverride("FIREBASE_SERVICE_USAGE_URL", "https://serviceusage.googleapis.com");
|
|
110
114
|
exports.serviceUsageOrigin = serviceUsageOrigin;
|
|
111
|
-
const apphostingOrigin = () => utils.envOverride("APPHOSTING_URL", "https://firebaseapphosting.googleapis.com");
|
|
112
|
-
exports.apphostingOrigin = apphostingOrigin;
|
|
113
115
|
const githubOrigin = () => utils.envOverride("GITHUB_URL", "https://github.com");
|
|
114
116
|
exports.githubOrigin = githubOrigin;
|
|
115
117
|
const githubApiOrigin = () => utils.envOverride("GITHUB_API_URL", "https://api.github.com");
|
|
@@ -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;
|
package/lib/apphosting/config.js
CHANGED
|
@@ -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
|
|
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("
|
|
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(
|
|
11
|
-
if (fs.fileExistsSync(
|
|
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 =
|
|
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
|
|
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.
|
|
27
|
+
return yaml.parseDocument(raw);
|
|
26
28
|
}
|
|
27
29
|
exports.load = load;
|
|
28
|
-
function store(yamlPath,
|
|
29
|
-
(0, fs_1.writeFileSync)(yamlPath,
|
|
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ALLOWED_DEPLOY_METHODS = exports.DEFAULT_DEPLOY_METHOD = exports.
|
|
4
|
-
exports.
|
|
3
|
+
exports.ALLOWED_DEPLOY_METHODS = exports.DEFAULT_DEPLOY_METHOD = exports.DEFAULT_LOCATION = void 0;
|
|
4
|
+
exports.DEFAULT_LOCATION = "us-central1";
|
|
5
5
|
exports.DEFAULT_DEPLOY_METHOD = "github";
|
|
6
6
|
exports.ALLOWED_DEPLOY_METHODS = [{ name: "Deploy using github", value: "github" }];
|
|
@@ -89,7 +89,7 @@ async function createFullyInstalledConnection(projectId, location, connectionId,
|
|
|
89
89
|
});
|
|
90
90
|
while (conn.installationState.stage !== "COMPLETE") {
|
|
91
91
|
utils.logBullet("Install the Firebase GitHub app to enable access to GitHub repositories");
|
|
92
|
-
const targetUri = conn.installationState.actionUri
|
|
92
|
+
const targetUri = conn.installationState.actionUri;
|
|
93
93
|
utils.logBullet(targetUri);
|
|
94
94
|
await utils.openInBrowser(targetUri);
|
|
95
95
|
await (0, prompt_1.promptOnce)({
|
|
@@ -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
|
|
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(),
|
package/lib/apphosting/index.js
CHANGED
|
@@ -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.promptLocation = 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,24 +37,23 @@ 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
|
-
location ||
|
|
43
|
-
|
|
44
|
-
name: "region",
|
|
45
|
-
type: "list",
|
|
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`,
|
|
49
|
-
choices: allowedLocations.map((loc) => ({ value: loc })),
|
|
50
|
-
}));
|
|
51
|
-
(0, utils_1.logSuccess)(`Region set to ${location}.\n`);
|
|
42
|
+
location || (await promptLocation(projectId, "Select a location to host your backend:\n"));
|
|
43
|
+
(0, utils_1.logSuccess)(`Location set to ${location}.\n`);
|
|
52
44
|
const backendId = await promptNewBackendId(projectId, location, {
|
|
53
45
|
name: "backendId",
|
|
54
46
|
type: "input",
|
|
55
47
|
default: "my-web-app",
|
|
56
48
|
message: "Create a name for your backend [1-30 characters]",
|
|
57
49
|
});
|
|
50
|
+
const webApp = await apps.getOrCreateWebApp(projectId, webAppName, backendId);
|
|
51
|
+
if (webApp) {
|
|
52
|
+
(0, utils_1.logSuccess)(`Firebase web app set to ${webApp.name}.\n`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
(0, utils_1.logWarning)(`Firebase web app not set`);
|
|
56
|
+
}
|
|
58
57
|
const gitRepositoryConnection = withDevConnect
|
|
59
58
|
? await githubConnections.linkGitHubRepository(projectId, location)
|
|
60
59
|
: await repo.linkGitHubRepository(projectId, location);
|
|
@@ -62,9 +61,9 @@ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
|
|
|
62
61
|
name: "rootDir",
|
|
63
62
|
type: "input",
|
|
64
63
|
default: "/",
|
|
65
|
-
message: "Specify
|
|
64
|
+
message: "Specify your app's root directory relative to your repository",
|
|
66
65
|
});
|
|
67
|
-
const backend = await createBackend(projectId, location, backendId, gitRepositoryConnection, serviceAccount, rootDir);
|
|
66
|
+
const backend = await createBackend(projectId, location, backendId, gitRepositoryConnection, serviceAccount, webApp === null || webApp === void 0 ? void 0 : webApp.id, rootDir);
|
|
68
67
|
const branch = await (0, prompt_1.promptOnce)({
|
|
69
68
|
name: "branch",
|
|
70
69
|
type: "input",
|
|
@@ -80,7 +79,7 @@ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
|
|
|
80
79
|
});
|
|
81
80
|
if (!confirmRollout) {
|
|
82
81
|
(0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
|
|
83
|
-
(0, utils_1.logSuccess)(`Your
|
|
82
|
+
(0, utils_1.logSuccess)(`Your backend will be deployed at:\n\thttps://${backend.uri}`);
|
|
84
83
|
return;
|
|
85
84
|
}
|
|
86
85
|
await orchestrateRollout(projectId, location, backendId, {
|
|
@@ -91,7 +90,7 @@ async function doSetup(projectId, location, serviceAccount, withDevConnect) {
|
|
|
91
90
|
},
|
|
92
91
|
});
|
|
93
92
|
(0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
|
|
94
|
-
(0, utils_1.logSuccess)(`Your
|
|
93
|
+
(0, utils_1.logSuccess)(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
|
|
95
94
|
}
|
|
96
95
|
exports.doSetup = doSetup;
|
|
97
96
|
async function promptNewBackendId(projectId, location, prompt) {
|
|
@@ -112,7 +111,7 @@ async function promptNewBackendId(projectId, location, prompt) {
|
|
|
112
111
|
function defaultComputeServiceAccountEmail(projectId) {
|
|
113
112
|
return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
|
|
114
113
|
}
|
|
115
|
-
async function createBackend(projectId, location, backendId, repository, serviceAccount, rootDir = "/") {
|
|
114
|
+
async function createBackend(projectId, location, backendId, repository, serviceAccount, webAppId, rootDir = "/") {
|
|
116
115
|
const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
|
|
117
116
|
const backendReqBody = {
|
|
118
117
|
servingLocality: "GLOBAL_ACCESS",
|
|
@@ -122,8 +121,8 @@ async function createBackend(projectId, location, backendId, repository, service
|
|
|
122
121
|
},
|
|
123
122
|
labels: deploymentTool.labels(),
|
|
124
123
|
serviceAccount: serviceAccount || defaultServiceAccount,
|
|
124
|
+
appId: webAppId,
|
|
125
125
|
};
|
|
126
|
-
delete backendReqBody.serviceAccount;
|
|
127
126
|
async function createBackendAndPoll() {
|
|
128
127
|
const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
|
|
129
128
|
return await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
|
|
@@ -154,13 +153,7 @@ async function provisionDefaultComputeServiceAccount(projectId) {
|
|
|
154
153
|
throw err;
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
|
-
await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), [
|
|
158
|
-
"roles/firebaseapphosting.viewer",
|
|
159
|
-
"roles/artifactregistry.createOnPushWriter",
|
|
160
|
-
"roles/logging.logWriter",
|
|
161
|
-
"roles/storage.objectAdmin",
|
|
162
|
-
"roles/firebase.sdkAdminServiceAgent",
|
|
163
|
-
], true);
|
|
156
|
+
await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), ["roles/firebaseapphosting.computeRunner", "roles/firebase.sdkAdminServiceAgent"], true);
|
|
164
157
|
}
|
|
165
158
|
async function setDefaultTrafficPolicy(projectId, location, backendId, codebaseBranch) {
|
|
166
159
|
const traffic = {
|
|
@@ -221,3 +214,19 @@ async function orchestrateRollout(projectId, location, backendId, buildInput) {
|
|
|
221
214
|
return { rollout, build };
|
|
222
215
|
}
|
|
223
216
|
exports.orchestrateRollout = orchestrateRollout;
|
|
217
|
+
async function deleteBackendAndPoll(projectId, location, backendId) {
|
|
218
|
+
const op = await apphosting.deleteBackend(projectId, location, backendId);
|
|
219
|
+
await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `delete-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
|
|
220
|
+
}
|
|
221
|
+
exports.deleteBackendAndPoll = deleteBackendAndPoll;
|
|
222
|
+
async function promptLocation(projectId, prompt = "Please select a location:") {
|
|
223
|
+
const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
|
|
224
|
+
return (await (0, prompt_1.promptOnce)({
|
|
225
|
+
name: "location",
|
|
226
|
+
type: "list",
|
|
227
|
+
default: constants_1.DEFAULT_LOCATION,
|
|
228
|
+
message: prompt,
|
|
229
|
+
choices: allowedLocations,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
exports.promptLocation = promptLocation;
|
package/lib/apphosting/repo.js
CHANGED
|
@@ -86,7 +86,7 @@ async function createFullyInstalledConnection(projectId, location, connectionId,
|
|
|
86
86
|
});
|
|
87
87
|
while (conn.installationState.stage !== "COMPLETE") {
|
|
88
88
|
utils.logBullet("Install the Cloud Build GitHub app to enable access to GitHub repositories");
|
|
89
|
-
const targetUri = conn.installationState.actionUri
|
|
89
|
+
const targetUri = conn.installationState.actionUri;
|
|
90
90
|
utils.logBullet(targetUri);
|
|
91
91
|
await utils.openInBrowser(targetUri);
|
|
92
92
|
await (0, prompt_1.promptOnce)({
|
|
@@ -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
|
|
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
|
|
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:
|
|
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.
|
|
76
|
-
`${clc.bold("firebase apphosting:secrets:
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
97
|
-
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
112
|
+
utils.logBullet(exports.GRANT_ACCESS_IN_FUTURE);
|
|
112
113
|
return { buildServiceAccounts: [], runServiceAccounts: [] };
|
|
113
114
|
}
|
|
114
|
-
utils.
|
|
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.
|
|
135
|
+
utils.logBullet(exports.GRANT_ACCESS_IN_FUTURE);
|
|
135
136
|
}
|
|
136
137
|
return selectFromMetadata(metadata, chosen);
|
|
137
138
|
}
|
|
@@ -5,6 +5,7 @@ const error_1 = require("../../error");
|
|
|
5
5
|
const gcsm = require("../../gcp/secretManager");
|
|
6
6
|
const gcb = require("../../gcp/cloudbuild");
|
|
7
7
|
const gce = require("../../gcp/computeEngine");
|
|
8
|
+
const apphosting = require("../../gcp/apphosting");
|
|
8
9
|
const secretManager_1 = require("../../gcp/secretManager");
|
|
9
10
|
const secretManager_2 = require("../../gcp/secretManager");
|
|
10
11
|
const utils = require("../../utils");
|
|
@@ -33,7 +34,8 @@ function serviceAccountsForBackend(projectNumber, backend) {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
exports.serviceAccountsForBackend = serviceAccountsForBackend;
|
|
36
|
-
async function grantSecretAccess(projectId, secretName, accounts) {
|
|
37
|
+
async function grantSecretAccess(projectId, projectNumber, secretName, accounts) {
|
|
38
|
+
const p4saEmail = apphosting.serviceAgentEmail(projectNumber);
|
|
37
39
|
const newBindings = [
|
|
38
40
|
{
|
|
39
41
|
role: "roles/secretmanager.secretAccessor",
|
|
@@ -43,6 +45,10 @@ async function grantSecretAccess(projectId, secretName, accounts) {
|
|
|
43
45
|
role: "roles/secretmanager.viewer",
|
|
44
46
|
members: accounts.buildServiceAccounts.map((sa) => `serviceAccount:${sa}`),
|
|
45
47
|
},
|
|
48
|
+
{
|
|
49
|
+
role: "roles/secretmanager.secretVersionManager",
|
|
50
|
+
members: [`serviceAccount:${p4saEmail}`],
|
|
51
|
+
},
|
|
46
52
|
];
|
|
47
53
|
let existingBindings;
|
|
48
54
|
try {
|
|
@@ -7,16 +7,18 @@ 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
|
|
11
|
-
.option("-
|
|
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")
|
|
12
|
+
.option("-l, --location <location>", "specify the location 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)
|
|
14
15
|
.before(apphosting_2.ensureApiEnabled)
|
|
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
|
});
|
|
@@ -5,31 +5,21 @@ const command_1 = require("../command");
|
|
|
5
5
|
const projectUtils_1 = require("../projectUtils");
|
|
6
6
|
const error_1 = require("../error");
|
|
7
7
|
const prompt_1 = require("../prompt");
|
|
8
|
-
const constants_1 = require("../apphosting/constants");
|
|
9
8
|
const utils = require("../utils");
|
|
10
9
|
const apphosting = require("../gcp/apphosting");
|
|
11
10
|
const apphosting_backends_list_1 = require("./apphosting-backends-list");
|
|
11
|
+
const apphosting_1 = require("../apphosting");
|
|
12
12
|
exports.command = new command_1.Command("apphosting:backends:delete <backend>")
|
|
13
|
-
.description("delete a
|
|
14
|
-
.option("-l, --location <location>", "
|
|
13
|
+
.description("delete a Firebase App Hosting backend")
|
|
14
|
+
.option("-l, --location <location>", "specify the location of the backend", "")
|
|
15
15
|
.withForce()
|
|
16
16
|
.before(apphosting.ensureApiEnabled)
|
|
17
17
|
.action(async (backendId, options) => {
|
|
18
18
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
19
19
|
let location = options.location;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!location) {
|
|
24
|
-
const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
|
|
25
|
-
location = await (0, prompt_1.promptOnce)({
|
|
26
|
-
name: "region",
|
|
27
|
-
type: "list",
|
|
28
|
-
default: constants_1.DEFAULT_REGION,
|
|
29
|
-
message: "Please select the region of the backend you'd like to delete:",
|
|
30
|
-
choices: allowedLocations,
|
|
31
|
-
});
|
|
32
|
-
}
|
|
20
|
+
location =
|
|
21
|
+
location ||
|
|
22
|
+
(await (0, apphosting_1.promptLocation)(projectId, "Please select the location of the backend you'd like to delete:"));
|
|
33
23
|
let backend;
|
|
34
24
|
try {
|
|
35
25
|
backend = await apphosting.getBackend(projectId, location, backendId);
|
|
@@ -52,7 +42,7 @@ exports.command = new command_1.Command("apphosting:backends:delete <backend>")
|
|
|
52
42
|
throw new error_1.FirebaseError("Deletion Aborted");
|
|
53
43
|
}
|
|
54
44
|
try {
|
|
55
|
-
await
|
|
45
|
+
await (0, apphosting_1.deleteBackendAndPoll)(projectId, location, backendId);
|
|
56
46
|
utils.logSuccess(`Successfully deleted the backend: ${backendId}`);
|
|
57
47
|
}
|
|
58
48
|
catch (err) {
|