From 8d993df404bf91903d63aaaaff4d150534d2f6d3 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Thu, 15 Jun 2023 03:02:36 -0700 Subject: [PATCH 01/24] Added cascading of latest approved version to latest version when installing. --- src/commands/ext-install.ts | 51 +++++++++++--------- src/deploy/extensions/planner.ts | 35 ++++++++------ src/extensions/extensionsHelper.ts | 23 --------- src/test/extensions/extensionsHelper.spec.ts | 34 ------------- 4 files changed, 47 insertions(+), 96 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index d4fe999a397..51a30099a53 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -9,7 +9,7 @@ import { Command } from "../command"; import { FirebaseError } from "../error"; import { getProjectId, needProjectId } from "../projectUtils"; import * as extensionsApi from "../extensions/extensionsApi"; -import { ExtensionVersion, ExtensionSource } from "../extensions/types"; +import { ExtensionVersion, ExtensionSource, Extension } from "../extensions/types"; import * as refs from "../extensions/refs"; import * as secretsUtils from "../extensions/secretsUtils"; import * as paramHelper from "../extensions/paramHelper"; @@ -22,8 +22,8 @@ import { diagnoseAndFixProject, isUrlPath, isLocalPath, - canonicalizeRefInput, } from "../extensions/extensionsHelper"; +import { resolveVersion } from "../deploy/extensions/planner"; import { getRandomString } from "../extensions/utils"; import { requirePermissions } from "../requirePermissions"; import * as utils from "../utils"; @@ -40,7 +40,7 @@ marked.setOptions({ /** * Command for installing an extension */ -export const command = new Command("ext:install [extensionName]") +export const command = new Command("ext:install [extensionRef]") .description( "add an uploaded extension to firebase.json if [publisherId/extensionId] is provided;" + "or, add a local extension if [localPath] is provided" @@ -51,21 +51,21 @@ export const command = new Command("ext:install [extensionName]") .before(ensureExtensionsApiEnabled) .before(checkMinRequiredVersion, "extMinVersion") .before(diagnoseAndFixProject) - .action(async (extensionName: string, options: Options) => { + .action(async (extensionRef: string, options: Options) => { const projectId = getProjectId(options); // TODO(b/230598656): Clean up paramsEnvPath after v11 launch. const paramsEnvPath = ""; let learnMore = false; - if (!extensionName) { + if (!extensionRef) { if (options.interactive) { learnMore = true; - extensionName = await promptForOfficialExtension( + extensionRef = await promptForOfficialExtension( "Which official extension do you wish to install?\n" + " Select an extension, then press Enter to learn more." ); } else { throw new FirebaseError( - `Unable to find published extension '${clc.bold(extensionName)}'. ` + + `Unable to find published extension '${clc.bold(extensionRef)}'. ` + `Run ${clc.bold( "firebase ext:install -i" )} to select from the list of all available published extensions.` @@ -77,7 +77,7 @@ export const command = new Command("ext:install [extensionName]") // TODO(b/220900194): Remove when deprecating old install flow. // --local doesn't support urlPath so this will become dead codepath. - if (isUrlPath(extensionName)) { + if (isUrlPath(extensionRef)) { throw new FirebaseError( `Installing with a source url is no longer supported in the CLI. Please use Firebase Console instead.` ); @@ -91,19 +91,21 @@ export const command = new Command("ext:install [extensionName]") // If the user types in a local path (prefixed with ~/, ../, or ./), install from local source. // Otherwise, treat the input as an extension reference and proceed with reference-based installation. - if (isLocalPath(extensionName)) { + if (isLocalPath(extensionRef)) { // TODO(b/228444119): Create source should happen at deploy time. // Should parse spec locally so we don't need project ID. - source = await createSourceFromLocation(needProjectId({ projectId }), extensionName); - await displayExtInfo(extensionName, "", source.spec); + source = await createSourceFromLocation(needProjectId({ projectId }), extensionRef); + await displayExtInfo(extensionRef, "", source.spec); void track("Extension Install", "Install by Source", options.interactive ? 1 : 0); } else { void track("Extension Install", "Install by Extension Ref", options.interactive ? 1 : 0); - extensionName = await canonicalizeRefInput(extensionName); - extensionVersion = await extensionsApi.getExtensionVersion(extensionName); + const extension = await extensionsApi.getExtension(extensionRef); + const extensionVersionRef = await resolveVersion(refs.parse(extensionRef), extension); + extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef); await infoExtensionVersion({ - extensionName, + extensionRef, extensionVersion, + published: !!extension.latestApprovedVersion, }); } if ( @@ -124,7 +126,7 @@ export const command = new Command("ext:install [extensionName]") if (!spec) { throw new FirebaseError( `Could not find the extension.yaml for extension '${clc.bold( - extensionName + extensionRef )}'. Please make sure this is a valid extension and try again.` ); } @@ -141,7 +143,7 @@ export const command = new Command("ext:install [extensionName]") return installToManifest({ paramsEnvPath, projectId, - extensionName, + extensionRef, source, extVersion: extensionVersion, nonInteractive: options.nonInteractive, @@ -158,17 +160,18 @@ export const command = new Command("ext:install [extensionName]") }); async function infoExtensionVersion(args: { - extensionName: string; + extensionRef: string; extensionVersion: ExtensionVersion; + published: boolean; }): Promise { - const ref = refs.parse(args.extensionName); - await displayExtInfo(args.extensionName, ref.publisherId, args.extensionVersion.spec, true); + const ref = refs.parse(args.extensionRef); + await displayExtInfo(args.extensionRef, ref.publisherId, args.extensionVersion.spec, args.published); } interface InstallExtensionOptions { paramsEnvPath?: string; projectId?: string; - extensionName: string; + extensionRef: string; source?: ExtensionSource; extVersion?: ExtensionVersion; nonInteractive: boolean; @@ -182,14 +185,14 @@ interface InstallExtensionOptions { * @param options */ async function installToManifest(options: InstallExtensionOptions): Promise { - const { projectId, extensionName, extVersion, source, paramsEnvPath, nonInteractive, force } = + const { projectId, extensionRef, extVersion, source, paramsEnvPath, nonInteractive, force } = options; - const isLocalSource = isLocalPath(extensionName); + const isLocalSource = isLocalPath(extensionRef); const spec = extVersion?.spec ?? source?.spec; if (!spec) { throw new FirebaseError( - `Could not find the extension.yaml for ${extensionName}. Please make sure this is a valid extension and try again.` + `Could not find the extension.yaml for ${extensionRef}. Please make sure this is a valid extension and try again.` ); } @@ -230,7 +233,7 @@ async function installToManifest(options: InstallExtensionOptions): Promise { +export async function resolveVersion(ref: refs.Ref, extension?: Extension): Promise { const extensionRef = refs.toExtensionRef(ref); - const extension = await extensionsApi.getExtension(extensionRef); - if (!ref.version || ref.version === "latest-approved") { - if (!extension.latestApprovedVersion) { + if (!ref.version && extension?.latestApprovedVersion) { + ref.version = extension.latestApprovedVersion; + return refs.toExtensionVersionRef(ref); + } + if (ref.version === "latest-approved") { + if (!extension?.latestApprovedVersion) { throw new FirebaseError( `${extensionRef} has not been published to Extensions Hub (https://extensions.dev). To install it, you must specify the version you want to install.` ); } - return extension.latestApprovedVersion; + ref.version = extension.latestApprovedVersion; + return refs.toExtensionVersionRef(ref); } - if (ref.version === "latest") { - if (!extension.latestVersion) { + if (!ref.version || ref.version === "latest") { + if (!extension?.latestVersion) { throw new FirebaseError( `${extensionRef} has no stable non-deprecated versions. If you wish to install a prerelease version, you must specify the version you want to install.` ); } - return extension.latestVersion; + ref.version = extension.latestVersion; + return refs.toExtensionVersionRef(ref); } - const versions = await extensionsApi.listExtensionVersions(extensionRef, undefined, true); if (versions.length === 0) { throw new FirebaseError(`No versions found for ${extensionRef}`); @@ -246,5 +250,6 @@ export async function resolveVersion(ref: refs.Ref): Promise { `No version of ${extensionRef} matches requested version ${ref.version}` ); } - return maxSatisfying; + ref.version = maxSatisfying; + return refs.toExtensionVersionRef(ref); } diff --git a/src/extensions/extensionsHelper.ts b/src/extensions/extensionsHelper.ts index 063d526af9e..b61073423f5 100644 --- a/src/extensions/extensionsHelper.ts +++ b/src/extensions/extensionsHelper.ts @@ -42,7 +42,6 @@ import { envOverride } from "../utils"; import { getLocalChangelog } from "./change-log"; import { getProjectNumber } from "../getProjectNumber"; import { Constants } from "../emulator/constants"; -import { resolveVersion } from "../deploy/extensions/planner"; /** * SpecParamType represents the exact strings that the extensions @@ -1245,25 +1244,3 @@ export async function diagnoseAndFixProject(options: any): Promise { throw new FirebaseError("Unable to proceed until all issues are resolved."); } } - -/** - * Canonicalize a user-inputted ref string. - * 1. Infer firebase publisher if not provided - * 2. Infer "latest-approved" as the version if not provided - */ -export async function canonicalizeRefInput(refInput: string): Promise { - let inferredRef = refInput; - // TODO: Stop defaulting to 'firebase' publisher ID if none provided. - // Infer 'firebase' if publisher ID not provided. - if (refInput.split("/").length < 2) { - inferredRef = `firebase/${inferredRef}`; - } - // Infer 'latest-approved' if no version provided. - if (refInput.split("@").length < 2) { - inferredRef = `${inferredRef}@latest-approved`; - } - // Get the correct version for a given extension reference from the Registry API. - const ref = refs.parse(inferredRef); - ref.version = await resolveVersion(ref); - return refs.toExtensionVersionRef(ref); -} diff --git a/src/test/extensions/extensionsHelper.spec.ts b/src/test/extensions/extensionsHelper.spec.ts index 446890600c6..66e995e0e50 100644 --- a/src/test/extensions/extensionsHelper.spec.ts +++ b/src/test/extensions/extensionsHelper.spec.ts @@ -22,7 +22,6 @@ import { } from "../../extensions/types"; import { Readable } from "stream"; import { ArchiveResult } from "../../archiveDirectory"; -import { canonicalizeRefInput } from "../../extensions/extensionsHelper"; import * as planner from "../../deploy/extensions/planner"; describe("extensionsHelper", () => { @@ -1004,37 +1003,4 @@ describe("extensionsHelper", () => { ).to.eql("Prerelease"); }); }); - - describe(`${canonicalizeRefInput.name}`, () => { - let resolveVersionStub: sinon.SinonStub; - beforeEach(() => { - resolveVersionStub = sinon.stub(planner, "resolveVersion").resolves("10.1.1"); - }); - afterEach(() => { - resolveVersionStub.restore(); - }); - it("should do nothing to a valid ref", async () => { - expect(await canonicalizeRefInput("firebase/bigquery-export@10.1.1")).to.equal( - "firebase/bigquery-export@10.1.1" - ); - }); - - it("should infer latest version", async () => { - expect(await canonicalizeRefInput("firebase/bigquery-export")).to.equal( - "firebase/bigquery-export@10.1.1" - ); - }); - - it("should infer publisher name as firebase", async () => { - expect(await canonicalizeRefInput("firebase/bigquery-export")).to.equal( - "firebase/bigquery-export@10.1.1" - ); - }); - - it("should infer publisher name as firebase and also infer latest as version", async () => { - expect(await canonicalizeRefInput("bigquery-export")).to.equal( - "firebase/bigquery-export@10.1.1" - ); - }); - }); }); From caf805d24d6ed20c2313cdca79afa511f3f5106a Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Thu, 15 Jun 2023 07:16:39 -0700 Subject: [PATCH 02/24] Changed output of extension version info. --- src/commands/ext-install.ts | 50 ++++-------- src/extensions/displayExtensionInfo.ts | 102 +++++++++++-------------- src/extensions/updateHelper.ts | 6 +- 3 files changed, 64 insertions(+), 94 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 51a30099a53..25e99c16ba5 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -2,7 +2,7 @@ import * as clc from "colorette"; import { marked } from "marked"; import * as TerminalRenderer from "marked-terminal"; -import { displayExtInfo } from "../extensions/displayExtensionInfo"; +import { displayExtensionVersionInfo } from "../extensions/displayExtensionInfo"; import * as askUserForEventsConfig from "../extensions/askUserForEventsConfig"; import { checkMinRequiredVersion } from "../checkMinRequiredVersion"; import { Command } from "../command"; @@ -55,25 +55,21 @@ export const command = new Command("ext:install [extensionRef]") const projectId = getProjectId(options); // TODO(b/230598656): Clean up paramsEnvPath after v11 launch. const paramsEnvPath = ""; - let learnMore = false; if (!extensionRef) { if (options.interactive) { - learnMore = true; - extensionRef = await promptForOfficialExtension( - "Which official extension do you wish to install?\n" + - " Select an extension, then press Enter to learn more." - ); + // TODO(alexpascal): Query the backend for published extensions instead. + extensionRef = await promptForOfficialExtension("Which extension do you wish to install?"); } else { throw new FirebaseError( - `Unable to find published extension '${clc.bold(extensionRef)}'. ` + + `Unable to find extension '${clc.bold(extensionRef)}'. ` + `Run ${clc.bold( "firebase ext:install -i" - )} to select from the list of all available published extensions.` + )} to select from the list of all available extensions.` ); } } - let source; - let extensionVersion; + let source: ExtensionSource | undefined; + let extensionVersion: ExtensionVersion | undefined; // TODO(b/220900194): Remove when deprecating old install flow. // --local doesn't support urlPath so this will become dead codepath. @@ -95,18 +91,21 @@ export const command = new Command("ext:install [extensionRef]") // TODO(b/228444119): Create source should happen at deploy time. // Should parse spec locally so we don't need project ID. source = await createSourceFromLocation(needProjectId({ projectId }), extensionRef); - await displayExtInfo(extensionRef, "", source.spec); + await displayExtensionVersionInfo(source.spec); void track("Extension Install", "Install by Source", options.interactive ? 1 : 0); } else { void track("Extension Install", "Install by Extension Ref", options.interactive ? 1 : 0); const extension = await extensionsApi.getExtension(extensionRef); const extensionVersionRef = await resolveVersion(refs.parse(extensionRef), extension); extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef); - await infoExtensionVersion({ - extensionRef, - extensionVersion, - published: !!extension.latestApprovedVersion, - }); + if (extensionVersion.state === "DEPRECATED") { + throw new FirebaseError( + `Extension version ${clc.bold( + extensionVersionRef + )} is deprecated and cannot be installed. To install the latest non-deprecated version, omit the version in \`extensionRef\`.` + ); + } + await displayExtensionVersionInfo(extensionVersion.spec, extensionVersion); } if ( !(await confirm({ @@ -130,14 +129,6 @@ export const command = new Command("ext:install [extensionRef]") )}'. Please make sure this is a valid extension and try again.` ); } - if (learnMore) { - utils.logLabeledBullet( - logPrefix, - `You selected: ${clc.bold(spec.displayName || "")}.\n` + - `${spec.description}\n` + - `View details: https://firebase.google.com/products/extensions/${spec.name}\n` - ); - } try { return installToManifest({ @@ -159,15 +150,6 @@ export const command = new Command("ext:install [extensionRef]") } }); -async function infoExtensionVersion(args: { - extensionRef: string; - extensionVersion: ExtensionVersion; - published: boolean; -}): Promise { - const ref = refs.parse(args.extensionRef); - await displayExtInfo(args.extensionRef, ref.publisherId, args.extensionVersion.spec, args.published); -} - interface InstallExtensionOptions { paramsEnvPath?: string; projectId?: string; diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index aef30fe192a..15400744149 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -1,12 +1,21 @@ import * as clc from "colorette"; import { marked } from "marked"; import * as TerminalRenderer from "marked-terminal"; +import * as path from "path"; import * as utils from "../utils"; import { logPrefix } from "./extensionsHelper"; +import * as refs from "../extensions/refs"; import { logger } from "../logger"; import { FirebaseError } from "../error"; -import { Api, ExtensionSpec, Role, Resource, FUNCTIONS_RESOURCE_TYPE } from "./types"; +import { + Api, + ExtensionSpec, + ExtensionVersion, + Role, + Resource, + FUNCTIONS_RESOURCE_TYPE, +} from "./types"; import * as iam from "../gcp/iam"; import { SECRET_ROLE, usesSecrets } from "./secretsUtils"; @@ -18,34 +27,38 @@ const TASKS_ROLE = "cloudtasks.enqueuer"; const TASKS_API = "cloudtasks.googleapis.com"; /** - * displayExtInfo prints the extension info displayed when running ext:install. + * Displays info about an extension version, whether it is uploaded to the registry or a local spec. * - * @param extensionName name of the extension to display information about - * @param spec extension spec - * @param published whether or not the extension is a published extension - */ -export async function displayExtInfo( - extensionName: string, - publisher: string, + * @param spec the extension spec + * @param extensionVersion the extension version + * */ +export async function displayExtensionVersionInfo( spec: ExtensionSpec, - published = false + extensionVersion?: ExtensionVersion ): Promise { + const extensionRef = extensionVersion + ? `(${refs.toExtensionRef(refs.parse(extensionVersion.ref))})` + : ""; const lines = []; - lines.push(`**Name**: ${spec.displayName}`); - if (publisher) { - lines.push(`**Publisher**: ${publisher}`); - } - if (spec.description) { - lines.push(`**Description**: ${spec.description}`); - } - if (published) { - if (spec.license) { - lines.push(`**License**: ${spec.license}`); - } - if (spec.sourceUrl) { - lines.push(`**Source code**: ${spec.sourceUrl}`); + lines.push(`${clc.bold("Extension:")} ${spec.displayName} ${extensionRef}`); + lines.push(`${clc.bold("Description:")} ${spec.description}`); + // TODO(alexpascal): Add latest version or add metadata about the version (e.g. not latest, rejected, etc). + lines.push(`${clc.bold("Version:")} ${spec.version}`); + if (extensionVersion) { + if (extensionVersion.buildSourceUri) { + const buildSourceUri = new URL(extensionVersion.buildSourceUri!); + buildSourceUri.pathname = path.join( + buildSourceUri.pathname, + extensionVersion.extensionRoot ?? "" + ); + lines.push(`${clc.bold("Source in GitHub:")} ${buildSourceUri}`); + } else { + lines.push( + `${clc.bold("Source download URI:")} ${extensionVersion.sourceDownloadUri ?? "-"}` + ); } } + lines.push(`${clc.bold("License:")} ${spec.license ?? "-"}`); const apis = impliedApis(spec); if (apis.length) { lines.push(displayApis(apis)); @@ -54,33 +67,8 @@ export async function displayExtInfo( if (roles.length) { lines.push(await displayRoles(roles)); } - if (lines.length > 0) { - utils.logLabeledBullet(logPrefix, `information about '${clc.bold(extensionName)}':`); - const infoStr = lines.join("\n"); - // Convert to markdown and convert any trailing newlines to a single newline. - const formatted = marked(infoStr).replace(/\n+$/, "\n"); - logger.info(formatted); - // Return for testing purposes. - return lines; - } else { - throw new FirebaseError( - "Error occurred during installation: cannot parse info from source spec", - { - context: { - spec: spec, - extensionName: extensionName, - }, - } - ); - } -} - -/** - * Prints a clickable link where users can download the source code for an Extension Version. - */ -export function printSourceDownloadLink(sourceDownloadUri: string): void { - const sourceDownloadMsg = `Want to review the source code that will be installed? Download it here: ${sourceDownloadUri}`; - utils.logBullet(marked(sourceDownloadMsg)); + logger.info(`\n${lines.join("\n")}\n`); + return lines; } /** @@ -92,7 +80,7 @@ export function printSourceDownloadLink(sourceDownloadUri: string): void { */ export async function retrieveRoleInfo(role: string) { const res = await iam.getRole(role); - return ` ${res.title} (${res.description})`; + return ` ${clc.yellow(res.title!)}: ${res.description}`; } async function displayRoles(roles: Role[]): Promise { @@ -101,14 +89,14 @@ async function displayRoles(roles: Role[]): Promise { return retrieveRoleInfo(role.role); }) ); - return clc.bold("**Roles granted to this Extension**:\n") + lines.join("\n"); + return clc.bold("Roles granted to this extension:\n") + lines.join("\n"); } function displayApis(apis: Api[]): string { const lines: string[] = apis.map((api: Api) => { - return ` ${api.apiName} (${api.reason})`; + return ` ${clc.blue(api.apiName!)}: ${api.reason}`; }); - return "**APIs used by this Extension**:\n" + lines.join("\n"); + return clc.bold("APIs used by this extension:\n") + lines.join("\n"); } function usesTasks(spec: ExtensionSpec): boolean { @@ -123,13 +111,13 @@ function impliedRoles(spec: ExtensionSpec): Role[] { if (usesSecrets(spec) && !spec.roles?.some((r: Role) => r.role === SECRET_ROLE)) { roles.push({ role: SECRET_ROLE, - reason: "Allows the extension to read secret values from Cloud Secret Manager", + reason: "Allows the extension to read secret values from Cloud Secret Manager.", }); } if (usesTasks(spec) && !spec.roles?.some((r: Role) => r.role === TASKS_ROLE)) { roles.push({ role: TASKS_ROLE, - reason: "Allows the extension to enqueue Cloud Tasks", + reason: "Allows the extension to enqueue Cloud Tasks.", }); } return roles.concat(spec.roles ?? []); @@ -140,7 +128,7 @@ function impliedApis(spec: ExtensionSpec): Api[] { if (usesTasks(spec) && !spec.apis?.some((a: Api) => a.apiName === TASKS_API)) { apis.push({ apiName: TASKS_API, - reason: "Allows the extension to enqueue Cloud Tasks", + reason: "Allows the extension to enqueue Cloud Tasks.", }); } diff --git a/src/extensions/updateHelper.ts b/src/extensions/updateHelper.ts index 521d91cb013..a87a20e4900 100644 --- a/src/extensions/updateHelper.ts +++ b/src/extensions/updateHelper.ts @@ -13,7 +13,7 @@ import { isLocalOrURLPath, } from "./extensionsHelper"; import * as utils from "../utils"; -import { displayExtInfo } from "./displayExtensionInfo"; +import { displayExtensionVersionInfo } from "./displayExtensionInfo"; function invalidSourceErrMsgTemplate(instanceId: string, source: string): string { return `Unable to update from the source \`${clc.bold( @@ -157,7 +157,7 @@ export async function updateFromLocalSource( localSource: string, existingSpec: ExtensionSpec ): Promise { - await displayExtInfo(instanceId, "", existingSpec, false); + await displayExtensionVersionInfo(existingSpec); let source; try { source = await createSourceFromLocation(projectId, localSource); @@ -187,7 +187,7 @@ export async function updateFromUrlSource( urlSource: string, existingSpec: ExtensionSpec ): Promise { - await displayExtInfo(instanceId, "", existingSpec, false); + await displayExtensionVersionInfo(existingSpec); let source; try { source = await createSourceFromLocation(projectId, urlSource); From 4ce8fc6b715b7ec77d6bf889fde090609acec8bc Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Thu, 15 Jun 2023 11:54:03 -0700 Subject: [PATCH 03/24] Formatting, added more metadata, and cleaned up TODOs. --- src/commands/ext-configure.ts | 2 - src/commands/ext-install.ts | 56 ++++++++------------------ src/commands/ext-update.ts | 2 - src/extensions/displayExtensionInfo.ts | 41 ++++++++++++++----- src/extensions/paramHelper.ts | 2 - 5 files changed, 46 insertions(+), 57 deletions(-) diff --git a/src/commands/ext-configure.ts b/src/commands/ext-configure.ts index 994523de058..fd6ba7e6577 100644 --- a/src/commands/ext-configure.ts +++ b/src/commands/ext-configure.ts @@ -88,8 +88,6 @@ export const command = new Command("ext:configure ") projectId, paramSpecs: tbdParams, nonInteractive: false, - // TODO(b/230598656): Clean up paramsEnvPath after v11 launch. - paramsEnvPath: "", instanceId, reconfiguring: true, }); diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 25e99c16ba5..1ed27a043fb 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -52,39 +52,20 @@ export const command = new Command("ext:install [extensionRef]") .before(checkMinRequiredVersion, "extMinVersion") .before(diagnoseAndFixProject) .action(async (extensionRef: string, options: Options) => { - const projectId = getProjectId(options); - // TODO(b/230598656): Clean up paramsEnvPath after v11 launch. - const paramsEnvPath = ""; - if (!extensionRef) { - if (options.interactive) { - // TODO(alexpascal): Query the backend for published extensions instead. - extensionRef = await promptForOfficialExtension("Which extension do you wish to install?"); - } else { - throw new FirebaseError( - `Unable to find extension '${clc.bold(extensionRef)}'. ` + - `Run ${clc.bold( - "firebase ext:install -i" - )} to select from the list of all available extensions.` - ); - } - } - let source: ExtensionSource | undefined; - let extensionVersion: ExtensionVersion | undefined; - - // TODO(b/220900194): Remove when deprecating old install flow. - // --local doesn't support urlPath so this will become dead codepath. - if (isUrlPath(extensionRef)) { - throw new FirebaseError( - `Installing with a source url is no longer supported in the CLI. Please use Firebase Console instead.` - ); - } if (options.local) { utils.logLabeledWarning( logPrefix, "As of firebase-tools@11.0.0, the `--local` flag is no longer required, as it is the default behavior." ); } - + if (!extensionRef) { + throw new FirebaseError( + "Extension ref is required to install. To see a full list of available extensions, go to https://extensions.dev/extensions." + ); + } + let source: ExtensionSource | undefined; + let extensionVersion: ExtensionVersion | undefined; + const projectId = getProjectId(options); // If the user types in a local path (prefixed with ~/, ../, or ./), install from local source. // Otherwise, treat the input as an extension reference and proceed with reference-based installation. if (isLocalPath(extensionRef)) { @@ -98,14 +79,19 @@ export const command = new Command("ext:install [extensionRef]") const extension = await extensionsApi.getExtension(extensionRef); const extensionVersionRef = await resolveVersion(refs.parse(extensionRef), extension); extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef); + await displayExtensionVersionInfo(extensionVersion.spec, extensionVersion); if (extensionVersion.state === "DEPRECATED") { throw new FirebaseError( `Extension version ${clc.bold( extensionVersionRef - )} is deprecated and cannot be installed. To install the latest non-deprecated version, omit the version in \`extensionRef\`.` + )} is deprecated and cannot be installed. To install the latest non-deprecated version, omit the version in the extension ref.` ); } - await displayExtensionVersionInfo(extensionVersion.spec, extensionVersion); + } + if (!source && !extensionVersion) { + throw new FirebaseError( + "Could not find a source. Please specify a valid source to continue." + ); } if ( !(await confirm({ @@ -116,11 +102,6 @@ export const command = new Command("ext:install [extensionRef]") ) { return; } - if (!source && !extensionVersion) { - throw new FirebaseError( - "Could not find a source. Please specify a valid source to continue." - ); - } const spec = source?.spec ?? extensionVersion?.spec; if (!spec) { throw new FirebaseError( @@ -129,10 +110,8 @@ export const command = new Command("ext:install [extensionRef]") )}'. Please make sure this is a valid extension and try again.` ); } - try { return installToManifest({ - paramsEnvPath, projectId, extensionRef, source, @@ -151,7 +130,6 @@ export const command = new Command("ext:install [extensionRef]") }); interface InstallExtensionOptions { - paramsEnvPath?: string; projectId?: string; extensionRef: string; source?: ExtensionSource; @@ -167,8 +145,7 @@ interface InstallExtensionOptions { * @param options */ async function installToManifest(options: InstallExtensionOptions): Promise { - const { projectId, extensionRef, extVersion, source, paramsEnvPath, nonInteractive, force } = - options; + const { projectId, extensionRef, extVersion, source, nonInteractive, force } = options; const isLocalSource = isLocalPath(extensionRef); const spec = extVersion?.spec ?? source?.spec; @@ -193,7 +170,6 @@ async function installToManifest(options: InstallExtensionOptions): Promise [updateSour newSpec: newExtensionVersion.spec, currentParams: oldParamValues, projectId, - // TODO(b/230598656): Clean up paramsEnvPath after v11 launch. - paramsEnvPath: "", nonInteractive: options.nonInteractive, instanceId, }); diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 15400744149..fb01f637479 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -36,15 +36,34 @@ export async function displayExtensionVersionInfo( spec: ExtensionSpec, extensionVersion?: ExtensionVersion ): Promise { - const extensionRef = extensionVersion - ? `(${refs.toExtensionRef(refs.parse(extensionVersion.ref))})` - : ""; - const lines = []; - lines.push(`${clc.bold("Extension:")} ${spec.displayName} ${extensionRef}`); - lines.push(`${clc.bold("Description:")} ${spec.description}`); - // TODO(alexpascal): Add latest version or add metadata about the version (e.g. not latest, rejected, etc). - lines.push(`${clc.bold("Version:")} ${spec.version}`); + const lines: string[] = []; + lines.push( + `${clc.bold("Extension:")} ${spec.displayName ?? "Unnamed extension"} ${ + extensionVersion ? `(${refs.toExtensionRef(refs.parse(extensionVersion.ref))})` : "" + }` + ); + if (spec.description) { + lines.push(`${clc.bold("Description")} ${spec.description}`); + } + lines.push( + `${clc.bold("Version:")} ${spec.version} ${ + extensionVersion?.state === "DEPRECATED" ? `(${clc.red("Deprecated")})` : "" + }` + ); if (extensionVersion) { + let reviewStatus: string; + switch (extensionVersion.listing?.state) { + case "APPROVED": + reviewStatus = clc.bold(clc.green("Accepted")); + break; + case "REJECTED": + reviewStatus = clc.bold(clc.red("Rejected")); + break; + default: + reviewStatus = clc.bold(clc.yellow("Unreviewed")); + break; + } + lines.push(`${clc.bold("Review status:")} ${reviewStatus}`); if (extensionVersion.buildSourceUri) { const buildSourceUri = new URL(extensionVersion.buildSourceUri!); buildSourceUri.pathname = path.join( @@ -67,7 +86,7 @@ export async function displayExtensionVersionInfo( if (roles.length) { lines.push(await displayRoles(roles)); } - logger.info(`\n${lines.join("\n")}\n`); + logger.info(`\n${lines.join("\n")}`); return lines; } @@ -80,7 +99,7 @@ export async function displayExtensionVersionInfo( */ export async function retrieveRoleInfo(role: string) { const res = await iam.getRole(role); - return ` ${clc.yellow(res.title!)}: ${res.description}`; + return ` - ${clc.cyan(res.title!)}: ${res.description}`; } async function displayRoles(roles: Role[]): Promise { @@ -94,7 +113,7 @@ async function displayRoles(roles: Role[]): Promise { function displayApis(apis: Api[]): string { const lines: string[] = apis.map((api: Api) => { - return ` ${clc.blue(api.apiName!)}: ${api.reason}`; + return ` - ${clc.cyan(api.apiName!)}: ${api.reason}`; }); return clc.bold("APIs used by this extension:\n") + lines.join("\n"); } diff --git a/src/extensions/paramHelper.ts b/src/extensions/paramHelper.ts index 8231d69ebaf..33b2e15edaf 100644 --- a/src/extensions/paramHelper.ts +++ b/src/extensions/paramHelper.ts @@ -95,7 +95,6 @@ export async function getParams(args: { instanceId: string; paramSpecs: Param[]; nonInteractive?: boolean; - paramsEnvPath?: string; reconfiguring?: boolean; }): Promise> { let params: Record; @@ -121,7 +120,6 @@ export async function getParamsForUpdate(args: { newSpec: ExtensionSpec; currentParams: { [option: string]: string }; projectId?: string; - paramsEnvPath?: string; nonInteractive?: boolean; instanceId: string; }): Promise> { From 3ce0c9564be12ec776a0b8e7e515984e95febe9d Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 03:46:23 -0700 Subject: [PATCH 04/24] Formatting and extra notices. --- src/commands/ext-install.ts | 13 ++++++++++++- src/extensions/displayExtensionInfo.ts | 9 +++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 1ed27a043fb..0fd660da5c0 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -1,5 +1,6 @@ import * as clc from "colorette"; import { marked } from "marked"; +import * as semver from "semver"; import * as TerminalRenderer from "marked-terminal"; import { displayExtensionVersionInfo } from "../extensions/displayExtensionInfo"; @@ -7,6 +8,7 @@ import * as askUserForEventsConfig from "../extensions/askUserForEventsConfig"; import { checkMinRequiredVersion } from "../checkMinRequiredVersion"; import { Command } from "../command"; import { FirebaseError } from "../error"; +import { logger } from "../logger"; import { getProjectId, needProjectId } from "../projectUtils"; import * as extensionsApi from "../extensions/extensionsApi"; import { ExtensionVersion, ExtensionSource, Extension } from "../extensions/types"; @@ -60,7 +62,7 @@ export const command = new Command("ext:install [extensionRef]") } if (!extensionRef) { throw new FirebaseError( - "Extension ref is required to install. To see a full list of available extensions, go to https://extensions.dev/extensions." + "Extension ref is required to install. To see a full list of available extensions, go to Extensions Hub (https://extensions.dev/extensions)." ); } let source: ExtensionSource | undefined; @@ -87,6 +89,15 @@ export const command = new Command("ext:install [extensionRef]") )} is deprecated and cannot be installed. To install the latest non-deprecated version, omit the version in the extension ref.` ); } + const latestApprovedSemver = semver.parse(extension.latestApprovedVersion!); + const selectedSemver = semver.parse(extensionVersion.spec.version); + if (latestApprovedSemver && semver.gt(latestApprovedSemver, selectedSemver!)) { + logger.info( + `You are about to install extension version ${clc.bold( + extensionVersion.spec.version + )} which is older than the latest version ${clc.bold(extension.latestApprovedVersion!)}.` + ); + } } if (!source && !extensionVersion) { throw new FirebaseError( diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index fb01f637479..7ada739eae5 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -3,11 +3,8 @@ import { marked } from "marked"; import * as TerminalRenderer from "marked-terminal"; import * as path from "path"; -import * as utils from "../utils"; -import { logPrefix } from "./extensionsHelper"; import * as refs from "../extensions/refs"; import { logger } from "../logger"; -import { FirebaseError } from "../error"; import { Api, ExtensionSpec, @@ -86,7 +83,7 @@ export async function displayExtensionVersionInfo( if (roles.length) { lines.push(await displayRoles(roles)); } - logger.info(`\n${lines.join("\n")}`); + logger.info(`\n${lines.join("\n")}\n`); return lines; } @@ -99,7 +96,7 @@ export async function displayExtensionVersionInfo( */ export async function retrieveRoleInfo(role: string) { const res = await iam.getRole(role); - return ` - ${clc.cyan(res.title!)}: ${res.description}`; + return ` - ${clc.yellow(res.title!)}: ${res.description}`; } async function displayRoles(roles: Role[]): Promise { @@ -113,7 +110,7 @@ async function displayRoles(roles: Role[]): Promise { function displayApis(apis: Api[]): string { const lines: string[] = apis.map((api: Api) => { - return ` - ${clc.cyan(api.apiName!)}: ${api.reason}`; + return ` - ${clc.yellow(api.apiName!)}: ${api.reason}`; }); return clc.bold("APIs used by this extension:\n") + lines.join("\n"); } From 1ef29929ba1b49688e0e49da1f561ed3bd336c1d Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 05:13:10 -0700 Subject: [PATCH 05/24] Added even more metadata. --- src/commands/ext-install.ts | 16 +++++++++++----- src/extensions/displayExtensionInfo.ts | 19 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 0fd660da5c0..b2e3fd27ae3 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -81,7 +81,7 @@ export const command = new Command("ext:install [extensionRef]") const extension = await extensionsApi.getExtension(extensionRef); const extensionVersionRef = await resolveVersion(refs.parse(extensionRef), extension); extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef); - await displayExtensionVersionInfo(extensionVersion.spec, extensionVersion); + await displayExtensionVersionInfo(extensionVersion.spec, extensionVersion, extension.latestApprovedVersion ?? extension.latestVersion); if (extensionVersion.state === "DEPRECATED") { throw new FirebaseError( `Extension version ${clc.bold( @@ -89,13 +89,19 @@ export const command = new Command("ext:install [extensionRef]") )} is deprecated and cannot be installed. To install the latest non-deprecated version, omit the version in the extension ref.` ); } - const latestApprovedSemver = semver.parse(extension.latestApprovedVersion!); - const selectedSemver = semver.parse(extensionVersion.spec.version); - if (latestApprovedSemver && semver.gt(latestApprovedSemver, selectedSemver!)) { + logger.info(); + // Check if selected version is older than the latest approved version, or the latest version only if there is no approved version. + if ( + (extension.latestApprovedVersion && + semver.gt(extension.latestApprovedVersion, extensionVersion.spec.version)) || + (!extension.latestApprovedVersion && + extension.latestVersion && + semver.gt(extension.latestVersion, extensionVersion.spec.version)) + ) { logger.info( `You are about to install extension version ${clc.bold( extensionVersion.spec.version - )} which is older than the latest version ${clc.bold(extension.latestApprovedVersion!)}.` + )} which is older than the latest ${extension.latestApprovedVersion ? "accepted version" : "version"} ${clc.bold(extension.latestApprovedVersion!)}.` ); } } diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 7ada739eae5..fabd919249b 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -1,5 +1,6 @@ import * as clc from "colorette"; import { marked } from "marked"; +import * as semver from "semver"; import * as TerminalRenderer from "marked-terminal"; import * as path from "path"; @@ -31,7 +32,8 @@ const TASKS_API = "cloudtasks.googleapis.com"; * */ export async function displayExtensionVersionInfo( spec: ExtensionSpec, - extensionVersion?: ExtensionVersion + extensionVersion?: ExtensionVersion, + latestRelevantVersion?: string ): Promise { const lines: string[] = []; lines.push( @@ -42,11 +44,14 @@ export async function displayExtensionVersionInfo( if (spec.description) { lines.push(`${clc.bold("Description")} ${spec.description}`); } - lines.push( - `${clc.bold("Version:")} ${spec.version} ${ - extensionVersion?.state === "DEPRECATED" ? `(${clc.red("Deprecated")})` : "" - }` - ); + let versionNote = ""; + if (latestRelevantVersion && semver.eq(extensionVersion?.spec.version!, latestRelevantVersion)) { + versionNote = `- ${clc.green("Latest")}`; + } + if (extensionVersion?.state === "DEPRECATED") { + versionNote = `- ${clc.red("Deprecated")}`; + } + lines.push(`${clc.bold("Version:")} ${spec.version} ${versionNote}`); if (extensionVersion) { let reviewStatus: string; switch (extensionVersion.listing?.state) { @@ -83,7 +88,7 @@ export async function displayExtensionVersionInfo( if (roles.length) { lines.push(await displayRoles(roles)); } - logger.info(`\n${lines.join("\n")}\n`); + logger.info(`\n${lines.join("\n")}`); return lines; } From 160eb51dc2187ed23a803409b35ef9433423b119 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 05:20:22 -0700 Subject: [PATCH 06/24] Formatting. --- src/commands/ext-install.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index b2e3fd27ae3..270df9b99c3 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -81,7 +81,11 @@ export const command = new Command("ext:install [extensionRef]") const extension = await extensionsApi.getExtension(extensionRef); const extensionVersionRef = await resolveVersion(refs.parse(extensionRef), extension); extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef); - await displayExtensionVersionInfo(extensionVersion.spec, extensionVersion, extension.latestApprovedVersion ?? extension.latestVersion); + await displayExtensionVersionInfo( + extensionVersion.spec, + extensionVersion, + extension.latestApprovedVersion ?? extension.latestVersion + ); if (extensionVersion.state === "DEPRECATED") { throw new FirebaseError( `Extension version ${clc.bold( @@ -101,7 +105,9 @@ export const command = new Command("ext:install [extensionRef]") logger.info( `You are about to install extension version ${clc.bold( extensionVersion.spec.version - )} which is older than the latest ${extension.latestApprovedVersion ? "accepted version" : "version"} ${clc.bold(extension.latestApprovedVersion!)}.` + )} which is older than the latest ${ + extension.latestApprovedVersion ? "accepted version" : "version" + } ${clc.bold(extension.latestApprovedVersion!)}.` ); } } From 55587605d2378eb691c84c61055ba2f041d712ab Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 05:26:48 -0700 Subject: [PATCH 07/24] Fixing tests. --- src/commands/ext-install.ts | 6 ++---- src/deploy/extensions/planner.ts | 4 ++-- src/extensions/displayExtensionInfo.ts | 6 +++++- src/test/extensions/extensionsHelper.spec.ts | 1 - 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 270df9b99c3..74fd6daaac6 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -11,7 +11,7 @@ import { FirebaseError } from "../error"; import { logger } from "../logger"; import { getProjectId, needProjectId } from "../projectUtils"; import * as extensionsApi from "../extensions/extensionsApi"; -import { ExtensionVersion, ExtensionSource, Extension } from "../extensions/types"; +import { ExtensionVersion, ExtensionSource } from "../extensions/types"; import * as refs from "../extensions/refs"; import * as secretsUtils from "../extensions/secretsUtils"; import * as paramHelper from "../extensions/paramHelper"; @@ -19,10 +19,8 @@ import { createSourceFromLocation, ensureExtensionsApiEnabled, logPrefix, - promptForOfficialExtension, promptForValidInstanceId, diagnoseAndFixProject, - isUrlPath, isLocalPath, } from "../extensions/extensionsHelper"; import { resolveVersion } from "../deploy/extensions/planner"; @@ -84,7 +82,7 @@ export const command = new Command("ext:install [extensionRef]") await displayExtensionVersionInfo( extensionVersion.spec, extensionVersion, - extension.latestApprovedVersion ?? extension.latestVersion + extension.latestApprovedVersion || extension.latestVersion ); if (extensionVersion.state === "DEPRECATED") { throw new FirebaseError( diff --git a/src/deploy/extensions/planner.ts b/src/deploy/extensions/planner.ts index 7c8c194ec78..613522fcf1b 100644 --- a/src/deploy/extensions/planner.ts +++ b/src/deploy/extensions/planner.ts @@ -207,9 +207,9 @@ export async function want(args: { } /** - * Resolves a semver string to the max matching version. If no version is specified, + * Resolves a semver string to the max matching version. If no version is specified, * it will default to the extension's latest approved version if set, otherwise to the latest version. - * + * * @param ref the extension version ref * @param extension the extension (optional) */ diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index fabd919249b..a654ad91710 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -45,7 +45,11 @@ export async function displayExtensionVersionInfo( lines.push(`${clc.bold("Description")} ${spec.description}`); } let versionNote = ""; - if (latestRelevantVersion && semver.eq(extensionVersion?.spec.version!, latestRelevantVersion)) { + if ( + latestRelevantVersion && + extensionVersion?.spec.version && + semver.eq(extensionVersion?.spec.version, latestRelevantVersion) + ) { versionNote = `- ${clc.green("Latest")}`; } if (extensionVersion?.state === "DEPRECATED") { diff --git a/src/test/extensions/extensionsHelper.spec.ts b/src/test/extensions/extensionsHelper.spec.ts index 66e995e0e50..51c8fab7b99 100644 --- a/src/test/extensions/extensionsHelper.spec.ts +++ b/src/test/extensions/extensionsHelper.spec.ts @@ -22,7 +22,6 @@ import { } from "../../extensions/types"; import { Readable } from "stream"; import { ArchiveResult } from "../../archiveDirectory"; -import * as planner from "../../deploy/extensions/planner"; describe("extensionsHelper", () => { describe("substituteParams", () => { From e3e70652b63f255e120dc09cadcde188befa96e2 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 07:35:58 -0700 Subject: [PATCH 08/24] Added display of extension resources. --- src/extensions/displayExtensionInfo.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index a654ad91710..649e4ef1570 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -84,6 +84,7 @@ export async function displayExtensionVersionInfo( } } lines.push(`${clc.bold("License:")} ${spec.license ?? "-"}`); + lines.push(displayResources(spec)); const apis = impliedApis(spec); if (apis.length) { lines.push(displayApis(apis)); @@ -96,6 +97,13 @@ export async function displayExtensionVersionInfo( return lines; } +export function displayResources(spec: ExtensionSpec) { + const lines: string[] = spec.resources.map((resource: Resource) => { + return ` - ${clc.blue(`${resource.name} (${resource.type})`)}: ${resource.description}`; + }); + return clc.bold("Resources created by this extension:\n") + lines.join("\n"); +} + /** * Returns a string representing a Role, see * https://cloud.google.com/iam/reference/rest/v1/organizations.roles#Role @@ -119,7 +127,7 @@ async function displayRoles(roles: Role[]): Promise { function displayApis(apis: Api[]): string { const lines: string[] = apis.map((api: Api) => { - return ` - ${clc.yellow(api.apiName!)}: ${api.reason}`; + return ` - ${clc.cyan(api.apiName!)}: ${api.reason}`; }); return clc.bold("APIs used by this extension:\n") + lines.join("\n"); } From d8fcfb4c5b00f2c795cce963ac41bd653b7d6cd4 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 07:49:15 -0700 Subject: [PATCH 09/24] Added link to Extensions Hub. --- src/commands/ext-install.ts | 3 ++- src/extensions/displayExtensionInfo.ts | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 74fd6daaac6..20865adc39a 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -82,7 +82,8 @@ export const command = new Command("ext:install [extensionRef]") await displayExtensionVersionInfo( extensionVersion.spec, extensionVersion, - extension.latestApprovedVersion || extension.latestVersion + extension.latestApprovedVersion, + extension.latestVersion ); if (extensionVersion.state === "DEPRECATED") { throw new FirebaseError( diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 649e4ef1570..47c9aa2c638 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -33,23 +33,20 @@ const TASKS_API = "cloudtasks.googleapis.com"; export async function displayExtensionVersionInfo( spec: ExtensionSpec, extensionVersion?: ExtensionVersion, - latestRelevantVersion?: string + latestApprovedVersion?: string, + latestVersion?: string ): Promise { const lines: string[] = []; + const extensionRef = extensionVersion ? refs.toExtensionRef(refs.parse(extensionVersion?.ref)) : ""; lines.push( - `${clc.bold("Extension:")} ${spec.displayName ?? "Unnamed extension"} ${ - extensionVersion ? `(${refs.toExtensionRef(refs.parse(extensionVersion.ref))})` : "" - }` + `${clc.bold("Extension:")} ${spec.displayName ?? "Unnamed extension"} ${extensionRef ? `(${extensionRef})` : ""}` ); if (spec.description) { - lines.push(`${clc.bold("Description")} ${spec.description}`); + lines.push(`${clc.bold("Description:")} ${spec.description}`); } let versionNote = ""; - if ( - latestRelevantVersion && - extensionVersion?.spec.version && - semver.eq(extensionVersion?.spec.version, latestRelevantVersion) - ) { + const latestRelevantVersion = latestApprovedVersion || latestVersion; + if (latestRelevantVersion && semver.eq(spec.version, latestRelevantVersion)) { versionNote = `- ${clc.green("Latest")}`; } if (extensionVersion?.state === "DEPRECATED") { @@ -70,6 +67,11 @@ export async function displayExtensionVersionInfo( break; } lines.push(`${clc.bold("Review status:")} ${reviewStatus}`); + if (latestApprovedVersion) { + lines.push( + `${clc.bold("View in Extensions Hub:")} https://extensions.dev/extensions/${extensionRef}` + ); + } if (extensionVersion.buildSourceUri) { const buildSourceUri = new URL(extensionVersion.buildSourceUri!); buildSourceUri.pathname = path.join( From 83abfb079bd3004ae12be9d8945de820b922f2b2 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 08:09:59 -0700 Subject: [PATCH 10/24] Added displaying of events. --- src/extensions/displayExtensionInfo.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 47c9aa2c638..00104f2c6b6 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -13,6 +13,7 @@ import { Role, Resource, FUNCTIONS_RESOURCE_TYPE, + EventDescriptor, } from "./types"; import * as iam from "../gcp/iam"; import { SECRET_ROLE, usesSecrets } from "./secretsUtils"; @@ -87,6 +88,9 @@ export async function displayExtensionVersionInfo( } lines.push(`${clc.bold("License:")} ${spec.license ?? "-"}`); lines.push(displayResources(spec)); + if (spec.events) { + lines.push(displayEvents(spec)); + } const apis = impliedApis(spec); if (apis.length) { lines.push(displayApis(apis)); @@ -99,11 +103,18 @@ export async function displayExtensionVersionInfo( return lines; } +export function displayEvents(spec: ExtensionSpec) { + const lines = spec.events?.map((event: EventDescriptor) => { + return ` - ${clc.magenta(event.type)}: ${event.description || "-"}`; + }); + return clc.bold("Events emitted by this extension:\n") + (lines?.length ? lines.join("\n") : " - None"); +} + export function displayResources(spec: ExtensionSpec) { - const lines: string[] = spec.resources.map((resource: Resource) => { - return ` - ${clc.blue(`${resource.name} (${resource.type})`)}: ${resource.description}`; + const lines = spec.resources.map((resource: Resource) => { + return ` - ${clc.blue(`${resource.name} (${resource.type})`)}: ${resource.description || "-"}`; }); - return clc.bold("Resources created by this extension:\n") + lines.join("\n"); + return clc.bold("Resources created by this extension:\n") + (lines.length ? lines.join("\n") : " - None"); } /** @@ -115,7 +126,7 @@ export function displayResources(spec: ExtensionSpec) { */ export async function retrieveRoleInfo(role: string) { const res = await iam.getRole(role); - return ` - ${clc.yellow(res.title!)}: ${res.description}`; + return ` - ${clc.yellow(res.title!)}: ${res.description}`; } async function displayRoles(roles: Role[]): Promise { @@ -129,7 +140,7 @@ async function displayRoles(roles: Role[]): Promise { function displayApis(apis: Api[]): string { const lines: string[] = apis.map((api: Api) => { - return ` - ${clc.cyan(api.apiName!)}: ${api.reason}`; + return ` - ${clc.cyan(api.apiName!)}: ${api.reason}`; }); return clc.bold("APIs used by this extension:\n") + lines.join("\n"); } From 89bd3d4c589dfd0660dfa91ce1da332dc0c5e1f6 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 08:10:58 -0700 Subject: [PATCH 11/24] Formatting. --- src/extensions/displayExtensionInfo.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 00104f2c6b6..94bb46c0398 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -38,9 +38,13 @@ export async function displayExtensionVersionInfo( latestVersion?: string ): Promise { const lines: string[] = []; - const extensionRef = extensionVersion ? refs.toExtensionRef(refs.parse(extensionVersion?.ref)) : ""; + const extensionRef = extensionVersion + ? refs.toExtensionRef(refs.parse(extensionVersion?.ref)) + : ""; lines.push( - `${clc.bold("Extension:")} ${spec.displayName ?? "Unnamed extension"} ${extensionRef ? `(${extensionRef})` : ""}` + `${clc.bold("Extension:")} ${spec.displayName ?? "Unnamed extension"} ${ + extensionRef ? `(${extensionRef})` : "" + }` ); if (spec.description) { lines.push(`${clc.bold("Description:")} ${spec.description}`); @@ -107,14 +111,19 @@ export function displayEvents(spec: ExtensionSpec) { const lines = spec.events?.map((event: EventDescriptor) => { return ` - ${clc.magenta(event.type)}: ${event.description || "-"}`; }); - return clc.bold("Events emitted by this extension:\n") + (lines?.length ? lines.join("\n") : " - None"); + return ( + clc.bold("Events emitted by this extension:\n") + (lines?.length ? lines.join("\n") : " - None") + ); } export function displayResources(spec: ExtensionSpec) { const lines = spec.resources.map((resource: Resource) => { return ` - ${clc.blue(`${resource.name} (${resource.type})`)}: ${resource.description || "-"}`; }); - return clc.bold("Resources created by this extension:\n") + (lines.length ? lines.join("\n") : " - None"); + return ( + clc.bold("Resources created by this extension:\n") + + (lines.length ? lines.join("\n") : " - None") + ); } /** @@ -126,7 +135,7 @@ export function displayResources(spec: ExtensionSpec) { */ export async function retrieveRoleInfo(role: string) { const res = await iam.getRole(role); - return ` - ${clc.yellow(res.title!)}: ${res.description}`; + return ` - ${clc.yellow(res.title!)}: ${res.description || "-"}`; } async function displayRoles(roles: Role[]): Promise { From 8aa5f1869039b730c603679d9e1c18ad6d684c66 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 08:14:05 -0700 Subject: [PATCH 12/24] Formatting. --- src/extensions/displayExtensionInfo.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 94bb46c0398..00120707425 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -109,7 +109,7 @@ export async function displayExtensionVersionInfo( export function displayEvents(spec: ExtensionSpec) { const lines = spec.events?.map((event: EventDescriptor) => { - return ` - ${clc.magenta(event.type)}: ${event.description || "-"}`; + return ` - ${clc.magenta(event.type)}${event.description ? `: ${event.description}` : ""}`; }); return ( clc.bold("Events emitted by this extension:\n") + (lines?.length ? lines.join("\n") : " - None") @@ -118,7 +118,9 @@ export function displayEvents(spec: ExtensionSpec) { export function displayResources(spec: ExtensionSpec) { const lines = spec.resources.map((resource: Resource) => { - return ` - ${clc.blue(`${resource.name} (${resource.type})`)}: ${resource.description || "-"}`; + return ` - ${clc.blue(`${resource.name} (${resource.type})`)}${ + resource.description ? `: ${resource.description}` : "" + }`; }); return ( clc.bold("Resources created by this extension:\n") + @@ -135,7 +137,7 @@ export function displayResources(spec: ExtensionSpec) { */ export async function retrieveRoleInfo(role: string) { const res = await iam.getRole(role); - return ` - ${clc.yellow(res.title!)}: ${res.description || "-"}`; + return ` - ${clc.yellow(res.title!)}${res.description ? `: ${res.description}` : ""}`; } async function displayRoles(roles: Role[]): Promise { From 2f26e269a611dd34ef4ecc712b05c715e0f0a3d9 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Fri, 16 Jun 2023 08:58:25 -0700 Subject: [PATCH 13/24] Version bug. --- src/commands/ext-install.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 20865adc39a..0a57ed48f3f 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -101,12 +101,13 @@ export const command = new Command("ext:install [extensionRef]") extension.latestVersion && semver.gt(extension.latestVersion, extensionVersion.spec.version)) ) { + const version = extension.latestApprovedVersion || extension.latestVersion; logger.info( `You are about to install extension version ${clc.bold( extensionVersion.spec.version )} which is older than the latest ${ extension.latestApprovedVersion ? "accepted version" : "version" - } ${clc.bold(extension.latestApprovedVersion!)}.` + } ${clc.bold(version!)}.` ); } } From 7190227e8c19e20b02c7b08bef10986751722db1 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Sat, 17 Jun 2023 02:00:31 -0700 Subject: [PATCH 14/24] Added displaying of secrets and task queues. --- src/extensions/displayExtensionInfo.ts | 31 ++++++++++++++++++++------ src/extensions/types.ts | 6 +++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index 00120707425..c94ee0d9109 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -10,7 +10,9 @@ import { Api, ExtensionSpec, ExtensionVersion, + LifecycleEvent, Role, + Param, Resource, FUNCTIONS_RESOURCE_TYPE, EventDescriptor, @@ -92,7 +94,7 @@ export async function displayExtensionVersionInfo( } lines.push(`${clc.bold("License:")} ${spec.license ?? "-"}`); lines.push(displayResources(spec)); - if (spec.events) { + if (spec.events?.length) { lines.push(displayEvents(spec)); } const apis = impliedApis(spec); @@ -108,12 +110,11 @@ export async function displayExtensionVersionInfo( } export function displayEvents(spec: ExtensionSpec) { - const lines = spec.events?.map((event: EventDescriptor) => { - return ` - ${clc.magenta(event.type)}${event.description ? `: ${event.description}` : ""}`; - }); - return ( - clc.bold("Events emitted by this extension:\n") + (lines?.length ? lines.join("\n") : " - None") - ); + const lines = + spec.events?.map((event: EventDescriptor) => { + return ` - ${clc.magenta(event.type)}${event.description ? `: ${event.description}` : ""}`; + }) ?? []; + return clc.bold("Events emitted by this extension:\n") + lines.join("\n"); } export function displayResources(spec: ExtensionSpec) { @@ -122,6 +123,22 @@ export function displayResources(spec: ExtensionSpec) { resource.description ? `: ${resource.description}` : "" }`; }); + lines.push( + ...new Set( + spec.lifecycleEvents?.map((event: LifecycleEvent) => { + return ` - ${clc.blue(`${event.taskQueueTriggerFunction} (Cloud Task queue)`)}`; + }) + ) + ); + lines.push( + ...spec.params + .filter((param: Param) => { + return param.type === "SECRET"; + }) + .map((param: Param) => { + return ` - ${clc.blue(`${param.param} (Cloud Secret Manager secret)`)}`; + }) + ); return ( clc.bold("Resources created by this extension:\n") + (lines.length ? lines.join("\n") : " - None") diff --git a/src/extensions/types.ts b/src/extensions/types.ts index a9c06cfe2f4..a2c29ce3137 100644 --- a/src/extensions/types.ts +++ b/src/extensions/types.ts @@ -122,6 +122,12 @@ export interface ExtensionSpec { readmeContent?: string; externalServices?: ExternalService[]; events?: EventDescriptor[]; + lifecycleEvents?: LifecycleEvent[]; +} + +export interface LifecycleEvent { + stage: "STAGE_UNSPECIFIED" | "ON_INSTALL" | "ON_UPDATE" | "ON_CONFIGURE"; + taskQueueTriggerFunction: string; } export interface EventDescriptor { From 79d21d180bd9e45bf24addb845ad95a580bfb0d2 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Sat, 17 Jun 2023 02:25:19 -0700 Subject: [PATCH 15/24] Added displaying of external services. --- src/extensions/displayExtensionInfo.ts | 35 ++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index c94ee0d9109..e3fed61d269 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -11,6 +11,7 @@ import { ExtensionSpec, ExtensionVersion, LifecycleEvent, + ExternalService, Role, Param, Resource, @@ -97,6 +98,9 @@ export async function displayExtensionVersionInfo( if (spec.events?.length) { lines.push(displayEvents(spec)); } + if (spec.externalServices?.length) { + lines.push(displayExternalServices(spec)); + } const apis = impliedApis(spec); if (apis.length) { lines.push(displayApis(apis)); @@ -109,17 +113,35 @@ export async function displayExtensionVersionInfo( return lines; } +export function displayExternalServices(spec: ExtensionSpec) { + const lines = + spec.externalServices?.map((service: ExternalService) => { + return ` - ${clc.cyan(`${service.name} (${service.pricingUri})`)}`; + }) ?? []; + return clc.bold("External services used:\n") + lines.join("\n"); +} + export function displayEvents(spec: ExtensionSpec) { const lines = spec.events?.map((event: EventDescriptor) => { return ` - ${clc.magenta(event.type)}${event.description ? `: ${event.description}` : ""}`; }) ?? []; - return clc.bold("Events emitted by this extension:\n") + lines.join("\n"); + return clc.bold("Events emitted:\n") + lines.join("\n"); } export function displayResources(spec: ExtensionSpec) { const lines = spec.resources.map((resource: Resource) => { - return ` - ${clc.blue(`${resource.name} (${resource.type})`)}${ + let type: string = resource.type; + switch (resource.type) { + case "firebaseextensions.v1beta.function": + type = "Cloud Function V1"; + break; + case "firebaseextensions.v1beta.v2function": + type = "Cloud Function V2"; + break; + default: + } + return ` - ${clc.blue(`${resource.name} (${type})`)}${ resource.description ? `: ${resource.description}` : "" }`; }); @@ -139,10 +161,7 @@ export function displayResources(spec: ExtensionSpec) { return ` - ${clc.blue(`${param.param} (Cloud Secret Manager secret)`)}`; }) ); - return ( - clc.bold("Resources created by this extension:\n") + - (lines.length ? lines.join("\n") : " - None") - ); + return clc.bold("Resources created:\n") + (lines.length ? lines.join("\n") : " - None"); } /** @@ -163,14 +182,14 @@ async function displayRoles(roles: Role[]): Promise { return retrieveRoleInfo(role.role); }) ); - return clc.bold("Roles granted to this extension:\n") + lines.join("\n"); + return clc.bold("Roles granted:\n") + lines.join("\n"); } function displayApis(apis: Api[]): string { const lines: string[] = apis.map((api: Api) => { return ` - ${clc.cyan(api.apiName!)}: ${api.reason}`; }); - return clc.bold("APIs used by this extension:\n") + lines.join("\n"); + return clc.bold("APIs used:\n") + lines.join("\n"); } function usesTasks(spec: ExtensionSpec): boolean { From f07fe5e06f00777d3114303498cc2fed805b83a6 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Tue, 20 Jun 2023 06:10:24 -0700 Subject: [PATCH 16/24] Fixed resolveVersion() + tests. --- src/commands/ext-install.ts | 4 ++- src/deploy/extensions/planner.ts | 12 +++------ src/test/deploy/extensions/planner.spec.ts | 31 +++++++++++----------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index 0a57ed48f3f..d11b65f2d68 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -77,7 +77,9 @@ export const command = new Command("ext:install [extensionRef]") } else { void track("Extension Install", "Install by Extension Ref", options.interactive ? 1 : 0); const extension = await extensionsApi.getExtension(extensionRef); - const extensionVersionRef = await resolveVersion(refs.parse(extensionRef), extension); + const ref = refs.parse(extensionRef); + ref.version = await resolveVersion(ref, extension); + const extensionVersionRef = refs.toExtensionVersionRef(ref); extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef); await displayExtensionVersionInfo( extensionVersion.spec, diff --git a/src/deploy/extensions/planner.ts b/src/deploy/extensions/planner.ts index 613522fcf1b..75c1778f2e9 100644 --- a/src/deploy/extensions/planner.ts +++ b/src/deploy/extensions/planner.ts @@ -216,8 +216,7 @@ export async function want(args: { export async function resolveVersion(ref: refs.Ref, extension?: Extension): Promise { const extensionRef = refs.toExtensionRef(ref); if (!ref.version && extension?.latestApprovedVersion) { - ref.version = extension.latestApprovedVersion; - return refs.toExtensionVersionRef(ref); + return extension.latestApprovedVersion; } if (ref.version === "latest-approved") { if (!extension?.latestApprovedVersion) { @@ -225,8 +224,7 @@ export async function resolveVersion(ref: refs.Ref, extension?: Extension): Prom `${extensionRef} has not been published to Extensions Hub (https://extensions.dev). To install it, you must specify the version you want to install.` ); } - ref.version = extension.latestApprovedVersion; - return refs.toExtensionVersionRef(ref); + return extension.latestApprovedVersion; } if (!ref.version || ref.version === "latest") { if (!extension?.latestVersion) { @@ -234,8 +232,7 @@ export async function resolveVersion(ref: refs.Ref, extension?: Extension): Prom `${extensionRef} has no stable non-deprecated versions. If you wish to install a prerelease version, you must specify the version you want to install.` ); } - ref.version = extension.latestVersion; - return refs.toExtensionVersionRef(ref); + return extension.latestVersion; } const versions = await extensionsApi.listExtensionVersions(extensionRef, undefined, true); if (versions.length === 0) { @@ -250,6 +247,5 @@ export async function resolveVersion(ref: refs.Ref, extension?: Extension): Prom `No version of ${extensionRef} matches requested version ${ref.version}` ); } - ref.version = maxSatisfying; - return refs.toExtensionVersionRef(ref); + return maxSatisfying; } diff --git a/src/test/deploy/extensions/planner.spec.ts b/src/test/deploy/extensions/planner.spec.ts index b4a6ee757a6..6c082b5bbbb 100644 --- a/src/test/deploy/extensions/planner.spec.ts +++ b/src/test/deploy/extensions/planner.spec.ts @@ -35,7 +35,6 @@ function extensionVersion(version?: string): any { describe("Extensions Deployment Planner", () => { describe("resolveSemver", () => { let listExtensionVersionsStub: sinon.SinonStub; - let getExtensionStub: sinon.SinonStub; before(() => { listExtensionVersionsStub = sinon.stub(extensionsApi, "listExtensionVersions").resolves([ extensionVersion("0.1.0"), @@ -43,14 +42,10 @@ describe("Extensions Deployment Planner", () => { extensionVersion("0.2.0"), extensionVersion(), // Explicitly test that this doesn't break on bad data ]); - getExtensionStub = sinon - .stub(extensionsApi, "getExtension") - .resolves(extension("0.2.0", "0.1.1")); }); after(() => { listExtensionVersionsStub.restore(); - getExtensionStub.restore(); }); const cases = [ @@ -94,19 +89,25 @@ describe("Extensions Deployment Planner", () => { it(c.description, () => { if (!c.err) { expect( - planner.resolveVersion({ - publisherId: "test", - extensionId: "test", - version: c.in, - }) + planner.resolveVersion( + { + publisherId: "test", + extensionId: "test", + version: c.in, + }, + extension("0.2.0", "0.1.1") + ) ).to.eventually.equal(c.out); } else { expect( - planner.resolveVersion({ - publisherId: "test", - extensionId: "test", - version: c.in, - }) + planner.resolveVersion( + { + publisherId: "test", + extensionId: "test", + version: c.in, + }, + extension("0.2.0", "0.1.1") + ) ).to.eventually.be.rejected; } }); From 6fb7722d04e48fc37f62d4711160ebdeb36cf3c9 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Tue, 20 Jun 2023 10:52:27 -0700 Subject: [PATCH 17/24] Added tests for displayExtensionInfo(). --- .../extensions/displayExtensionInfo.spec.ts | 180 ++++++++---------- 1 file changed, 82 insertions(+), 98 deletions(-) diff --git a/src/test/extensions/displayExtensionInfo.spec.ts b/src/test/extensions/displayExtensionInfo.spec.ts index 8057b149dba..83e29d1a12a 100644 --- a/src/test/extensions/displayExtensionInfo.spec.ts +++ b/src/test/extensions/displayExtensionInfo.spec.ts @@ -3,18 +3,18 @@ import { expect } from "chai"; import * as iam from "../../gcp/iam"; import * as displayExtensionInfo from "../../extensions/displayExtensionInfo"; -import { ExtensionSpec, Param, Resource } from "../../extensions/types"; +import { ExtensionSpec, ExtensionVersion, Resource } from "../../extensions/types"; import { ParamType } from "../../extensions/types"; const SPEC: ExtensionSpec = { name: "test", - displayName: "Old", - description: "descriptive", - version: "0.1.0", + displayName: "My Extension", + description: "My extension's description", + version: "1.0.0", license: "MIT", apis: [ - { apiName: "api1", reason: "" }, - { apiName: "api2", reason: "" }, + { apiName: "api1.googleapis.com", reason: "" }, + { apiName: "api2.googleapis.com", reason: "" }, ], roles: [ { role: "role1", reason: "" }, @@ -23,29 +23,53 @@ const SPEC: ExtensionSpec = { resources: [ { name: "resource1", type: "firebaseextensions.v1beta.function", description: "desc" }, { name: "resource2", type: "other", description: "" } as unknown as Resource, + { + name: "taskResource", + type: "firebaseextensions.v1beta.function", + properties: { + taskQueueTrigger: {}, + }, + }, ], author: { authorName: "Tester", url: "firebase.google.com" }, contributors: [{ authorName: "Tester 2" }], billingRequired: true, sourceUrl: "test.com", - params: [], + params: [ + { + param: "secret", + label: "Secret", + type: ParamType.SECRET, + }, + ], systemParams: [], + events: [ + { + type: "abc.def.my-event", + description: "desc", + }, + ], + lifecycleEvents: [ + { + stage: "ON_INSTALL", + taskQueueTriggerFunction: "taskResource", + }, + ], }; -const TASK_FUNCTION_RESOURCE: Resource = { - name: "taskResource", - type: "firebaseextensions.v1beta.function", - properties: { - taskQueueTrigger: {}, +const EXT_VERSION: ExtensionVersion = { + name: "publishers/pub/extensions/my-ext/versions/1.0.0", + ref: "pub/my-ext@1.0.0", + state: "PUBLISHED", + spec: SPEC, + hash: "abc123", + sourceDownloadUri: "https://google.com", + buildSourceUri: "https://github.com/pub/extensions/my-ext", + listing: { + state: "APPROVED", }, }; -const SECRET_PARAM: Param = { - param: "secret", - label: "Secret", - type: ParamType.SECRET, -}; - describe("displayExtensionInfo", () => { describe("displayExtInfo", () => { let getRoleStub: sinon.SinonStub; @@ -74,91 +98,51 @@ describe("displayExtensionInfo", () => { }); it("should display info during install", async () => { - const loggedLines = await displayExtensionInfo.displayExtInfo(SPEC.name, "", SPEC); - const expected: string[] = [ - "**Name**: Old", - "**Description**: descriptive", - "**APIs used by this Extension**:\n api1 ()\n api2 ()", - "\u001b[1m**Roles granted to this Extension**:\n\u001b[22m Role 1 (a role)\n Role 2 (a role)", - ]; - expect(loggedLines.length).to.eql(expected.length); - expect(loggedLines[0]).to.include("Old"); - expect(loggedLines[1]).to.include("descriptive"); - expect(loggedLines[2]).to.include("api1"); - expect(loggedLines[2]).to.include("api2"); - expect(loggedLines[3]).to.include("Role 1"); - expect(loggedLines[3]).to.include("Role 2"); + const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo(SPEC); + expect(loggedLines[0]).to.include(SPEC.displayName); + expect(loggedLines[1]).to.include(SPEC.description); + expect(loggedLines[2]).to.include(SPEC.version); + expect(loggedLines[3]).to.include(SPEC.license); + expect(loggedLines[4]).to.include("resource1 (Cloud Function V1)"); + expect(loggedLines[4]).to.include("resource2 (other)"); + expect(loggedLines[4]).to.include("taskResource (Cloud Function V1)"); + expect(loggedLines[4]).to.include("taskResource (Cloud Task queue)"); + expect(loggedLines[4]).to.include("secret (Cloud Secret Manager secret)"); + expect(loggedLines[5]).to.include("abc.def.my-event"); + expect(loggedLines[6]).to.include("api1.googleapis.com"); + expect(loggedLines[6]).to.include("api1.googleapis.com"); + expect(loggedLines[6]).to.include("cloudtasks.googleapis.com"); + expect(loggedLines[7]).to.include("Role 1"); + expect(loggedLines[7]).to.include("Role 2"); + expect(loggedLines[7]).to.include("Cloud Task Enqueuer"); }); it("should display additional information for a published extension", async () => { - const loggedLines = await displayExtensionInfo.displayExtInfo( - SPEC.name, - "testpublisher", + const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo( SPEC, - true + EXT_VERSION, + "1.0.0", + "1.0.0" ); - const expected: string[] = [ - "**Name**: Old", - "**Publisher**: testpublisher", - "**Description**: descriptive", - "**License**: MIT", - "**Source code**: test.com", - "**APIs used by this Extension**:\n api1 ()\n api2 ()", - "\u001b[1m**Roles granted to this Extension**:\n\u001b[22m Role 1 (a role)\n Role 2 (a role)", - ]; - expect(loggedLines.length).to.eql(expected.length); - expect(loggedLines[0]).to.include("Old"); - expect(loggedLines[1]).to.include("testpublisher"); - expect(loggedLines[2]).to.include("descriptive"); - expect(loggedLines[3]).to.include("MIT"); - expect(loggedLines[4]).to.include("test.com"); - expect(loggedLines[5]).to.include("api1"); - expect(loggedLines[5]).to.include("api2"); - expect(loggedLines[6]).to.include("Role 1"); - expect(loggedLines[6]).to.include("Role 2"); - }); - - it("should display role and api for Cloud Tasks during install", async () => { - const specWithTasks = JSON.parse(JSON.stringify(SPEC)) as ExtensionSpec; - specWithTasks.resources.push(TASK_FUNCTION_RESOURCE); - - const loggedLines = await displayExtensionInfo.displayExtInfo(SPEC.name, "", specWithTasks); - const expected: string[] = [ - "**Name**: Old", - "**Description**: descriptive", - "**APIs used by this Extension**:\n api1 ()\n api2 ()", - "\u001b[1m**Roles granted to this Extension**:\n\u001b[22m Role 1 (a role)\n Role 2 (a role)\n Cloud Task Enqueuer (Enqueue tasks)", - ]; - expect(loggedLines.length).to.eql(expected.length); - expect(loggedLines[0]).to.include("Old"); - expect(loggedLines[1]).to.include("descriptive"); - expect(loggedLines[2]).to.include("api1"); - expect(loggedLines[2]).to.include("api2"); - expect(loggedLines[2]).to.include("Cloud Tasks"); - expect(loggedLines[3]).to.include("Role 1"); - expect(loggedLines[3]).to.include("Role 2"); - expect(loggedLines[3]).to.include("Cloud Task Enqueuer"); - }); - - it("should display role for Cloud Secret Manager during install", async () => { - const specWithSecret = JSON.parse(JSON.stringify(SPEC)) as ExtensionSpec; - specWithSecret.params.push(SECRET_PARAM); - - const loggedLines = await displayExtensionInfo.displayExtInfo(SPEC.name, "", specWithSecret); - const expected: string[] = [ - "**Name**: Old", - "**Description**: descriptive", - "**APIs used by this Extension**:\n api1 ()\n api2 ()", - "\u001b[1m**Roles granted to this Extension**:\n\u001b[22m Role 1 (a role)\n Role 2 (a role)\n Secret Accessor (Access secrets)", - ]; - expect(loggedLines.length).to.eql(expected.length); - expect(loggedLines[0]).to.include("Old"); - expect(loggedLines[1]).to.include("descriptive"); - expect(loggedLines[2]).to.include("api1"); - expect(loggedLines[2]).to.include("api2"); - expect(loggedLines[3]).to.include("Role 1"); - expect(loggedLines[3]).to.include("Role 2"); - expect(loggedLines[3]).to.include("Secret Accessor"); + expect(loggedLines[0]).to.include(SPEC.displayName); + expect(loggedLines[1]).to.include(SPEC.description); + expect(loggedLines[2]).to.include(SPEC.version); + expect(loggedLines[3]).to.include("Accepted"); + expect(loggedLines[4]).to.include("View in Extensions Hub"); + expect(loggedLines[5]).to.include(EXT_VERSION.buildSourceUri); + expect(loggedLines[6]).to.include(SPEC.license); + expect(loggedLines[7]).to.include("resource1 (Cloud Function V1)"); + expect(loggedLines[7]).to.include("resource2 (other)"); + expect(loggedLines[7]).to.include("taskResource (Cloud Function V1)"); + expect(loggedLines[7]).to.include("taskResource (Cloud Task queue)"); + expect(loggedLines[7]).to.include("secret (Cloud Secret Manager secret)"); + expect(loggedLines[8]).to.include("abc.def.my-event"); + expect(loggedLines[9]).to.include("api1.googleapis.com"); + expect(loggedLines[9]).to.include("api1.googleapis.com"); + expect(loggedLines[9]).to.include("cloudtasks.googleapis.com"); + expect(loggedLines[10]).to.include("Role 1"); + expect(loggedLines[10]).to.include("Role 2"); + expect(loggedLines[10]).to.include("Cloud Task Enqueuer"); }); }); }); From 66edd997c7ff2d6281ccd70b941f9058bcc673a0 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Wed, 21 Jun 2023 07:34:15 -0700 Subject: [PATCH 18/24] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..4c26771081b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Refactored ext:install to use the latest extension metadata. (#5997) \ No newline at end of file From 1e3d2e281bf4a93d7519cb5fbbc1392d233bc4ea Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Wed, 21 Jun 2023 07:38:19 -0700 Subject: [PATCH 19/24] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c26771081b..8a1b5a81e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- Refactored ext:install to use the latest extension metadata. (#5997) \ No newline at end of file +- Refactored ext:install to use the latest extension metadata. (#5997) From 286685f13075a359fe97f9ea3cc7b95dfb04c1eb Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Tue, 27 Jun 2023 14:06:14 -0700 Subject: [PATCH 20/24] Update CHANGELOG.md Co-authored-by: joehan --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a1b5a81e64..2668eb75f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- Refactored ext:install to use the latest extension metadata. (#5997) +- Refactored `ext:install` to use the latest extension metadata. (#5997) From 6d0c1a95fd7b025917629ae4117f771d86c99970 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Tue, 27 Jun 2023 14:20:00 -0700 Subject: [PATCH 21/24] Better messaging and parameterizing. --- src/commands/ext-install.ts | 16 +++++++++------- src/extensions/displayExtensionInfo.ts | 14 +++++++------- src/extensions/updateHelper.ts | 4 ++-- src/test/extensions/displayExtensionInfo.spec.ts | 14 +++++++------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/commands/ext-install.ts b/src/commands/ext-install.ts index d11b65f2d68..0c8dc3b95e2 100644 --- a/src/commands/ext-install.ts +++ b/src/commands/ext-install.ts @@ -72,7 +72,7 @@ export const command = new Command("ext:install [extensionRef]") // TODO(b/228444119): Create source should happen at deploy time. // Should parse spec locally so we don't need project ID. source = await createSourceFromLocation(needProjectId({ projectId }), extensionRef); - await displayExtensionVersionInfo(source.spec); + await displayExtensionVersionInfo({ spec: source.spec }); void track("Extension Install", "Install by Source", options.interactive ? 1 : 0); } else { void track("Extension Install", "Install by Extension Ref", options.interactive ? 1 : 0); @@ -81,12 +81,12 @@ export const command = new Command("ext:install [extensionRef]") ref.version = await resolveVersion(ref, extension); const extensionVersionRef = refs.toExtensionVersionRef(ref); extensionVersion = await extensionsApi.getExtensionVersion(extensionVersionRef); - await displayExtensionVersionInfo( - extensionVersion.spec, + await displayExtensionVersionInfo({ + spec: extensionVersion.spec, extensionVersion, - extension.latestApprovedVersion, - extension.latestVersion - ); + latestApprovedVersion: extension.latestApprovedVersion, + latestVersion: extension.latestVersion, + }); if (extensionVersion.state === "DEPRECATED") { throw new FirebaseError( `Extension version ${clc.bold( @@ -115,7 +115,9 @@ export const command = new Command("ext:install [extensionRef]") } if (!source && !extensionVersion) { throw new FirebaseError( - "Could not find a source. Please specify a valid source to continue." + `Failed to parse ${clc.bold( + extensionRef + )} as an extension version or a path to a local extension. Please specify a valid reference.` ); } if ( diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index e3fed61d269..a5794a6b828 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -34,12 +34,13 @@ const TASKS_API = "cloudtasks.googleapis.com"; * @param spec the extension spec * @param extensionVersion the extension version * */ -export async function displayExtensionVersionInfo( - spec: ExtensionSpec, - extensionVersion?: ExtensionVersion, +export async function displayExtensionVersionInfo(args: { + spec: ExtensionSpec; + extensionVersion?: ExtensionVersion; latestApprovedVersion?: string, latestVersion?: string -): Promise { +}): Promise { + const { spec, extensionVersion, latestApprovedVersion, latestVersion } = args; const lines: string[] = []; const extensionRef = extensionVersion ? refs.toExtensionRef(refs.parse(extensionVersion?.ref)) @@ -72,7 +73,6 @@ export async function displayExtensionVersionInfo( break; default: reviewStatus = clc.bold(clc.yellow("Unreviewed")); - break; } lines.push(`${clc.bold("Review status:")} ${reviewStatus}`); if (latestApprovedVersion) { @@ -134,10 +134,10 @@ export function displayResources(spec: ExtensionSpec) { let type: string = resource.type; switch (resource.type) { case "firebaseextensions.v1beta.function": - type = "Cloud Function V1"; + type = "Cloud Function (1st gen)"; break; case "firebaseextensions.v1beta.v2function": - type = "Cloud Function V2"; + type = "Cloud Function (2nd gen)"; break; default: } diff --git a/src/extensions/updateHelper.ts b/src/extensions/updateHelper.ts index a87a20e4900..699107bdf3d 100644 --- a/src/extensions/updateHelper.ts +++ b/src/extensions/updateHelper.ts @@ -157,7 +157,7 @@ export async function updateFromLocalSource( localSource: string, existingSpec: ExtensionSpec ): Promise { - await displayExtensionVersionInfo(existingSpec); + await displayExtensionVersionInfo({ spec: existingSpec }); let source; try { source = await createSourceFromLocation(projectId, localSource); @@ -187,7 +187,7 @@ export async function updateFromUrlSource( urlSource: string, existingSpec: ExtensionSpec ): Promise { - await displayExtensionVersionInfo(existingSpec); + await displayExtensionVersionInfo({ spec: existingSpec }); let source; try { source = await createSourceFromLocation(projectId, urlSource); diff --git a/src/test/extensions/displayExtensionInfo.spec.ts b/src/test/extensions/displayExtensionInfo.spec.ts index 83e29d1a12a..f9f0a696bd3 100644 --- a/src/test/extensions/displayExtensionInfo.spec.ts +++ b/src/test/extensions/displayExtensionInfo.spec.ts @@ -98,7 +98,7 @@ describe("displayExtensionInfo", () => { }); it("should display info during install", async () => { - const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo(SPEC); + const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo({ spec: SPEC }); expect(loggedLines[0]).to.include(SPEC.displayName); expect(loggedLines[1]).to.include(SPEC.description); expect(loggedLines[2]).to.include(SPEC.version); @@ -118,12 +118,12 @@ describe("displayExtensionInfo", () => { }); it("should display additional information for a published extension", async () => { - const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo( - SPEC, - EXT_VERSION, - "1.0.0", - "1.0.0" - ); + const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo({ + spec: SPEC, + extensionVersion: EXT_VERSION, + latestApprovedVersion: "1.0.0", + latestVersion: "1.0.0", + }); expect(loggedLines[0]).to.include(SPEC.displayName); expect(loggedLines[1]).to.include(SPEC.description); expect(loggedLines[2]).to.include(SPEC.version); From 01c68e333022a862b2745db18bfe4fde7e66f6a3 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Tue, 27 Jun 2023 14:29:59 -0700 Subject: [PATCH 22/24] Update displayExtensionInfo.ts --- src/extensions/displayExtensionInfo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index a5794a6b828..fc23c344f7b 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -37,8 +37,8 @@ const TASKS_API = "cloudtasks.googleapis.com"; export async function displayExtensionVersionInfo(args: { spec: ExtensionSpec; extensionVersion?: ExtensionVersion; - latestApprovedVersion?: string, - latestVersion?: string + latestApprovedVersion?: string; + latestVersion?: string; }): Promise { const { spec, extensionVersion, latestApprovedVersion, latestVersion } = args; const lines: string[] = []; From dad2289de9867c4694e3f2d43501547ee075bfd8 Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Tue, 27 Jun 2023 14:36:37 -0700 Subject: [PATCH 23/24] Update displayExtensionInfo.spec.ts --- src/test/extensions/displayExtensionInfo.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/extensions/displayExtensionInfo.spec.ts b/src/test/extensions/displayExtensionInfo.spec.ts index f9f0a696bd3..7aa8dac1b10 100644 --- a/src/test/extensions/displayExtensionInfo.spec.ts +++ b/src/test/extensions/displayExtensionInfo.spec.ts @@ -103,9 +103,9 @@ describe("displayExtensionInfo", () => { expect(loggedLines[1]).to.include(SPEC.description); expect(loggedLines[2]).to.include(SPEC.version); expect(loggedLines[3]).to.include(SPEC.license); - expect(loggedLines[4]).to.include("resource1 (Cloud Function V1)"); + expect(loggedLines[4]).to.include("resource1 (Cloud Function (1st gen))"); expect(loggedLines[4]).to.include("resource2 (other)"); - expect(loggedLines[4]).to.include("taskResource (Cloud Function V1)"); + expect(loggedLines[4]).to.include("taskResource (Cloud Function (1st gen))"); expect(loggedLines[4]).to.include("taskResource (Cloud Task queue)"); expect(loggedLines[4]).to.include("secret (Cloud Secret Manager secret)"); expect(loggedLines[5]).to.include("abc.def.my-event"); @@ -131,9 +131,9 @@ describe("displayExtensionInfo", () => { expect(loggedLines[4]).to.include("View in Extensions Hub"); expect(loggedLines[5]).to.include(EXT_VERSION.buildSourceUri); expect(loggedLines[6]).to.include(SPEC.license); - expect(loggedLines[7]).to.include("resource1 (Cloud Function V1)"); + expect(loggedLines[7]).to.include("resource1 (Cloud Function (1st gen))"); expect(loggedLines[7]).to.include("resource2 (other)"); - expect(loggedLines[7]).to.include("taskResource (Cloud Function V1)"); + expect(loggedLines[7]).to.include("taskResource (Cloud Function (1st gen))"); expect(loggedLines[7]).to.include("taskResource (Cloud Task queue)"); expect(loggedLines[7]).to.include("secret (Cloud Secret Manager secret)"); expect(loggedLines[8]).to.include("abc.def.my-event"); From 376e5951208333e12e5af7e875a40ac3ff83494f Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Tue, 27 Jun 2023 14:46:55 -0700 Subject: [PATCH 24/24] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0d08aa69e..2668eb75f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- Refactored `ext:install` to use the latest extension metadata. (#5997) \ No newline at end of file +- Refactored `ext:install` to use the latest extension metadata. (#5997)