firebase-tools
Version:
Command-Line Interface for Firebase
360 lines (359 loc) • 16.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getKitPackage = getKitPackage;
exports.getInstanceId = getInstanceId;
exports.getExtensionRef = getExtensionRef;
exports.formatExtensionsTable = formatExtensionsTable;
exports.getMigratableInstances = getMigratableInstances;
exports.promptInstanceSelection = promptInstanceSelection;
exports.createMigrationPlan = createMigrationPlan;
exports.ensureInstanceUpToDate = ensureInstanceUpToDate;
exports.migrateSecrets = migrateSecrets;
exports.uninstallExtension = uninstallExtension;
const clc = require("colorette");
const Table = require("cli-table3");
const error_1 = require("../error");
const logger_1 = require("../logger");
const utils_1 = require("../utils");
const extensionsHelper_1 = require("./extensionsHelper");
const prompt_1 = require("../prompt");
const extensionsApi = require("./extensionsApi");
const refs = require("./refs");
const paramHelper = require("./paramHelper");
const updateHelper = require("./updateHelper");
const replacements = require("./replacements.json");
const export_1 = require("./export");
const manifest = require("./manifest");
function getKitPackage(extensionRef, packageOverride) {
if (packageOverride) {
return packageOverride;
}
const data = replacements;
const map = data.replacements || {};
let entry = map[extensionRef];
if (!entry && !extensionRef.includes("/")) {
entry = map[`firebase/${extensionRef}`];
}
if (entry && entry.npmPackage && entry.status === "REPLACEMENT_AVAILABLE") {
return entry.npmPackage;
}
return undefined;
}
function getInstanceId(instance) {
return (0, utils_1.last)(instance.name.split("/")) ?? instance.name;
}
function getExtensionRef(instance) {
return instance.config.extensionRef || instance.config.source?.spec?.name || "";
}
function formatExtensionsTable(instances, packageOverride) {
const table = new Table({
head: ["Extension", "publisher", "Instances", "kit package"],
style: { head: ["yellow"] },
});
const grouped = new Map();
instances.forEach((instance) => {
const ref = getExtensionRef(instance);
const instanceId = getInstanceId(instance);
let publisher = "N/A";
if (ref.includes("/")) {
publisher = ref.split("/")[0];
}
const kitPkg = getKitPackage(ref, packageOverride) ?? "N/A";
const groupEntry = grouped.get(ref);
if (!groupEntry) {
grouped.set(ref, { publisher, instances: [instanceId], kitPackage: kitPkg });
}
else {
groupEntry.instances.push(instanceId);
}
});
const rows = [];
grouped.forEach((data, ref) => {
table.push([ref, data.publisher, data.instances.join(", "), data.kitPackage]);
rows.push({
extension: ref,
publisher: data.publisher,
instances: data.instances,
kitPackage: data.kitPackage,
});
});
return { tableString: table.toString(), rows };
}
function getMigratableInstances(instances, packageOverride) {
const result = [];
for (const instance of instances) {
const ref = getExtensionRef(instance);
const instanceId = getInstanceId(instance);
const kitPkg = getKitPackage(ref, packageOverride);
if (kitPkg) {
result.push({
instance,
instanceId,
extensionRef: ref,
kitPackage: kitPkg,
});
}
}
return result;
}
async function promptInstanceSelection(migratableInstances, nonInteractive) {
const choices = migratableInstances.map((item) => ({
name: `${item.instanceId} (${item.extensionRef})`,
value: item,
}));
const selected = await (0, prompt_1.select)({
message: "Which extension instance would you like to migrate?",
choices,
nonInteractive,
});
return selected;
}
async function createMigrationPlan(projectId, options) {
const instances = await extensionsApi.listInstances(projectId);
if (options.extInstance) {
const foundInstance = instances.find((inst) => getInstanceId(inst) === options.extInstance);
if (!foundInstance) {
throw new error_1.FirebaseError(`Extension instance ${options.extInstance} was not found on project ${projectId}.`);
}
const ref = getExtensionRef(foundInstance);
const kitPkg = getKitPackage(ref, options.package);
if (!kitPkg) {
throw new error_1.FirebaseError("This extension does not have an associated function kit. You can create your own function kit by forking the extension.");
}
return {
instance: foundInstance,
instanceId: getInstanceId(foundInstance),
extensionRef: ref,
kitPackage: kitPkg,
};
}
if (options.extension) {
const targetExt = options.extension;
const matchingInstances = instances.filter((inst) => {
const ref = getExtensionRef(inst);
return (ref === targetExt ||
(ref.includes("/") && ref.split("/")[1] === targetExt) ||
(targetExt.includes("/") && ref === targetExt.split("/")[1]));
});
if (matchingInstances.length === 0) {
throw new error_1.FirebaseError(`Extension ${options.extension} is not installed on project ${projectId}.`);
}
const matchingInstancesWithKit = matchingInstances
.map((inst) => {
const ref = getExtensionRef(inst);
const kitPkg = getKitPackage(ref, options.package);
return { inst, ref, kitPkg };
})
.filter((item) => item.kitPkg !== undefined);
if (matchingInstancesWithKit.length === 0) {
throw new error_1.FirebaseError("This extension does not have an associated function kit. You can create your own function kit by forking the extension.");
}
if (matchingInstancesWithKit.length === 1) {
const { inst, ref, kitPkg } = matchingInstancesWithKit[0];
return {
instance: inst,
instanceId: getInstanceId(inst),
extensionRef: ref,
kitPackage: kitPkg,
};
}
const migratableChoices = matchingInstancesWithKit.map(({ inst, ref, kitPkg }) => ({
instance: inst,
instanceId: getInstanceId(inst),
extensionRef: ref,
kitPackage: kitPkg,
}));
return promptInstanceSelection(migratableChoices, options.nonInteractive);
}
if (instances.length === 0) {
throw new error_1.FirebaseError(`All extensions in project ${clc.bold(projectId)} have already been migrated.`);
}
const { tableString } = formatExtensionsTable(instances, options.package);
(0, utils_1.logLabeledBullet)(extensionsHelper_1.logPrefix, `list of extensions installed in ${clc.bold(projectId)}:`);
logger_1.logger.info(tableString);
const migratable = getMigratableInstances(instances, options.package);
if (migratable.length === 0) {
throw new error_1.FirebaseError("No remaining Extensions have an associated function kit. You can create your own function kit by forking the extension.");
}
return promptInstanceSelection(migratable, options.nonInteractive);
}
async function fetchOldSpec(instance, rawRef) {
if (instance.config.source?.spec) {
return instance.config.source.spec;
}
if (!rawRef) {
return undefined;
}
try {
const oldVersion = instance.config.extensionVersion;
const oldVersionRef = rawRef.includes("@")
? rawRef
: oldVersion
? `${rawRef}@${oldVersion}`
: rawRef;
logger_1.logger.debug(`[ensureInstanceUpToDate] Fetching oldSpec for ${oldVersionRef}...`);
const oldExtVersion = await extensionsApi.getExtensionVersion(oldVersionRef);
return oldExtVersion.spec;
}
catch (err) {
logger_1.logger.debug(`Could not fetch old spec for ${rawRef}:`, err);
return undefined;
}
}
async function getLatestExtensionVersionNumber(baseRef) {
try {
const extInfo = await extensionsApi.getExtension(baseRef);
return extInfo.latestApprovedVersion || extInfo.latestVersion;
}
catch (err) {
logger_1.logger.debug(`Could not fetch extension details for ${baseRef}:`, err);
return undefined;
}
}
async function ensureInstanceUpToDate(projectId, instance, options) {
const instanceId = getInstanceId(instance);
(0, utils_1.logLabeledBullet)(extensionsHelper_1.logPrefix, `Checking whether extension instance ${clc.bold(instanceId)} is up to date...`);
const rawRef = getExtensionRef(instance);
if (!rawRef) {
return instance;
}
let baseRef;
let currentVersion;
try {
const parsed = refs.parse(rawRef);
baseRef = refs.toExtensionRef(parsed);
currentVersion =
parsed.version ||
instance.config?.source?.spec?.version ||
instance.config?.extensionVersion ||
instance.extensionVersion;
}
catch (err) {
logger_1.logger.debug(`[ensureInstanceUpToDate] Could not parse extension reference '${rawRef}':`, err);
(0, utils_1.logLabeledWarning)(extensionsHelper_1.logPrefix, `Unable to parse extension reference ${clc.bold(rawRef)} to check for available updates.`);
const shouldContinue = await (0, prompt_1.confirm)({
message: `Do you want to proceed with migrating instance ${clc.bold(instanceId)} using its current configuration?`,
default: true,
nonInteractive: options?.nonInteractive,
force: options?.force,
});
if (!shouldContinue) {
throw new error_1.FirebaseError("Migration cancelled.");
}
return instance;
}
if (!currentVersion) {
return instance;
}
const latestVersion = await getLatestExtensionVersionNumber(baseRef);
if (!latestVersion || currentVersion === latestVersion) {
return instance;
}
const shouldUpgrade = await (0, prompt_1.confirm)({
message: `Extension instance ${clc.bold(instanceId)} is on version ${clc.bold(currentVersion)}, but the latest version is ${clc.bold(latestVersion)}. An upgrade is strongly recommended before migrating to avoid breaking changes. Upgrade it now?`,
default: true,
nonInteractive: options?.nonInteractive,
force: options?.force,
});
if (!shouldUpgrade) {
(0, utils_1.logLabeledWarning)(extensionsHelper_1.logPrefix, `Continuing migration with extension instance ${clc.bold(instanceId)} on outdated version ${clc.bold(currentVersion)}.`);
return instance;
}
(0, utils_1.logLabeledBullet)(extensionsHelper_1.logPrefix, `Upgrading extension instance ${clc.bold(instanceId)} from version ${clc.bold(currentVersion)} to ${clc.bold(latestVersion)} to ensure a smooth migration...`);
const targetRef = `${baseRef}@${latestVersion}`;
let finalParams = {
...instance.config.params,
...(instance.config.systemParams ?? {}),
};
const newExtensionVersion = await extensionsApi.getExtensionVersion(targetRef);
const oldSpec = await fetchOldSpec(instance, rawRef);
if (oldSpec) {
logger_1.logger.debug(`[ensureInstanceUpToDate] Comparing oldSpec (${oldSpec.version}) with newSpec (${newExtensionVersion.spec.version})...`);
const paramBindings = await paramHelper.promptForNewParams({
spec: oldSpec,
newSpec: newExtensionVersion.spec,
currentParams: finalParams ?? {},
projectId,
instanceId,
});
finalParams = paramHelper.getBaseParamBindings(paramBindings);
logger_1.logger.debug(`[ensureInstanceUpToDate] Resulting finalParams:`, JSON.stringify(finalParams, null, 2));
}
else {
logger_1.logger.debug(`[ensureInstanceUpToDate] WARNING: Could not resolve oldSpec for instance ${instanceId}`);
}
if (finalParams["LOCATION"] && !finalParams["firebaseextensions.v1beta.function/location"]) {
finalParams["firebaseextensions.v1beta.function/location"] = finalParams["LOCATION"];
}
const { params, systemParams } = paramHelper.partitionParams(finalParams);
(0, utils_1.logLabeledBullet)(extensionsHelper_1.logPrefix, `Updating instance ${clc.bold(instanceId)}...`);
try {
await updateHelper.update({
projectId,
instanceId,
extRef: targetRef,
canEmitEvents: Boolean(instance.config.allowedEventTypes?.length),
allowedEventTypes: instance.config.allowedEventTypes,
eventarcChannel: instance.config.eventarcChannel,
params,
systemParams,
});
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new error_1.FirebaseError(`Failed to automatically upgrade extension instance ${instanceId} to version ${latestVersion}: ${message}. Please upgrade your extension instance manually using 'firebase ext:update ${instanceId}' before attempting migration.`, { original: err instanceof Error ? err : undefined });
}
const updatedInstance = await extensionsApi.getInstance(projectId, instanceId);
return updatedInstance ? await (0, extensionsHelper_1.ensureInstanceSpec)(updatedInstance) : instance;
}
async function migrateSecrets(instance, options) {
const secrets = await (0, export_1.secretsNeedingEjection)(instance);
if (secrets.length === 0) {
return [];
}
const instanceId = getInstanceId(instance);
(0, utils_1.logLabeledBullet)(extensionsHelper_1.logPrefix, `Transferring secrets for instance ${clc.bold(instanceId)} to Functions management...`);
const results = await (0, export_1.ejectSecretsFromInstance)(instance);
if (results.success.length > 0) {
(0, utils_1.logLabeledSuccess)(extensionsHelper_1.logPrefix, `Successfully transferred secrets to Functions management: ${results.success.join(", ")}`);
}
if (results.fail.length > 0) {
const resultMsg = `${results.fail.length} secrets failed to update: ${results.fail.join(", ")}.`;
(0, utils_1.logLabeledError)("functions", resultMsg);
(0, utils_1.logLabeledError)("functions", "Ensure you have Secret Manager Admin (roles/secretmanager.admin) permissions to transfer secrets.");
if (!options?.force) {
throw new error_1.FirebaseError("Secret migration failed.", { exit: 1 });
}
(0, utils_1.logLabeledWarning)("functions", "Proceeding after secret migration failure in --force mode. Manually remove the 'firebase-extensions-managed' label from the secrets before running ext:uninstall, or risk permanent data loss.");
}
return results.success;
}
async function uninstallExtension(projectId, instanceId, options, skipConfirm = false) {
if (!skipConfirm &&
!(await (0, prompt_1.confirm)({
message: `About to delete Extensions instance ${projectId}/${instanceId}, its associated resources, and service account. Continue?`,
nonInteractive: options.nonInteractive,
force: options.force,
default: true,
}))) {
return;
}
let config;
try {
config = manifest.loadConfig(options);
}
catch {
(0, utils_1.logLabeledBullet)(extensionsHelper_1.logPrefix, "No firebase.json found. Proceeding to immediate extension instance teardown.");
}
if (config && manifest.instanceExists(instanceId, config)) {
manifest.removeFromManifest(instanceId, config);
}
(0, utils_1.logLabeledBullet)(extensionsHelper_1.logPrefix, `Uninstalling extension instance ${clc.bold(instanceId)}...`);
try {
await extensionsApi.deleteInstance(projectId, instanceId);
}
catch (err) {
throw new error_1.FirebaseError(`Error when attempting deletion: ${err instanceof Error ? err.message : String(err)}`, { original: err instanceof Error ? err : undefined });
}
(0, utils_1.logLabeledSuccess)(extensionsHelper_1.logPrefix, `Deleted Extensions instance ${projectId}/${instanceId}.`);
}