Skip to content

Commit

Permalink
Print instructions to assist FDC Onboard Flow (#7802)
Browse files Browse the repository at this point in the history
  • Loading branch information
fredzqm authored Oct 7, 2024
1 parent f350d60 commit 1085394
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 71 deletions.
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export class Config {
fs.removeSync(this.path(p));
}

askWriteProjectFile(p: string, content: any, force?: boolean) {
askWriteProjectFile(p: string, content: any, force?: boolean, confirmByDefault?: boolean) {
const writeTo = this.path(p);
let next;
if (typeof content !== "string") {
Expand All @@ -243,7 +243,7 @@ export class Config {
next = promptOnce({
type: "confirm",
message: "File " + clc.underline(p) + " already exists. Overwrite?",
default: false,
default: !!confirmByDefault,
});
} else {
next = Promise.resolve(true);
Expand Down
8 changes: 8 additions & 0 deletions src/dataconnect/freeTrial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ export function printFreeTrialUnavailable(
`Alternatively, you may create a new (paid) CloudSQL instance at https://console.cloud.google.com/sql/instances`,
);
}

export function upgradeInstructions(projectId: string): string {
return `If you'd like to provision a CloudSQL Postgres instance on the Firebase Data Connect no-cost trial:
1. Please upgrade to the pay-as-you-go (Blaze) billing plan. Visit the following page:
https://console.firebase.google.com/project/${projectId}/usage/details
2. Run ${clc.bold("firebase init dataconnect")} again to configure the Cloud SQL instance.
3. Run ${clc.bold("firebase deploy --only dataconnect")} to deploy your Data Connect service.`;
}
5 changes: 5 additions & 0 deletions src/deploy/dataconnect/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { ensureApis } from "../../dataconnect/ensureApis";
import { requireTosAcceptance } from "../../requireTosAcceptance";
import { DATA_CONNECT_TOS_ID } from "../../gcp/firedata";
import { provisionCloudSql } from "../../dataconnect/provisionCloudSql";
import { checkBillingEnabled } from "../../gcp/cloudbilling";
import { parseServiceName } from "../../dataconnect/names";
import { FirebaseError } from "../../error";
import { requiresVector } from "../../dataconnect/types";
import { diffSchema } from "../../dataconnect/schemaMigration";
import { join } from "node:path";
import { upgradeInstructions } from "../../dataconnect/freeTrial";

/**
* Prepares for a Firebase DataConnect deployment by loading schemas and connectors from file.
Expand All @@ -25,6 +27,9 @@ import { join } from "node:path";
*/
export default async function (context: any, options: DeployOptions): Promise<void> {
const projectId = needProjectId(options);
if (!(await checkBillingEnabled(projectId))) {
throw new FirebaseError(upgradeInstructions(projectId));
}
await ensureApis(projectId);
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
const serviceCfgs = readFirebaseJson(options.config);
Expand Down
133 changes: 64 additions & 69 deletions src/init/features/dataconnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { confirm, promptOnce } from "../../../prompt";
import { Config } from "../../../config";
import { Setup } from "../..";
import { provisionCloudSql } from "../../../dataconnect/provisionCloudSql";
import { checkFreeTrialInstanceUsed } from "../../../dataconnect/freeTrial";
import { checkFreeTrialInstanceUsed, upgradeInstructions } from "../../../dataconnect/freeTrial";
import * as cloudsql from "../../../gcp/cloudsql/cloudsqladmin";
import { ensureApis, ensureSparkApis } from "../../../dataconnect/ensureApis";
import * as experiments from "../../../experiments";
Expand All @@ -19,7 +19,7 @@ import { Schema, Service, File, Platform } from "../../../dataconnect/types";
import { parseCloudSQLInstanceName, parseServiceName } from "../../../dataconnect/names";
import { logger } from "../../../logger";
import { readTemplateSync } from "../../../templates";
import { logBullet, logSuccess } from "../../../utils";
import { logBullet } from "../../../utils";
import { checkBillingEnabled } from "../../../gcp/cloudbilling";
import * as sdk from "./sdk";
import { getPlatformFromFolder } from "../../../dataconnect/fileUtils";
Expand Down Expand Up @@ -74,22 +74,29 @@ const defaultSchema = { path: "schema.gql", content: SCHEMA_TEMPLATE };

// doSetup is split into 2 phases - ask questions and then actuate files and API calls based on those answers.
export async function doSetup(setup: Setup, config: Config): Promise<void> {
const info = await askQuestions(setup);
const isBillingEnabled = setup.projectId ? await checkBillingEnabled(setup.projectId) : false;
if (setup.projectId) {
isBillingEnabled ? await ensureApis(setup.projectId) : await ensureSparkApis(setup.projectId);
}
const info = await askQuestions(setup, isBillingEnabled);
await actuate(setup, config, info);

const cwdPlatformGuess = await getPlatformFromFolder(process.cwd());
if (cwdPlatformGuess !== Platform.NONE) {
await sdk.doSetup(setup, config);
} else {
logBullet(
`If you'd like to add the generated SDK to your app your later, run ${clc.bold("firebase init dataconnect:sdk")}`,
`If you'd like to add the generated SDK to your app your, run ${clc.bold("firebase init dataconnect:sdk")}`,
);
}
if (setup.projectId && !isBillingEnabled) {
logBullet(upgradeInstructions(setup.projectId));
}
}

// askQuestions prompts the user about the Data Connect service they want to init. Any prompting
// logic should live here, and _no_ actuation logic should live here.
async function askQuestions(setup: Setup): Promise<RequiredInfo> {
async function askQuestions(setup: Setup, isBillingEnabled: boolean): Promise<RequiredInfo> {
let info: RequiredInfo = {
serviceId: "",
locationId: "",
Expand All @@ -101,11 +108,8 @@ async function askQuestions(setup: Setup): Promise<RequiredInfo> {
schemaGql: [defaultSchema],
shouldProvisionCSQL: false,
};
const isBillingEnabled = setup.projectId ? await checkBillingEnabled(setup.projectId) : false;
if (setup.projectId) {
isBillingEnabled ? await ensureApis(setup.projectId) : await ensureSparkApis(setup.projectId);
}
info = await checkExistingInstances(setup, info, isBillingEnabled);
// Query backend and pick up any existing services quickly.
info = await promptForExistingServices(setup, info, isBillingEnabled);

const requiredConfigUnset =
info.serviceId === "" ||
Expand All @@ -123,8 +127,7 @@ async function askQuestions(setup: Setup): Promise<RequiredInfo> {
: false;
if (shouldConfigureBackend) {
info = await promptForService(info);
info = await promptForCloudSQLInstance(setup, info);
info = await promptForDatabase(info);
info = await promptForCloudSQL(setup, info);

info.shouldProvisionCSQL = !!(
setup.projectId &&
Expand All @@ -136,16 +139,10 @@ async function askQuestions(setup: Setup): Promise<RequiredInfo> {
}))
);
} else {
if (requiredConfigUnset) {
logBullet(
`Setting placeholder values in dataconnect.yaml. You can edit these before you deploy to specify different IDs or regions.`,
);
}
info.serviceId = info.serviceId !== "" ? info.serviceId : basename(process.cwd());
info.cloudSqlInstanceId =
info.cloudSqlInstanceId !== "" ? info.cloudSqlInstanceId : `${info.serviceId || "app"}-fdc`;
info.locationId = info.locationId !== "" ? info.locationId : `us-central1`;
info.cloudSqlDatabase = info.cloudSqlDatabase !== "" ? info.cloudSqlDatabase : `fdcdb`;
info.serviceId = info.serviceId || basename(process.cwd());
info.cloudSqlInstanceId = info.cloudSqlInstanceId || `${info.serviceId || "app"}-fdc`;
info.locationId = info.locationId || `us-central1`;
info.cloudSqlDatabase = info.cloudSqlDatabase || `fdcdb`;
}
return info;
}
Expand Down Expand Up @@ -176,12 +173,16 @@ async function writeFiles(config: Config, info: RequiredInfo) {
});

config.set("dataconnect", { source: dir });
await config.askWriteProjectFile(join(dir, "dataconnect.yaml"), subbedDataconnectYaml);
await config.askWriteProjectFile(
join(dir, "dataconnect.yaml"),
subbedDataconnectYaml,
false,
// Default to override dataconnect.yaml
// Sole purpose of `firebase init dataconnect` is to update `dataconnect.yaml`.
true,
);

if (info.schemaGql.length) {
logSuccess(
"The service you chose already has GQL files deployed. We'll use those instead of the default templates.",
);
for (const f of info.schemaGql) {
await config.askWriteProjectFile(join(dir, "schema", f.path), f.content);
}
Expand Down Expand Up @@ -244,7 +245,7 @@ function subConnectorYamlValues(replacementValues: { connectorId: string }): str
return replaced;
}

async function checkExistingInstances(
async function promptForExistingServices(
setup: Setup,
info: RequiredInfo,
isBillingEnabled: boolean,
Expand Down Expand Up @@ -314,13 +315,14 @@ async function checkExistingInstances(
});
}
}
} else {
info = await promptForService(info);
}
}
return info;
}

async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<RequiredInfo> {
// Check for existing Cloud SQL instances, if we didn't already set one.
if (info.cloudSqlInstanceId === "") {
if (info.cloudSqlInstanceId === "" && setup.projectId) {
const instances = await cloudsql.listInstances(setup.projectId);
let choices = instances.map((i) => {
let display = `${i.name} (${i.region})`;
Expand All @@ -343,14 +345,31 @@ async function checkExistingInstances(
if (info.cloudSqlInstanceId !== "") {
// Infer location if a CloudSQL instance is chosen.
info.locationId = choices.find((c) => c.value === info.cloudSqlInstanceId)!.location;
} else {
info = await promptForCloudSQLInstance(setup, info);
}
}
}

// Check for existing Cloud SQL databases, if we didn't already set one.
if (info.cloudSqlDatabase === "" && info.cloudSqlInstanceId !== "") {
// No existing instance found or choose to create new instance.
if (info.cloudSqlInstanceId === "") {
info.isNewInstance = true;
info.cloudSqlInstanceId = await promptOnce({
message: `What ID would you like to use for your new CloudSQL instance?`,
type: "input",
default: `${info.serviceId || "app"}-fdc`,
});
}
if (info.locationId === "") {
const choices = await locationChoices(setup);
info.locationId = await promptOnce({
message: "What location would like to use?",
type: "list",
choices,
});
}

// Look for existing databases within the picked instance.
// Best effort since the picked `info.cloudSqlInstanceId` may not exists or is still being provisioned.
if (info.cloudSqlDatabase === "" && setup.projectId) {
try {
const dbs = await cloudsql.listDatabases(setup.projectId, info.cloudSqlInstanceId);
const choices = dbs.map((d) => {
Expand All @@ -363,16 +382,24 @@ async function checkExistingInstances(
type: "list",
choices,
});
if (info.cloudSqlDatabase === "") {
info = await promptForDatabase(info);
}
}
} catch (err) {
// Show existing databases in a list is optional, ignore any errors from ListDatabases.
// This often happen when the Cloud SQL instance is still being created.
logger.debug(`[dataconnect] Cannot list databases during init: ${err}`);
}
}

// No existing database found or cannot access the instance.
// Prompt for a name.
if (info.cloudSqlDatabase === "") {
info.isNewDatabase = true;
info.cloudSqlDatabase = await promptOnce({
message: `What ID would you like to use for your new database in ${info.cloudSqlInstanceId}?`,
type: "input",
default: `fdcdb`,
});
}
return info;
}

Expand All @@ -387,26 +414,6 @@ async function promptForService(info: RequiredInfo): Promise<RequiredInfo> {
return info;
}

async function promptForCloudSQLInstance(setup: Setup, info: RequiredInfo): Promise<RequiredInfo> {
if (info.cloudSqlInstanceId === "") {
info.isNewInstance = true;
info.cloudSqlInstanceId = await promptOnce({
message: `What ID would you like to use for your new CloudSQL instance?`,
type: "input",
default: `${info.serviceId || "app"}-fdc`,
});
}
if (info.locationId === "") {
const choices = await locationChoices(setup);
info.locationId = await promptOnce({
message: "What location would like to use?",
type: "list",
choices,
});
}
return info;
}

async function locationChoices(setup: Setup) {
if (setup.projectId) {
const locations = await listLocations(setup.projectId);
Expand All @@ -427,15 +434,3 @@ async function locationChoices(setup: Setup) {
];
}
}

async function promptForDatabase(info: RequiredInfo): Promise<RequiredInfo> {
if (info.cloudSqlDatabase === "") {
info.isNewDatabase = true;
info.cloudSqlDatabase = await promptOnce({
message: `What ID would you like to use for your new database in ${info.cloudSqlInstanceId}?`,
type: "input",
default: `fdcdb`,
});
}
return info;
}

0 comments on commit 1085394

Please sign in to comment.