From 2b470ecdb1942756211c3d7f201eb0d16e3e940c Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Tue, 3 Dec 2024 09:56:48 -0800 Subject: [PATCH 1/6] Adding better error messaging when using non Firebase projects --- CHANGELOG.md | 1 + src/command.ts | 26 ++++++++++++--- src/commands/apphosting-backends-create.ts | 1 + src/commands/apphosting-backends-delete.ts | 1 + src/commands/apphosting-backends-get.ts | 1 + src/commands/apphosting-backends-list.ts | 1 + src/commands/apphosting-builds-create.ts | 1 + src/commands/apphosting-builds-get.ts | 1 + src/commands/apphosting-config-export.ts | 1 + src/commands/apphosting-repos-create.ts | 1 + src/commands/apphosting-rollouts-create.ts | 1 + src/commands/apphosting-rollouts-list.ts | 1 + src/commands/apphosting-secrets-access.ts | 1 + src/commands/apphosting-secrets-describe.ts | 1 + .../apphosting-secrets-grantaccess.ts | 1 + src/commands/apphosting-secrets-set.ts | 1 + src/commands/apps-android-sha-create.ts | 2 +- src/commands/dataconnect-sdk-generate.ts | 1 + src/commands/dataconnect-services-list.ts | 1 + src/commands/dataconnect-sql-diff.ts | 1 + src/commands/dataconnect-sql-grant.ts | 1 + src/commands/dataconnect-sql-migrate.ts | 1 + src/commands/dataconnect-sql-shell.ts | 1 + src/commands/deploy.ts | 33 ++++++++++++++++--- src/commands/emulators-exec.ts | 1 + src/commands/emulators-export.ts | 1 + src/commands/emulators-start.ts | 1 + src/commands/experimental-functions-shell.ts | 1 + src/commands/experiments-describe.ts | 1 + src/commands/experiments-disable.ts | 1 + src/commands/experiments-enable.ts | 1 + src/commands/experiments-list.ts | 1 + src/commands/firestore-backups-delete.ts | 1 + src/commands/firestore-backups-get.ts | 1 + src/commands/firestore-backups-list.ts | 1 + .../firestore-backups-schedules-create.ts | 1 + .../firestore-backups-schedules-delete.ts | 1 + .../firestore-backups-schedules-list.ts | 1 + .../firestore-backups-schedules-update.ts | 1 + src/commands/firestore-databases-create.ts | 1 + src/commands/firestore-databases-delete.ts | 1 + src/commands/firestore-databases-get.ts | 1 + src/commands/firestore-databases-list.ts | 1 + src/commands/firestore-databases-restore.ts | 1 + src/commands/firestore-databases-update.ts | 1 + src/commands/firestore-delete.ts | 1 + src/commands/firestore-indexes-list.ts | 1 + src/commands/firestore-locations.ts | 1 + src/commands/functions-config-export.ts | 1 + src/commands/functions-shell.ts | 1 + src/commands/help.ts | 1 + src/commands/init.ts | 1 + src/commands/login-add.ts | 1 + src/commands/login-ci.ts | 1 + src/commands/login-list.ts | 1 + src/commands/login-use.ts | 1 + src/commands/login.ts | 1 + src/commands/logout.ts | 1 + src/commands/open.ts | 1 + src/commands/projects-addfirebase.ts | 1 + src/commands/projects-create.ts | 1 + src/commands/projects-list.ts | 1 + src/commands/serve.ts | 1 + src/commands/setup-emulators-database.ts | 1 + src/commands/setup-emulators-dataconnect.ts | 1 + src/commands/setup-emulators-firestore.ts | 1 + src/commands/setup-emulators-pubsub.ts | 1 + src/commands/setup-emulators-storage.ts | 1 + src/commands/setup-emulators-ui.ts | 1 + src/commands/target-apply.ts | 1 + src/commands/target-clear.ts | 1 + src/commands/target-remove.ts | 1 + src/commands/target.ts | 1 + src/commands/use.ts | 1 + src/filterTargets.ts | 6 ++-- src/management/projects.ts | 21 ++++++++++++ src/options.ts | 12 ++++++- src/utils.ts | 9 +++++ 78 files changed, 166 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e2248731e4..d6b7f8aa075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Added default value for `emulators.dataconnect.dataDir` to `init dataconnect`. - Fixed an issue where `firebase` would error out instead of displaying help text. +- Improved error messaging when using a project that does not have Firebase enabled. diff --git a/src/command.ts b/src/command.ts index 68b045e5f3e..20390942c58 100644 --- a/src/command.ts +++ b/src/command.ts @@ -10,7 +10,11 @@ import { configstore } from "./configstore"; import { detectProjectRoot } from "./detectProjectRoot"; import { trackEmulator, trackGA4 } from "./track"; import { selectAccount, setActiveAccount } from "./auth"; -import { getProject } from "./management/projects"; +import { + getProject, + isFirebaseProject, + printMigrateToFirebaseMessage, +} from "./management/projects"; import { requireAuth } from "./requireAuth"; import { Options } from "./options"; @@ -38,6 +42,7 @@ export class Command { // eslint-disable-next-line @typescript-eslint/no-explicit-any private options: any[][] = []; private aliases: string[] = []; + private firebaseRequired = true; private actionFn: ActionFunction = (): void => { // noop by default, unless overwritten by `.action(fn)`. }; @@ -99,6 +104,15 @@ export class Command { return this; } + /** + * This command is safe to run on Cloud projects that do not have Firebase enabled. + * @returns + */ + firebaseNotRequired(): Command { + this.firebaseRequired = false; + return this; + } + /** * Attaches a function to run before the command's action function. * @param fn the function to run. @@ -291,7 +305,7 @@ export class Command { * @param options the command options object. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async prepare(options: any): Promise { + public async prepare(options: any): Promise { options = options || {}; options.project = getInheritedOption(options, "project"); @@ -342,6 +356,7 @@ export class Command { await this.resolveProjectIdentifiers(options); validateProjectId(options.projectId); } + return options as Options; } /** @@ -415,12 +430,15 @@ export class Command { args.splice(args.length - 1, 0, ""); } - const options = last(args); - await this.prepare(options); + const options = await this.prepare(last(args)); for (const before of this.befores) { await before.fn(options, ...before.args); } + if (this.firebaseRequired && options.project && !(await isFirebaseProject(options.project))) { + printMigrateToFirebaseMessage(options.project); + throw new FirebaseError("This command requires your project to be migrated to Firebase."); + } return this.actionFn(...args); }; } diff --git a/src/commands/apphosting-backends-create.ts b/src/commands/apphosting-backends-create.ts index 440870fe853..2f364434d2b 100644 --- a/src/commands/apphosting-backends-create.ts +++ b/src/commands/apphosting-backends-create.ts @@ -9,6 +9,7 @@ import { requireTosAcceptance } from "../requireTosAcceptance"; export const command = new Command("apphosting:backends:create") .description("create a Firebase App Hosting backend") + .firebaseNotRequired() .option( "-a, --app ", "specify an existing Firebase web app's ID to associate your App Hosting backend with", diff --git a/src/commands/apphosting-backends-delete.ts b/src/commands/apphosting-backends-delete.ts index d6caed07346..06015741a84 100644 --- a/src/commands/apphosting-backends-delete.ts +++ b/src/commands/apphosting-backends-delete.ts @@ -15,6 +15,7 @@ import * as ora from "ora"; export const command = new Command("apphosting:backends:delete ") .description("delete a Firebase App Hosting backend") + .firebaseNotRequired() .option("-l, --location ", "specify the location of the backend", "-") .withForce() .before(apphosting.ensureApiEnabled) diff --git a/src/commands/apphosting-backends-get.ts b/src/commands/apphosting-backends-get.ts index c4f5f1dd083..a21d132f311 100644 --- a/src/commands/apphosting-backends-get.ts +++ b/src/commands/apphosting-backends-get.ts @@ -8,6 +8,7 @@ import { printBackendsTable } from "./apphosting-backends-list"; export const command = new Command("apphosting:backends:get ") .description("print info about a Firebase App Hosting backend") + .firebaseNotRequired() .option("-l, --location ", "backend location", "-") .before(apphosting.ensureApiEnabled) .action(async (backend: string, options: Options) => { diff --git a/src/commands/apphosting-backends-list.ts b/src/commands/apphosting-backends-list.ts index 0b583e2d602..d7caaa1d63d 100644 --- a/src/commands/apphosting-backends-list.ts +++ b/src/commands/apphosting-backends-list.ts @@ -11,6 +11,7 @@ const TABLE_HEAD = ["Backend", "Repository", "URL", "Location", "Updated Date"]; export const command = new Command("apphosting:backends:list") .description("list Firebase App Hosting backends") + .firebaseNotRequired() .option("-l, --location ", "list backends in the specified location", "-") .before(apphosting.ensureApiEnabled) .action(async (options: Options) => { diff --git a/src/commands/apphosting-builds-create.ts b/src/commands/apphosting-builds-create.ts index 2050917c92f..084af5ba707 100644 --- a/src/commands/apphosting-builds-create.ts +++ b/src/commands/apphosting-builds-create.ts @@ -6,6 +6,7 @@ import { needProjectId } from "../projectUtils"; export const command = new Command("apphosting:builds:create ") .description("create a build for an App Hosting backend") + .firebaseNotRequired() .option("-l, --location ", "specify the region of the backend", "us-central1") .option("-i, --id ", "id of the build (defaults to autogenerating a random id)", "") .option("-b, --branch ", "repository branch to deploy (defaults to 'main')", "main") diff --git a/src/commands/apphosting-builds-get.ts b/src/commands/apphosting-builds-get.ts index 1889b50029c..6a050cde33b 100644 --- a/src/commands/apphosting-builds-get.ts +++ b/src/commands/apphosting-builds-get.ts @@ -6,6 +6,7 @@ import { needProjectId } from "../projectUtils"; export const command = new Command("apphosting:builds:get ") .description("get a build for an App Hosting backend") + .firebaseNotRequired() .option("-l, --location ", "specify the region of the backend", "us-central1") .before(apphosting.ensureApiEnabled) .action(async (backendId: string, buildId: string, options: Options) => { diff --git a/src/commands/apphosting-config-export.ts b/src/commands/apphosting-config-export.ts index 7cb3351c5d4..18c54b7f469 100644 --- a/src/commands/apphosting-config-export.ts +++ b/src/commands/apphosting-config-export.ts @@ -12,6 +12,7 @@ export const command = new Command("apphosting:config:export") .description( "Export App Hosting configurations such as secrets into an apphosting.local.yaml file", ) + .firebaseNotRequired() .option( "-s, --secrets .yaml file to export secrets from>", "This command combines the base apphosting.yaml with the specified environment-specific file (e.g., apphosting.staging.yaml). If keys conflict, the environment-specific file takes precedence.", diff --git a/src/commands/apphosting-repos-create.ts b/src/commands/apphosting-repos-create.ts index e361a35b8bc..7f4f80c7b0b 100644 --- a/src/commands/apphosting-repos-create.ts +++ b/src/commands/apphosting-repos-create.ts @@ -9,6 +9,7 @@ import { requireTosAcceptance } from "../requireTosAcceptance"; export const command = new Command("apphosting:repos:create") .description("create a Firebase App Hosting Developer Connect Git Repository Link") + .firebaseNotRequired() .option("-l, --location ", "specify the location of the backend", "") .option("-g, --gitconnection ", "id of the connection", "") .before(ensureApiEnabled) diff --git a/src/commands/apphosting-rollouts-create.ts b/src/commands/apphosting-rollouts-create.ts index 5b827aa0f2b..8a97408f81d 100644 --- a/src/commands/apphosting-rollouts-create.ts +++ b/src/commands/apphosting-rollouts-create.ts @@ -7,6 +7,7 @@ import { createRollout } from "../apphosting/rollout"; export const command = new Command("apphosting:rollouts:create ") .description("create a rollout using a build for an App Hosting backend") + .firebaseNotRequired() .option("-l, --location ", "specify the region of the backend", "-") .option( "-b, --git-branch ", diff --git a/src/commands/apphosting-rollouts-list.ts b/src/commands/apphosting-rollouts-list.ts index 5e3ac8c5350..2aa8369c9f1 100644 --- a/src/commands/apphosting-rollouts-list.ts +++ b/src/commands/apphosting-rollouts-list.ts @@ -6,6 +6,7 @@ import { needProjectId } from "../projectUtils"; export const command = new Command("apphosting:rollouts:list ") .description("list rollouts of an App Hosting backend") + .firebaseNotRequired() .option( "-l, --location ", "region of the rollouts (defaults to listing rollouts from all regions)", diff --git a/src/commands/apphosting-secrets-access.ts b/src/commands/apphosting-secrets-access.ts index ecd5d539e7e..f9fd4325655 100644 --- a/src/commands/apphosting-secrets-access.ts +++ b/src/commands/apphosting-secrets-access.ts @@ -11,6 +11,7 @@ export const command = new Command("apphosting:secrets:access ") .description("Get metadata for secret and its versions.") + .firebaseNotRequired() .before(requireAuth) .before(secretManager.ensureApi) .before(requirePermissions, ["secretmanager.secrets.get"]) diff --git a/src/commands/apphosting-secrets-grantaccess.ts b/src/commands/apphosting-secrets-grantaccess.ts index 8d6c7da4134..5f5ff9a8e5e 100644 --- a/src/commands/apphosting-secrets-grantaccess.ts +++ b/src/commands/apphosting-secrets-grantaccess.ts @@ -11,6 +11,7 @@ import { getBackendForAmbiguousLocation } from "../apphosting/backend"; export const command = new Command("apphosting:secrets:grantaccess ") .description("grant service accounts permissions to the provided secret") + .firebaseNotRequired() .option("-l, --location ", "backend location", "-") .option("-b, --backend ", "backend name") .before(requireAuth) diff --git a/src/commands/apphosting-secrets-set.ts b/src/commands/apphosting-secrets-set.ts index 390181afbb6..372bbf40d88 100644 --- a/src/commands/apphosting-secrets-set.ts +++ b/src/commands/apphosting-secrets-set.ts @@ -14,6 +14,7 @@ import * as utils from "../utils"; export const command = new Command("apphosting:secrets:set ") .description("create or update a secret for use in Firebase App Hosting") + .firebaseNotRequired() .option("-l, --location ", "optional location to retrict secret replication") // TODO: What is the right --force behavior for granting access? Seems correct to grant permissions // if there is only one set of accounts, but should maybe fail if there are more than one set of diff --git a/src/commands/apps-android-sha-create.ts b/src/commands/apps-android-sha-create.ts index 5103374f4f3..ae73eae9952 100644 --- a/src/commands/apps-android-sha-create.ts +++ b/src/commands/apps-android-sha-create.ts @@ -29,7 +29,7 @@ export const command = new Command("apps:android:sha:create ") }), `Creating Android SHA certificate ${clc.bold( options.shaHash, - )}with Android app Id ${clc.bold(appId)}`, + )} with Android app Id ${clc.bold(appId)}`, ); return shaCertificate; diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index 8892471e185..9dba61ce68d 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -12,6 +12,7 @@ type GenerateOptions = Options & { watch?: boolean }; export const command = new Command("dataconnect:sdk:generate") .description("generates typed SDKs for your Data Connect connectors") + .firebaseNotRequired() .option( "--watch", "watch for changes to your connector GQL files and regenerate your SDKs when updates occur", diff --git a/src/commands/dataconnect-services-list.ts b/src/commands/dataconnect-services-list.ts index b0e5a3c2da4..7922176639d 100644 --- a/src/commands/dataconnect-services-list.ts +++ b/src/commands/dataconnect-services-list.ts @@ -10,6 +10,7 @@ const Table = require("cli-table"); export const command = new Command("dataconnect:services:list") .description("list all deployed services in your Firebase project") + .firebaseNotRequired() .before(requirePermissions, [ "dataconnect.services.list", "dataconnect.schemas.list", diff --git a/src/commands/dataconnect-sql-diff.ts b/src/commands/dataconnect-sql-diff.ts index cd8e68682f1..bf300add65a 100644 --- a/src/commands/dataconnect-sql-diff.ts +++ b/src/commands/dataconnect-sql-diff.ts @@ -11,6 +11,7 @@ export const command = new Command("dataconnect:sql:diff [serviceId]") .description( "displays the differences between a local DataConnect schema and your CloudSQL database's current schema", ) + .firebaseNotRequired() .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", diff --git a/src/commands/dataconnect-sql-grant.ts b/src/commands/dataconnect-sql-grant.ts index 95899de25b3..2a6b0c222ce 100644 --- a/src/commands/dataconnect-sql-grant.ts +++ b/src/commands/dataconnect-sql-grant.ts @@ -13,6 +13,7 @@ const allowedRoles = Object.keys(fdcSqlRoleMap); export const command = new Command("dataconnect:sql:grant [serviceId]") .description("Grants the SQL role to the provided user or service account .") + .firebaseNotRequired() .option("-R, --role ", "The SQL role to grant. One of: owner, writer, or reader.") .option( "-E, --email ", diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts index 5aa1f23558c..a1226d58841 100644 --- a/src/commands/dataconnect-sql-migrate.ts +++ b/src/commands/dataconnect-sql-migrate.ts @@ -11,6 +11,7 @@ import { logLabeledSuccess } from "../utils"; export const command = new Command("dataconnect:sql:migrate [serviceId]") .description("migrates your CloudSQL database's schema to match your local DataConnect schema") + .firebaseNotRequired() .before(requirePermissions, [ "firebasedataconnect.services.list", "firebasedataconnect.schemas.list", diff --git a/src/commands/dataconnect-sql-shell.ts b/src/commands/dataconnect-sql-shell.ts index 9055267d6a5..1c4cf0aee89 100644 --- a/src/commands/dataconnect-sql-shell.ts +++ b/src/commands/dataconnect-sql-shell.ts @@ -86,6 +86,7 @@ async function mainShellLoop(conn: pg.PoolClient) { export const command = new Command("dataconnect:sql:shell [serviceId]") .description("Starts a shell connected directly to your dataconnect cloudsql instance.") + .firebaseNotRequired() .before(requirePermissions, ["firebasedataconnect.services.list", "cloudsql.instances.connect"]) .before(requireAuth) .action(async (serviceId: string, options: Options) => { diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 2d750c9a470..69ebee2296c 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -12,6 +12,9 @@ import { FirebaseError } from "../error"; import { bold } from "colorette"; import { interactiveCreateHostingSite } from "../hosting/interactive"; import { logBullet } from "../utils"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { isFirebaseProject, printMigrateToFirebaseMessage } from "../management/projects"; // in order of least time-consuming to most time-consuming export const VALID_DEPLOY_TARGETS = [ @@ -74,6 +77,7 @@ export const TARGET_PERMISSIONS: Record<(typeof VALID_DEPLOY_TARGETS)[number], s export const command = new Command("deploy") .description("deploy code and assets to your Firebase project") + .firebaseNotRequired() .withForce( "delete Cloud Functions missing from the current working directory and bypass interactive prompts", ) @@ -97,19 +101,38 @@ export const command = new Command("deploy") "In order to provide better validation, this may still enable APIs on the target project.", ) .before(requireConfig) - .before((options) => { + .before((options: Options) => { options.filteredTargets = filterTargets(options, VALID_DEPLOY_TARGETS); const permissions = options.filteredTargets.reduce((perms: string[], target: string) => { return perms.concat(TARGET_PERMISSIONS[target]); }, []); return requirePermissions(options, permissions); }) - .before((options) => { + .before(async (options: Options) => { + if ( + options.filteredTargets.includes("database") || + options.filteredTargets.includes("extensions") || + options.filteredTargets.includes("functions") || + options.filteredTargets.includes("hosting") || + options.filteredTargets.includes("remoteconfig") || + options.filteredTargets.includes("storage") + ) { + const projectId = needProjectId(options); + if (!(await isFirebaseProject(projectId))) { + printMigrateToFirebaseMessage(projectId); + throw new FirebaseError( + "Some of the products in this deployment require this project to be migrated to Firebase.", + ); + } + } + }) + .before((options: Options) => { if (options.filteredTargets.includes("functions")) { - return checkServiceAccountIam(options.project); + const projectId = needProjectId(options); + return checkServiceAccountIam(projectId); } }) - .before(async (options) => { + .before(async (options: Options) => { // only fetch the default instance for hosting or database deploys if (options.filteredTargets.includes("database")) { await requireDatabaseInstance(options); @@ -145,6 +168,6 @@ export const command = new Command("deploy") } }) .before(checkValidTargetFilters) - .action((options) => { + .action((options: Options) => { return deploy(options.filteredTargets, options); }); diff --git a/src/commands/emulators-exec.ts b/src/commands/emulators-exec.ts index 6c023cbe098..71aeecdd0af 100644 --- a/src/commands/emulators-exec.ts +++ b/src/commands/emulators-exec.ts @@ -8,6 +8,7 @@ export const command = new Command("emulators:exec