firebase-tools
Version:
Command-Line Interface for Firebase
248 lines (247 loc) • 11.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.command = void 0;
exports.hasNonEmptyProjectEnv = hasNonEmptyProjectEnv;
exports.handleSecretEjection = handleSecretEjection;
const checkMinRequiredVersion_1 = require("../checkMinRequiredVersion");
const command_1 = require("../command");
const planner = require("../deploy/extensions/planner");
const etags_1 = require("../extensions/etags");
const export_1 = require("../extensions/export");
const extensionsHelper_1 = require("../extensions/extensionsHelper");
const manifest = require("../extensions/manifest");
const paramHelper_1 = require("../extensions/paramHelper");
const functional_1 = require("../functional");
const getProjectNumber_1 = require("../getProjectNumber");
const logger_1 = require("../logger");
const projectUtils_1 = require("../projectUtils");
const prompt_1 = require("../prompt");
const requirePermissions_1 = require("../requirePermissions");
const extensionsApi_1 = require("../extensions/extensionsApi");
const utils_1 = require("../utils");
const env_1 = require("../functions/env");
const fs_1 = require("fs");
const path_1 = require("path");
const config_1 = require("../config");
const projectConfig_1 = require("../functions/projectConfig");
const error_1 = require("../error");
const experiments = require("../experiments");
const migrate_1 = require("../extensions/migrate");
exports.command = new command_1.Command("ext:export")
.description("export Extension instances installed on a project to a local Firebase directory")
.option(`--mode <target>`, `experimental: controls the target system of the export (supports "extensions", "functions")`)
.option("--instance <instanceId>", "scope the export to the single instance with the specified instance id")
.option("-k, --kit-instance <kitId>", "write the .env export from --mode functions to the config path for a kit instance currently defined in firebase.json")
.option(`--outputDir <path>`, `override the .env export from --mode functions to a specific arbitrary path`)
.before(requirePermissions_1.requirePermissions, ["firebaseextensions.instances.list"])
.before(extensionsHelper_1.ensureExtensionsApiEnabled)
.before(checkMinRequiredVersion_1.checkMinRequiredVersion, "extMinVersion")
.withForce()
.action(async (options) => {
if (experiments.isEnabled("extMigrationFeatures") && options.mode === "functions") {
await fnHandler(options);
}
else {
await extHandler(options);
}
});
async function extHandler(options) {
const projectId = (0, projectUtils_1.needProjectId)(options);
const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(options);
let have = await Promise.all(await planner.have(projectId));
if (have.length === 0) {
logger_1.logger.info(`No extension instances installed on ${projectId}, so there is nothing to export.`);
return;
}
if (options.instance) {
have = have.filter((s) => s.instanceId === options.instance);
if (have.length === 0) {
logger_1.logger.info(`No extension instances installed on ${projectId} match specified instance ID ${options.instance}.`);
return;
}
}
const [withRef, withoutRef] = (0, functional_1.partition)(have, (s) => !!s.ref);
const withRefSubbed = await Promise.all(withRef.map(async (i) => {
const subbed = await (0, export_1.setSecretParamsToLatest)(i);
return (0, export_1.parameterizeProject)(projectId, projectNumber, subbed);
}));
(0, export_1.displayExportInfo)(withRefSubbed, withoutRef);
if (!options.nonInteractive &&
!options.force &&
!(await (0, prompt_1.confirm)({
message: "Do you wish to add these Extension instances to firebase.json?",
default: true,
}))) {
logger_1.logger.info("Exiting. No changes made.");
return;
}
const manifestSpecs = withRefSubbed.map((spec) => {
const paramCopy = { ...spec.params, ...spec.systemParams };
if (spec.eventarcChannel) {
paramCopy.EVENTARC_CHANNEL = spec.eventarcChannel;
}
if (spec.allowedEventTypes) {
paramCopy.ALLOWED_EVENT_TYPES = spec.allowedEventTypes.join(",");
}
return {
instanceId: spec.instanceId,
ref: spec.ref,
params: (0, paramHelper_1.buildBindingOptionsWithBaseValue)(paramCopy),
};
});
const existingConfig = manifest.loadConfig(options);
await manifest.writeToManifest(manifestSpecs, existingConfig, {
nonInteractive: options.nonInteractive,
force: options.force,
}, true);
(0, etags_1.saveEtags)(options.rc, projectId, have);
}
async function fnHandler(options) {
if (!options.instance) {
logger_1.logger.info(`ext:export must specify an --instance <instanceId> option when exporting to Functions. Use ext:list to find your instance IDs.`);
return;
}
const projectId = (0, projectUtils_1.needProjectId)(options);
let instance = await (0, extensionsApi_1.getInstance)(projectId, options.instance);
if (typeof instance === "undefined") {
logger_1.logger.info(`No extension matching instance ID ${options.instance} found`);
return;
}
if (instance.state !== "ACTIVE" && !options.force) {
throw new error_1.FirebaseError(`Extension ${options.instance} is in state ${instance.state}. To export a non-ACTIVE extension, use the --force option.`);
}
instance = await (0, extensionsHelper_1.ensureInstanceSpec)(instance);
if (!instance.config?.source?.spec) {
throw new error_1.FirebaseError(`Could not load extension specification for ${options.instance}. Unable to export configuration.`);
}
instance = await (0, migrate_1.ensureInstanceUpToDate)(projectId, instance, {
nonInteractive: options.nonInteractive,
force: options.force,
});
const instanceId = (0, utils_1.last)(instance.name.split("/")) ?? "";
if (instanceId !== options.instance) {
return;
}
await handleSecretEjection(options, instance);
const convertedEnv = (0, export_1.functionsEnvFromInstance)(instance);
const writeLocation = kitExportTarget(instanceId, projectId, options);
if (hasNonEmptyProjectEnv(writeLocation)) {
logger_1.logger.info(`Exported extensions config appears to already exist in /${instanceId}, aborting write.`);
}
else {
(0, utils_1.logLabeledBullet)("functions", `Saving exported extensions config as a Function Kits .env file`);
(0, fs_1.mkdirSync)((0, path_1.resolve)(writeLocation.projectDir, writeLocation.configDir ?? instanceId), {
recursive: true,
});
(0, env_1.writeUserEnvs)(convertedEnv, writeLocation);
}
}
function hasNonEmptyProjectEnv(opts) {
const configDir = opts.configDir || opts.functionsSource;
const files = [
`.env.${opts.projectId}`,
...(opts.projectAlias ? [`.env.${opts.projectAlias}`] : []),
];
return files.some((f) => {
const fullPath = (0, path_1.join)(configDir, f);
try {
return (0, fs_1.statSync)(fullPath).size > 0;
}
catch {
return false;
}
});
}
async function handleSecretEjection(options, instance) {
const secrets = await (0, export_1.secretsNeedingEjection)(instance);
if (secrets.length === 0) {
return;
}
(0, utils_1.logLabeledBullet)("functions", "Cloud Secret Manager resources still require migration from Extensions lifecycle management to Kits.");
(0, utils_1.logLabeledWarning)("functions", "Finishing the Extension migration process with extensions:uninstall without migrating bound secrets will result in permanent loss of secrets.");
const userAllowedEjection = await (0, prompt_1.confirm)({
message: `Automatically migrate ${secrets.length} Secrets from Extensions to Kits? (Y/n)`,
nonInteractive: options.nonInteractive,
force: options.force,
default: true,
});
if (userAllowedEjection) {
const results = await (0, export_1.ejectSecretsFromInstance)(instance);
if (results.fail.length > 0) {
let resultMsg = "";
if (results.success.length > 0) {
resultMsg = `Added functions-managed label to secrets: ${results.success}.`;
}
resultMsg += `${results.fail.length} secrets failed to update: ${results.fail}`;
(0, utils_1.logLabeledError)("functions", resultMsg);
if (!options.force) {
throw new error_1.FirebaseError("Secret migration failed.");
}
else {
(0, utils_1.logLabeledWarning)("functions", "Proceeding after secret migration failure in --force mode. Manually remove the 'firebase-extensions-managed' label from the secrets, or risk permanent data loss.");
}
}
else {
(0, utils_1.logLabeledBullet)("functions", `Added functions-managed label to secrets: ${results.success}.`);
}
}
else {
(0, utils_1.logLabeledError)("functions", "Proceeding without migrating secrets risks permanent data loss. Manually remove the 'firebase-extensions-managed' label from the secrets before running ext:uninstall.");
}
}
function kitExportTarget(instanceId, projectId, options) {
const firebaseConfig = config_1.Config.load(options, true);
const cwd = options.cwd ?? process.cwd();
if (typeof options.outputDir !== "undefined") {
return {
functionsSource: instanceId,
configDir: (0, path_1.resolve)(cwd, String(options.outputDir)),
projectId: projectId,
isEmulator: false,
projectDir: cwd,
};
}
if (typeof options.kitInstance !== "undefined") {
if (!firebaseConfig) {
throw new error_1.FirebaseError("--kit-instance option was provided but no firebase.json available to look in for kit definitions.");
}
const functionsConfig = firebaseConfig.src.functions ?? [];
const validatedFunctions = (0, projectConfig_1.normalizeAndValidate)(functionsConfig);
for (const fn of validatedFunctions) {
if (!(0, projectConfig_1.isKitConfig)(fn)) {
continue;
}
for (const [kitInstanceId, configDirPath] of Object.entries(fn.instances)) {
if (kitInstanceId === options.kitInstance) {
const projectDir = options.projectRoot ?? cwd;
return {
functionsSource: instanceId,
configDir: (0, path_1.resolve)(projectDir, String(configDirPath)),
projectId: projectId,
isEmulator: false,
projectDir: projectDir,
};
}
}
}
throw new error_1.FirebaseError(`Could not find kit instance "${options.kitInstance}" in firebase.json.`);
}
if (typeof options.projectRoot !== "undefined") {
return {
functionsSource: instanceId,
configDir: (0, path_1.resolve)(options.projectRoot, instanceId),
projectId: projectId,
isEmulator: false,
projectDir: options.projectRoot,
};
}
else {
return {
functionsSource: instanceId,
configDir: (0, path_1.resolve)(cwd, instanceId),
projectId: projectId,
isEmulator: false,
projectDir: cwd,
};
}
}