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
13 changes: 2 additions & 11 deletions src/dataconnect/freeTrial.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import * as clc from "colorette";

import { queryTimeSeries, CmQuery } from "../gcp/cloudmonitoring";
import * as utils from "../utils";

export function freeTrialTermsLink(): string {

Check warning on line 5 in src/dataconnect/freeTrial.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
return "https://firebase.google.com/pricing";
}

const FREE_TRIAL_METRIC = "sqladmin.googleapis.com/fdc_lifetime_free_trial_per_project";

// Checks whether there is already a free trial instance on a project.
export async function checkFreeTrialInstanceUsed(projectId: string): Promise<boolean> {

Check warning on line 12 in src/dataconnect/freeTrial.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const past7d = new Date();
past7d.setDate(past7d.getDate() - 7);
const query: CmQuery = {
Expand All @@ -24,23 +23,15 @@
if (ts.length) {
used = ts[0].points.some((p) => p.value.int64Value);
}
} catch (err: any) {

Check warning on line 26 in src/dataconnect/freeTrial.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
// If the metric doesn't exist, free trial is not used.
used = false;
}
if (used) {
utils.logLabeledWarning(
"dataconnect",
"CloudSQL no cost trial has already been used on this project.",
);
} else {
utils.logLabeledSuccess("dataconnect", "CloudSQL no cost trial available!");
}
return used;
}

export function upgradeInstructions(projectId: string): string {
return `To provision a CloudSQL Postgres instance on the Firebase Data Connect no-cost trial:
export function upgradeInstructions(projectId: string, trialUsed: boolean): string {

Check warning on line 33 in src/dataconnect/freeTrial.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
return `To provision a ${trialUsed ? "paid CloudSQL Postgres instance" : "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:

Expand Down
26 changes: 18 additions & 8 deletions src/dataconnect/provisionCloudSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { trackGA4 } from "../track";
import * as utils from "../utils";
import { Source } from "../init/features/dataconnect";
import { checkBillingEnabled } from "../gcp/cloudbilling";

const GOOGLE_ML_INTEGRATION_ROLE = "roles/aiplatform.user";

Expand Down Expand Up @@ -109,8 +110,8 @@
}
}
await upsertDatabase({ ...args });
} catch (err: any) {

Check warning on line 113 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 114 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.
Expand Down Expand Up @@ -147,7 +148,12 @@
});
utils.logLabeledBullet(
"dataconnect",
cloudSQLBeingCreated(projectId, instanceId, freeTrialLabel === "ft"),
cloudSQLBeingCreated(
projectId,
instanceId,
freeTrialLabel === "ft",
await checkBillingEnabled(projectId),
),
);
}
}
Expand All @@ -158,18 +164,22 @@
export function cloudSQLBeingCreated(
projectId: string,
instanceId: string,
includeFreeTrialToS?: boolean,
isFreeTrial?: boolean,
billingEnabled?: boolean,
): string {
return (
`Cloud SQL Instance ${clc.bold(instanceId)} is being created.` +
(includeFreeTrialToS
(isFreeTrial
? `\nThis instance is provided under the terms of the Data Connect no-cost trial ${freeTrialTermsLink()}`
: "") +
`
Meanwhile, your data are saved in a temporary database and will be migrated once complete. Monitor its progress at

${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}
`
`\n
Meanwhile, your data are saved in a temporary database and will be migrated once complete.` +
(isFreeTrial && !billingEnabled
? `
Your free trial instance won't show in google cloud console until a billing account is added.
However, you can still use the gcloud cli to monitor your database instance:\n\n\te.g. gcloud sql instances list --project ${projectId}\n`
: `
Monitor its progress at\n\n\t${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}\n`)
);
}

Expand All @@ -183,11 +193,11 @@
try {
await cloudSqlAdminClient.getDatabase(projectId, instanceId, databaseId);
utils.logLabeledBullet("dataconnect", `Found existing Postgres Database ${databaseId}.`);
} catch (err: any) {

Check warning on line 196 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 197 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 200 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
15 changes: 2 additions & 13 deletions src/deploy/dataconnect/prepare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@
import * as build from "../../dataconnect/build";
import * as ensureApis from "../../dataconnect/ensureApis";
import * as requireTosAcceptance from "../../requireTosAcceptance";
import * as cloudbilling from "../../gcp/cloudbilling";
import * as schemaMigration from "../../dataconnect/schemaMigration";
import * as provisionCloudSql from "../../dataconnect/provisionCloudSql";
import * as cloudbilling from "../../gcp/cloudbilling";
import { FirebaseError } from "../../error";

describe("dataconnect prepare", () => {
let sandbox: sinon.SinonSandbox;
let loadAllStub: sinon.SinonStub;
let buildStub: sinon.SinonStub;
let checkBillingEnabledStub: sinon.SinonStub;
let getResourceFiltersStub: sinon.SinonStub;
let diffSchemaStub: sinon.SinonStub;
let setupCloudSqlStub: sinon.SinonStub;
Expand All @@ -26,15 +25,15 @@
beforeEach(() => {
sandbox = sinon.createSandbox();
loadAllStub = sandbox.stub(load, "loadAll").resolves([]);
buildStub = sandbox.stub(build, "build").resolves({} as any);

Check warning on line 28 in src/deploy/dataconnect/prepare.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `DeploymentMetadata | undefined`
checkBillingEnabledStub = sandbox.stub(cloudbilling, "checkBillingEnabled").resolves(true);
sandbox.stub(ensureApis, "ensureApis").resolves();
sandbox.stub(requireTosAcceptance, "requireTosAcceptance").returns(() => Promise.resolve());
getResourceFiltersStub = sandbox.stub(filters, "getResourceFilters").returns(undefined);
diffSchemaStub = sandbox.stub(schemaMigration, "diffSchema").resolves();
setupCloudSqlStub = sandbox.stub(provisionCloudSql, "setupCloudSql").resolves();
sandbox.stub(projectUtils, "needProjectId").returns("test-project");
sandbox.stub(utils, "logLabeledBullet");
sandbox.stub(cloudbilling, "checkBillingEnabled").resolves();
});

afterEach(() => {
Expand All @@ -57,16 +56,6 @@
});
});

it("should throw an error if billing is not enabled", async () => {
checkBillingEnabledStub.resolves(false);
const context = {};
const options = { config: {} } as any;
await expect(prepare.default(context, options)).to.be.rejectedWith(
FirebaseError,
"To provision a CloudSQL Postgres instance on the Firebase Data Connect no-cost trial",
);
});

it("should build services", async () => {
const serviceInfos = [{ sourceDirectory: "a" }, { sourceDirectory: "b" }];
loadAllStub.resolves(serviceInfos as any);
Expand Down
4 changes: 1 addition & 3 deletions src/deploy/dataconnect/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import { ensureApis } from "../../dataconnect/ensureApis";
import { requireTosAcceptance } from "../../requireTosAcceptance";
import { DATA_CONNECT_TOS_ID } from "../../gcp/firedata";
import { setupCloudSql } from "../../dataconnect/provisionCloudSql";
import { checkBillingEnabled } from "../../gcp/cloudbilling";
import { parseServiceName } from "../../dataconnect/names";
import { FirebaseError } from "../../error";
import { mainSchema, requiresVector } from "../../dataconnect/types";
import { diffSchema } from "../../dataconnect/schemaMigration";
import { upgradeInstructions } from "../../dataconnect/freeTrial";
import { checkBillingEnabled } from "../../gcp/cloudbilling";
import { Context, initDeployStats } from "./context";

/**
Expand All @@ -35,7 +34,6 @@ export default async function (context: Context, options: DeployOptions): Promis
const { serviceInfos, filters, deployStats } = context.dataconnect;
if (!(await checkBillingEnabled(projectId))) {
deployStats.missingBilling = true;
throw new FirebaseError(upgradeInstructions(projectId));
}
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
for (const si of serviceInfos) {
Expand Down
5 changes: 5 additions & 0 deletions src/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ export const ALL_EXPERIMENTS = experiments({
default: false,
public: true,
},
fdcift: {
shortDescription: "Enable instrumentless trial for Data Connect",
public: false,
default: false,
},
apptesting: {
shortDescription: "Adds experimental App Testing feature",
public: true,
Expand Down
26 changes: 25 additions & 1 deletion src/gcp/cloudsql/cloudsqladmin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,15 @@ describe("cloudsqladmin", () => {
});

describe("createInstance", () => {
it("should create an instance", async () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
});

afterEach(() => {
sandbox.restore();
nock.cleanAll();
});
it("should create an paid instance", async () => {
nock(cloudSQLAdminOrigin())
.post(`/${API_VERSION}/projects/${PROJECT_ID}/instances`)
.reply(200, {});
Expand All @@ -164,6 +172,22 @@ describe("cloudsqladmin", () => {

expect(nock.isDone()).to.be.true;
});

it("should create a free instance.", async () => {
nock(cloudSQLAdminOrigin())
.post(`/${API_VERSION}/projects/${PROJECT_ID}/instances`)
.reply(200, {});

await sqladmin.createInstance({
projectId: PROJECT_ID,
location: "us-central",
instanceId: INSTANCE_ID,
enableGoogleMlIntegration: false,
freeTrialLabel: "ft",
});

expect(nock.isDone()).to.be.true;
});
});

describe("updateInstanceForDataConnect", () => {
Expand Down
1 change: 1 addition & 0 deletions src/gcp/cloudsql/cloudsqladmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Options } from "../../options";
import { logger } from "../../logger";
import { testIamPermissions } from "../iam";
import { FirebaseError } from "../../error";

const API_VERSION = "v1";

const client = new Client({
Expand Down
48 changes: 33 additions & 15 deletions src/init/features/dataconnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
promiseWithSpinner,
logLabeledError,
newUniqueId,
logLabeledWarning,
logLabeledSuccess,
} from "../../../utils";
import { isBillingEnabled } from "../../../gcp/cloudbilling";
import * as sdk from "./sdk";
Expand All @@ -40,6 +42,7 @@ import {
} from "../../../gemini/fdcExperience";
import { configstore } from "../../../configstore";
import { trackGA4 } from "../../../track";
import { isEnabled } from "../../../experiments";

// Default GCP region for Data Connect
export const FDC_DEFAULT_REGION = "us-east4";
Expand Down Expand Up @@ -126,7 +129,6 @@ export async function askQuestions(setup: Setup): Promise<void> {
shouldProvisionCSQL: false,
};
if (setup.projectId) {
const hasBilling = await isBillingEnabled(setup);
await ensureApis(setup.projectId);
await promptForExistingServices(setup, info);
if (!info.serviceGql) {
Expand Down Expand Up @@ -154,11 +156,7 @@ export async function askQuestions(setup: Setup): Promise<void> {
});
}
}
if (hasBilling) {
await promptForCloudSQL(setup, info);
} else if (info.appDescription) {
await promptForLocation(setup, info);
}
await promptForCloudSQL(setup, info);
}
setup.featureInfo = setup.featureInfo || {};
setup.featureInfo.dataconnect = info;
Expand Down Expand Up @@ -216,9 +214,6 @@ export async function actuate(setup: Setup, config: Config, options: any): Promi
https://console.firebase.google.com/project/${setup.projectId!}/dataconnect/locations/${info.locationId}/services/${info.serviceId}/schema`,
);
}
if (!(await isBillingEnabled(setup))) {
setup.instructions.push(upgradeInstructions(setup.projectId || "your-firebase-project"));
}
setup.instructions.push(
`Install the Data Connect VS Code Extensions. You can explore Data Connect Query on local pgLite and Cloud SQL Postgres Instance.`,
);
Expand All @@ -238,9 +233,7 @@ async function actuateWithInfo(
}

await ensureApis(projectId, /* silent =*/ true);
const provisionCSQL = info.shouldProvisionCSQL && (await isBillingEnabled(setup));
if (provisionCSQL) {
// Kicks off Cloud SQL provisioning if the project has billing enabled.
if (info.shouldProvisionCSQL) {
await setupCloudSql({
projectId: projectId,
location: info.locationId,
Expand Down Expand Up @@ -296,7 +289,7 @@ async function actuateWithInfo(
projectId,
info,
schemaFiles,
provisionCSQL,
info.shouldProvisionCSQL,
);
await upsertSchema(saveSchemaGql);
if (waitForCloudSQLProvision) {
Expand Down Expand Up @@ -695,6 +688,31 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
if (!setup.projectId) {
return;
}
const instrumentlessTrialEnabled = isEnabled("fdcift");
const billingEnabled = await isBillingEnabled(setup);
const freeTrialUsed = await checkFreeTrialInstanceUsed(setup.projectId);
const freeTrialAvailable = !freeTrialUsed && (billingEnabled || instrumentlessTrialEnabled);

if (!billingEnabled && !instrumentlessTrialEnabled) {
setup.instructions.push(upgradeInstructions(setup.projectId || "your-firebase-project", false));
return;
}

if (freeTrialUsed) {
logLabeledWarning(
"dataconnect",
"CloudSQL no cost trial has already been used on this project.",
);
if (!billingEnabled) {
setup.instructions.push(
upgradeInstructions(setup.projectId || "your-firebase-project", true),
);
return;
}
} else if (instrumentlessTrialEnabled || billingEnabled) {
logLabeledSuccess("dataconnect", "CloudSQL no cost trial available!");
}

// Check for existing Cloud SQL instances, if we didn't already set one.
if (info.cloudSqlInstanceId === "") {
const instances = await cloudsql.listInstances(setup.projectId);
Expand All @@ -708,7 +726,7 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
// If we've already chosen a region (ie service already exists), only list instances from that region.
choices = choices.filter((c) => info.locationId === "" || info.locationId === c.location);
if (choices.length) {
if (!(await checkFreeTrialInstanceUsed(setup.projectId))) {
if (freeTrialAvailable) {
choices.push({ name: "Create a new free trial instance", value: "", location: "" });
} else {
choices.push({ name: "Create a new CloudSQL instance", value: "", location: "" });
Expand Down Expand Up @@ -737,7 +755,7 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
if (info.locationId === "") {
await promptForLocation(setup, info);
info.shouldProvisionCSQL = await confirm({
message: `Would you like to provision your Cloud SQL instance and database now?`,
message: `Would you like to provision your ${freeTrialAvailable ? "free trial " : ""}Cloud SQL instance and database now?`,
default: true,
});
}
Expand Down
Loading