Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 65 additions & 19 deletions src/dataconnect/provisionCloudSql.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,85 @@
import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
import * as utils from "../utils";
import * as clc from "colorette";
import { grantRolesToCloudSqlServiceAccount } from "./checkIam";

import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
import { Instance } from "../gcp/cloudsql/types";
import { promiseWithSpinner } from "../utils";
import { logger } from "../logger";
import { freeTrialTermsLink, checkFreeTrialInstanceUsed } from "./freeTrial";
import { grantRolesToCloudSqlServiceAccount } from "./checkIam";
import { checkFreeTrialInstanceUsed, freeTrialTermsLink } from "./freeTrial";
import { promiseWithSpinner } from "../utils";
import { trackGA4 } from "../track";
import * as utils from "../utils";

const GOOGLE_ML_INTEGRATION_ROLE = "roles/aiplatform.user";

type SetupStats = {
action: "get" | "update" | "create";
databaseVersion?: string;
dataconnectLabel?: cloudSqlAdminClient.DataConnectLabel;
};

/** Sets up a Cloud SQL instance, database and its permissions. */
export async function setupCloudSql(args: {
projectId: string;
location: string;
instanceId: string;
databaseId: string;
requireGoogleMlIntegration: boolean;
source: "init" | "mcp_init" | "deploy";
dryRun?: boolean;
}): Promise<void> {
await upsertInstance({ ...args });
const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args;

const startTime = Date.now();
const stats: SetupStats = { action: "get" };
let success = false;
try {
await upsertInstance(stats, { ...args });
success = true;
} finally {
if (!dryRun) {
await trackGA4(
"dataconnect_cloud_sql",
{
source: args.source,
action: success ? stats.action : `${stats.action}_error`,
location: args.location,
enable_google_ml_integration: args.requireGoogleMlIntegration.toString(),
database_version: stats.databaseVersion?.toLowerCase() || "unknown",
dataconnect_label: stats.dataconnectLabel || "unknown",
},
Date.now() - startTime,
);
}
}

if (requireGoogleMlIntegration && !dryRun) {
await grantRolesToCloudSqlServiceAccount(projectId, instanceId, [GOOGLE_ML_INTEGRATION_ROLE]);
}
}

async function upsertInstance(args: {
projectId: string;
location: string;
instanceId: string;
databaseId: string;
requireGoogleMlIntegration: boolean;
dryRun?: boolean;
}): Promise<void> {
async function upsertInstance(
stats: SetupStats,
args: {
projectId: string;
location: string;
instanceId: string;
databaseId: string;
requireGoogleMlIntegration: boolean;
dryRun?: boolean;
},
): Promise<void> {
const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args;
try {
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
utils.logLabeledBullet(
"dataconnect",
`Found existing Cloud SQL instance ${clc.bold(instanceId)}.`,
);
stats.databaseVersion = existingInstance.databaseVersion;
stats.dataconnectLabel = existingInstance.settings?.userLabels?.["firebase-data-connect"] as
| cloudSqlAdminClient.DataConnectLabel
| undefined;

const why = getUpdateReason(existingInstance, requireGoogleMlIntegration);
if (why) {
if (dryRun) {
Expand All @@ -55,6 +95,7 @@
`Cloud SQL instance ${clc.bold(instanceId)} settings are not compatible with Firebase Data Connect. ` +
why,
);
stats.action = "update";
await promiseWithSpinner(
() =>
cloudSqlAdminClient.updateInstanceForDataConnect(
Expand All @@ -66,12 +107,16 @@
}
}
await upsertDatabase({ ...args });
} catch (err: any) {

Check warning on line 110 in src/dataconnect/provisionCloudSql.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err.status !== 404) {

Check warning on line 111 in src/dataconnect/provisionCloudSql.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value
throw err;
}
// Cloud SQL instance is not found, start its creation.
await createInstance({ ...args });
stats.action = "create";
stats.databaseVersion = cloudSqlAdminClient.DEFAULT_DATABASE_VERSION;
const freeTrialUsed = await checkFreeTrialInstanceUsed(projectId);
stats.dataconnectLabel = freeTrialUsed ? "nt" : "ft";
await createInstance({ ...args, freeTrialLabel: stats.dataconnectLabel });
}
}

Expand All @@ -80,10 +125,11 @@
location: string;
instanceId: string;
requireGoogleMlIntegration: boolean;
freeTrialLabel: cloudSqlAdminClient.DataConnectLabel;
dryRun?: boolean;
}): Promise<void> {
const { projectId, location, instanceId, requireGoogleMlIntegration, dryRun } = args;
const freeTrialUsed = await checkFreeTrialInstanceUsed(projectId);
const { projectId, location, instanceId, requireGoogleMlIntegration, dryRun, freeTrialLabel } =
args;
if (dryRun) {
utils.logLabeledBullet(
"dataconnect",
Expand All @@ -95,11 +141,11 @@
location,
instanceId,
enableGoogleMlIntegration: requireGoogleMlIntegration,
freeTrial: !freeTrialUsed,
freeTrialLabel,
});
utils.logLabeledBullet(
"dataconnect",
cloudSQLBeingCreated(projectId, instanceId, !freeTrialUsed),
cloudSQLBeingCreated(projectId, instanceId, freeTrialLabel === "ft"),
);
}
}
Expand Down Expand Up @@ -135,11 +181,11 @@
try {
await cloudSqlAdminClient.getDatabase(projectId, instanceId, databaseId);
utils.logLabeledBullet("dataconnect", `Found existing Postgres Database ${databaseId}.`);
} catch (err: any) {

Check warning on line 184 in src/dataconnect/provisionCloudSql.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err.status !== 404) {

Check warning on line 185 in src/dataconnect/provisionCloudSql.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value
// Skip it if the database is not accessible.
// Possible that the CSQL instance is in the middle of something.
logger.debug(`Unexpected error from Cloud SQL: ${err}`);

Check warning on line 188 in src/dataconnect/provisionCloudSql.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "any" of template literal expression
utils.logLabeledWarning("dataconnect", `Postgres Database ${databaseId} is not accessible.`);
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/deploy/dataconnect/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/**
* Checks for and creates a Firebase DataConnect service, if needed.
* TODO: Also checks for and creates a CloudSQL instance and database.
* @param context The deploy context.

Check warning on line 16 in src/deploy/dataconnect/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing @param "context.dataconnect.filters"

Check warning on line 16 in src/deploy/dataconnect/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing @param "context.dataconnect.serviceInfos"

Check warning on line 16 in src/deploy/dataconnect/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing @param "context.dataconnect"
* @param options The CLI options object.
*/
export default async function (
Expand All @@ -26,7 +26,7 @@
options: Options,
): Promise<void> {
const projectId = needProjectId(options);
const serviceInfos = context.dataconnect.serviceInfos as ServiceInfo[];

Check warning on line 29 in src/deploy/dataconnect/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

This assertion is unnecessary since it does not change the type of the expression
const services = await client.listAllServices(projectId);
const filters = context.dataconnect.filters;

Expand Down Expand Up @@ -96,6 +96,7 @@
instanceId,
databaseId,
requireGoogleMlIntegration: requiresVector(s.deploymentMetadata),
source: "deploy",
});
}
}),
Expand All @@ -103,7 +104,7 @@
return;
}

function matches(si: ServiceInfo, s: Service) {

Check warning on line 107 in src/deploy/dataconnect/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
return si.serviceName === s.name;
}

Expand Down
1 change: 1 addition & 0 deletions src/deploy/dataconnect/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default async function (context: any, options: DeployOptions): Promise<vo
databaseId,
requireGoogleMlIntegration: requiresVector(s.deploymentMetadata),
dryRun: true,
source: "deploy",
});
}
}),
Expand Down
4 changes: 2 additions & 2 deletions src/gcp/cloudsql/cloudsqladmin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe("cloudsqladmin", () => {
location: "us-central",
instanceId: INSTANCE_ID,
enableGoogleMlIntegration: false,
freeTrial: false,
freeTrialLabel: "nt",
}),
).to.be.rejectedWith("Cloud SQL free trial instances are not yet available in us-central");
expect(nock.isDone()).to.be.true;
Expand Down Expand Up @@ -160,7 +160,7 @@ describe("cloudsqladmin", () => {
location: "us-central",
instanceId: INSTANCE_ID,
enableGoogleMlIntegration: false,
freeTrial: false,
freeTrialLabel: "nt",
});

expect(nock.isDone()).to.be.true;
Expand Down
9 changes: 6 additions & 3 deletions src/gcp/cloudsql/cloudsqladmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ export function instanceConsoleLink(projectId: string, instanceId: string) {
return `https://console.cloud.google.com/sql/instances/${instanceId}/overview?project=${projectId}`;
}

export type DataConnectLabel = "ft" | "nt";
export const DEFAULT_DATABASE_VERSION = "POSTGRES_15";

export async function createInstance(args: {
projectId: string;
location: string;
instanceId: string;
enableGoogleMlIntegration: boolean;
freeTrial: boolean;
freeTrialLabel: DataConnectLabel;
}): Promise<void> {
const databaseFlags = [{ name: "cloudsql.iam_authentication", value: "on" }];
if (args.enableGoogleMlIntegration) {
Expand All @@ -74,7 +77,7 @@ export async function createInstance(args: {
await client.post<Partial<Instance>, Operation>(`projects/${args.projectId}/instances`, {
name: args.instanceId,
region: args.location,
databaseVersion: "POSTGRES_15",
databaseVersion: DEFAULT_DATABASE_VERSION,
settings: {
tier: "db-f1-micro",
edition: "ENTERPRISE",
Expand All @@ -84,7 +87,7 @@ export async function createInstance(args: {
enableGoogleMlIntegration: args.enableGoogleMlIntegration,
databaseFlags,
storageAutoResize: false,
userLabels: { "firebase-data-connect": args.freeTrial ? "ft" : "nt" },
userLabels: { "firebase-data-connect": args.freeTrialLabel },
insightsConfig: {
queryInsightsEnabled: true,
queryPlansPerMinute: 5, // Match the default settings
Expand Down
1 change: 1 addition & 0 deletions src/init/features/dataconnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ async function actuateWithInfo(
instanceId: info.cloudSqlInstanceId,
databaseId: info.cloudSqlDatabase,
requireGoogleMlIntegration: false,
source: info.analyticsFlow.startsWith("mcp") ? "mcp_init" : "init",
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/init/features/dataconnect/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export async function actuate(setup: Setup, config: Config) {
} finally {
let flow = "no_app";
if (sdkInfo.apps.length) {
const platforms = sdkInfo.apps.map(appDescription).sort();
const platforms = sdkInfo.apps.map((a) => a.platform.toLowerCase()).sort();
flow = `${platforms.join("_")}_app`;
}
if (fdcInfo) {
Expand Down
1 change: 1 addition & 0 deletions src/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type cliEventNames =
| "product_init"
| "product_init_mcp"
| "dataconnect_init"
| "dataconnect_cloud_sql"
| "error"
| "login"
| "api_enabled"
Expand Down
Loading