firebase-tools
Version:
Command-Line Interface for Firebase
244 lines (243 loc) • 12.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
exports.injectEnvVarsFromApphostingConfig = injectEnvVarsFromApphostingConfig;
exports.injectAutoInitEnvVars = injectAutoInitEnvVars;
exports.getBackendConfigs = getBackendConfigs;
const path = require("path");
const backend_1 = require("../../apphosting/backend");
const apphosting_1 = require("../../gcp/apphosting");
const yaml_1 = require("../../apphosting/yaml");
const config_1 = require("../../apphosting/config");
const devConnect_1 = require("../../gcp/devConnect");
const resourceManager_1 = require("../../gcp/resourceManager");
const projectUtils_1 = require("../../projectUtils");
const getProjectNumber_1 = require("../../getProjectNumber");
const prompt_1 = require("../../prompt");
const utils_1 = require("../../utils");
const localbuilds_1 = require("../../apphosting/localbuilds");
const error_1 = require("../../error");
const managementApps = require("../../management/apps");
const utils_2 = require("../../apphosting/utils");
const experiments = require("../../experiments");
const logger_1 = require("../../logger");
async function default_1(context, options) {
const projectId = (0, projectUtils_1.needProjectId)(options);
await (0, apphosting_1.ensureApiEnabled)(options);
await (0, backend_1.ensureRequiredApisEnabled)(projectId);
await (0, backend_1.ensureAppHostingComputeServiceAccount)(projectId, "");
context.backendConfigs = {};
context.backendLocations = {};
context.backendStorageUris = {};
context.backendLocalBuilds = {};
const configs = getBackendConfigs(options);
if (configs.some((cfg) => cfg.localBuild) && experiments.isEnabled("apphostinglocalbuilds")) {
const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(options);
await ensureAppHostingServiceAgentRoles(projectId, projectNumber);
}
const { backends } = await (0, apphosting_1.listBackends)(projectId, "-");
const foundBackends = [];
const notFoundBackends = [];
const ambiguousBackends = [];
const skippedBackends = [];
for (const cfg of configs) {
const filteredBackends = backends.filter((backend) => (0, apphosting_1.parseBackendName)(backend.name).id === cfg.backendId);
if (filteredBackends.length === 0) {
notFoundBackends.push(cfg);
}
else if (filteredBackends.length === 1) {
foundBackends.push(cfg);
}
else {
ambiguousBackends.push(cfg);
}
}
for (const cfg of ambiguousBackends) {
const filteredBackends = backends.filter((backend) => (0, apphosting_1.parseBackendName)(backend.name).id === cfg.backendId);
const locations = filteredBackends.map((b) => (0, apphosting_1.parseBackendName)(b.name).location);
(0, utils_1.logLabeledWarning)("apphosting", `You have multiple backends with the same ${cfg.backendId} ID in regions: ${locations.join(", ")}. This is not allowed until we can support more locations. ` +
"Please delete and recreate any backends that share an ID with another backend.");
}
if (foundBackends.length > 0) {
(0, utils_1.logLabeledBullet)("apphosting", `Found backend(s) ${foundBackends.map((cfg) => cfg.backendId).join(", ")}`);
}
for (const cfg of foundBackends) {
const filteredBackends = backends.filter((backend) => (0, apphosting_1.parseBackendName)(backend.name).id === cfg.backendId);
if (cfg.alwaysDeployFromSource === false) {
skippedBackends.push(cfg);
continue;
}
const backend = filteredBackends[0];
const { location } = (0, apphosting_1.parseBackendName)(backend.name);
if (cfg.alwaysDeployFromSource === undefined && backend.codebase?.repository) {
const { connectionName, id } = (0, devConnect_1.parseGitRepositoryLinkName)(backend.codebase.repository);
const gitRepositoryLink = await (0, devConnect_1.getGitRepositoryLink)(projectId, location, connectionName, id);
if (!options.force) {
const confirmDeploy = await (0, prompt_1.confirm)({
default: true,
message: `${cfg.backendId} is linked to the remote repository at ${gitRepositoryLink.cloneUri}. Are you sure you want to deploy your local source?`,
});
cfg.alwaysDeployFromSource = confirmDeploy;
const configPath = path.join(options.projectRoot || "", "firebase.json");
options.config.writeProjectFile(configPath, options.config.src);
(0, utils_1.logLabeledBullet)("apphosting", `On future invocations of "firebase deploy", your local source will ${!confirmDeploy ? "not " : ""}be deployed to ${cfg.backendId}. You can edit this setting in your firebase.json at any time.`);
if (!confirmDeploy) {
skippedBackends.push(cfg);
continue;
}
}
}
context.backendConfigs[cfg.backendId] = cfg;
context.backendLocations[cfg.backendId] = location;
}
if (notFoundBackends.length > 0) {
if (options.force) {
(0, utils_1.logLabeledWarning)("apphosting", `Skipping deployments of backend(s) ${notFoundBackends.map((cfg) => cfg.backendId).join(", ")}; ` +
"the backend(s) do not exist yet and we cannot create them for you because you must choose primary regions for each one. " +
"Please run 'firebase deploy' without the --force flag, or 'firebase apphosting:backends:create' to create the backend, " +
"then retry deployment.");
return;
}
const confirmCreate = await (0, prompt_1.confirm)({
default: true,
message: `Did not find backend(s) ${notFoundBackends.map((cfg) => cfg.backendId).join(", ")}. Do you want to create them (you'll have the option to select which to create in the next step)?`,
});
if (confirmCreate) {
const selected = await (0, prompt_1.checkbox)({
message: "Which backends do you want to create and deploy to?",
choices: notFoundBackends.map((cfg) => cfg.backendId),
});
const selectedBackends = selected.map((id) => notFoundBackends.find((backend) => backend.backendId === id));
for (const cfg of selectedBackends) {
(0, utils_1.logLabeledBullet)("apphosting", `Creating a new backend ${cfg.backendId}...`);
const { location } = await (0, backend_1.doSetupSourceDeploy)(projectId, cfg.backendId);
context.backendConfigs[cfg.backendId] = cfg;
context.backendLocations[cfg.backendId] = location;
}
}
else {
skippedBackends.push(...notFoundBackends);
}
}
if (skippedBackends.length > 0) {
(0, utils_1.logLabeledWarning)("apphosting", `Skipping deployment of backend(s) ${skippedBackends.map((cfg) => cfg.backendId).join(", ")}.`);
}
const buildEnv = {};
const runtimeEnv = {};
for (const cfg of Object.values(context.backendConfigs)) {
if (!cfg.localBuild) {
continue;
}
experiments.assertEnabled("apphostinglocalbuilds", "locally build App Hosting backends");
(0, utils_1.logLabeledBullet)("apphosting", `Starting local build for backend ${cfg.backendId}`);
await injectEnvVarsFromApphostingConfig(configs.filter((c) => c.backendId === cfg.backendId), options, buildEnv, runtimeEnv);
await injectAutoInitEnvVars(cfg, backends, buildEnv, runtimeEnv);
try {
const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(options.projectRoot || "./", "nextjs", buildEnv[cfg.backendId] || {});
if (outputFiles.length !== 1) {
throw new error_1.FirebaseError(`Local build for backend ${cfg.backendId} failed: No output files found.`);
}
context.backendLocalBuilds[cfg.backendId] = {
buildDir: outputFiles[0],
buildConfig: {
...buildConfig,
env: mergeEnvVars(buildConfig.env || [], runtimeEnv[cfg.backendId] || {}),
},
annotations,
};
}
catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
throw new error_1.FirebaseError(`Local Build for backend ${cfg.backendId} failed: ${errorMsg}`);
}
}
}
async function injectEnvVarsFromApphostingConfig(configs, options, buildEnv, runtimeEnv) {
for (const cfg of configs) {
const rootDir = options.projectRoot || process.cwd();
const appDir = path.join(rootDir, cfg.rootDir || "");
let yamlConfig = yaml_1.AppHostingYamlConfig.empty();
try {
yamlConfig = await (0, config_1.getAppHostingConfiguration)(appDir);
}
catch (e) {
(0, utils_1.logLabeledWarning)("apphosting", `Failed to read apphosting.yaml, may be missing environment variables and other configs`);
}
const { build, runtime } = (0, config_1.splitEnvVars)(yamlConfig.env);
buildEnv[cfg.backendId] = { ...buildEnv[cfg.backendId], ...build };
runtimeEnv[cfg.backendId] = { ...runtimeEnv[cfg.backendId], ...runtime };
}
}
async function injectAutoInitEnvVars(cfg, backends, buildEnv, runtimeEnv) {
var _a, _b;
const backend = backends.find((b) => (0, apphosting_1.parseBackendName)(b.name).id === cfg.backendId);
if (backend?.appId) {
try {
const webappConfig = (await managementApps.getAppConfig(backend.appId, managementApps.AppPlatform.WEB));
const autoinitVars = (0, utils_2.getAutoinitEnvVars)(webappConfig);
for (const [envVarName, envVarValue] of Object.entries(autoinitVars)) {
(_a = buildEnv[cfg.backendId])[envVarName] ?? (_a[envVarName] = { value: envVarValue });
(_b = runtimeEnv[cfg.backendId])[envVarName] ?? (_b[envVarName] = { value: envVarValue });
}
}
catch (e) {
(0, utils_1.logLabeledWarning)("apphosting", `Unable to lookup details for backend ${cfg.backendId}. Firebase SDK autoinit will not be available.`);
}
}
}
function getBackendConfigs(options) {
if (!options.config.src.apphosting) {
return [];
}
const backendConfigs = Array.isArray(options.config.src.apphosting)
? options.config.src.apphosting
: [options.config.src.apphosting];
if (!options.only) {
return backendConfigs;
}
const selectors = options.only.split(",");
const backendIds = [];
for (const selector of selectors) {
if (selector === "apphosting") {
return backendConfigs;
}
if (selector.startsWith("apphosting:")) {
const backendId = selector.replace("apphosting:", "");
if (backendId.length > 0) {
backendIds.push(backendId);
}
}
}
if (backendIds.length === 0) {
return [];
}
const filteredConfigs = backendConfigs.filter((cfg) => backendIds.includes(cfg.backendId));
const foundIds = filteredConfigs.map((cfg) => cfg.backendId);
const missingIds = backendIds.filter((id) => !foundIds.includes(id));
if (missingIds.length > 0) {
throw new error_1.FirebaseError(`App Hosting backend IDs ${missingIds.join(",")} not detected in firebase.json`);
}
return filteredConfigs;
}
function mergeEnvVars(base, overrides) {
const merged = new Map();
for (const env of base) {
if (env.variable) {
merged.set(env.variable, env);
}
}
for (const [envVarName, envVarConfig] of Object.entries(overrides)) {
merged.set(envVarName, { ...envVarConfig, variable: envVarName });
}
return Array.from(merged.values());
}
async function ensureAppHostingServiceAgentRoles(projectId, projectNumber) {
const p4saEmail = (0, apphosting_1.serviceAgentEmail)(projectNumber);
try {
await (0, resourceManager_1.addServiceAccountToRoles)(projectId, p4saEmail, ["roles/storage.objectViewer"], true);
}
catch (err) {
logger_1.logger.debug(`Failed to grant storage.objectViewer to ${p4saEmail}: ${String(err)}`);
(0, utils_1.logLabeledWarning)("apphosting", `Unable to verify App Hosting service agent permissions for ${p4saEmail}. If you encounter a PERMISSION_DENIED error during rollout, please ensure the service agent has the "Storage Object Viewer" role.`);
}
}