UNPKG

firebase-tools

Version:
360 lines (359 loc) • 16.4 kB
"use strict"; 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}.`); }