Skip to content

Commit 67362a2

Browse files
tammam-gjoehan
andauthored
FDC free trial (#9494)
* Starting to make billing optional * Finish removing billing requirements * Fixing tests * Clean up * Format * One more usage of removed variable * Improve gating for when to indicate a free trial is avaialbe + format fixes * Fix inversed if statement * improves when messages show about free trial availability * Use expirement flag and make flow identical when flag is disabled * fix formatting * fix tests * cleanup --------- Co-authored-by: Joe Hanley <[email protected]>
1 parent f85f4db commit 67362a2

File tree

8 files changed

+87
-51
lines changed

8 files changed

+87
-51
lines changed

src/dataconnect/freeTrial.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as clc from "colorette";
22

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

65
export function freeTrialTermsLink(): string {
76
return "https://firebase.google.com/pricing";
@@ -28,19 +27,11 @@ export async function checkFreeTrialInstanceUsed(projectId: string): Promise<boo
2827
// If the metric doesn't exist, free trial is not used.
2928
used = false;
3029
}
31-
if (used) {
32-
utils.logLabeledWarning(
33-
"dataconnect",
34-
"CloudSQL no cost trial has already been used on this project.",
35-
);
36-
} else {
37-
utils.logLabeledSuccess("dataconnect", "CloudSQL no cost trial available!");
38-
}
3930
return used;
4031
}
4132

42-
export function upgradeInstructions(projectId: string): string {
43-
return `To provision a CloudSQL Postgres instance on the Firebase Data Connect no-cost trial:
33+
export function upgradeInstructions(projectId: string, trialUsed: boolean): string {
34+
return `To provision a ${trialUsed ? "paid CloudSQL Postgres instance" : "CloudSQL Postgres instance on the Firebase Data Connect no-cost trial"}:
4435
4536
1. Please upgrade to the pay-as-you-go (Blaze) billing plan. Visit the following page:
4637

src/dataconnect/provisionCloudSql.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { promiseWithSpinner } from "../utils";
99
import { trackGA4 } from "../track";
1010
import * as utils from "../utils";
1111
import { Source } from "../init/features/dataconnect";
12+
import { checkBillingEnabled } from "../gcp/cloudbilling";
1213

1314
const GOOGLE_ML_INTEGRATION_ROLE = "roles/aiplatform.user";
1415

@@ -147,7 +148,12 @@ async function createInstance(args: {
147148
});
148149
utils.logLabeledBullet(
149150
"dataconnect",
150-
cloudSQLBeingCreated(projectId, instanceId, freeTrialLabel === "ft"),
151+
cloudSQLBeingCreated(
152+
projectId,
153+
instanceId,
154+
freeTrialLabel === "ft",
155+
await checkBillingEnabled(projectId),
156+
),
151157
);
152158
}
153159
}
@@ -158,18 +164,22 @@ async function createInstance(args: {
158164
export function cloudSQLBeingCreated(
159165
projectId: string,
160166
instanceId: string,
161-
includeFreeTrialToS?: boolean,
167+
isFreeTrial?: boolean,
168+
billingEnabled?: boolean,
162169
): string {
163170
return (
164171
`Cloud SQL Instance ${clc.bold(instanceId)} is being created.` +
165-
(includeFreeTrialToS
172+
(isFreeTrial
166173
? `\nThis instance is provided under the terms of the Data Connect no-cost trial ${freeTrialTermsLink()}`
167174
: "") +
168-
`
169-
Meanwhile, your data are saved in a temporary database and will be migrated once complete. Monitor its progress at
170-
171-
${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}
172-
`
175+
`\n
176+
Meanwhile, your data are saved in a temporary database and will be migrated once complete.` +
177+
(isFreeTrial && !billingEnabled
178+
? `
179+
Your free trial instance won't show in google cloud console until a billing account is added.
180+
However, you can still use the gcloud cli to monitor your database instance:\n\n\te.g. gcloud sql instances list --project ${projectId}\n`
181+
: `
182+
Monitor its progress at\n\n\t${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}\n`)
173183
);
174184
}
175185

src/deploy/dataconnect/prepare.spec.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@ import * as filters from "../../dataconnect/filters";
99
import * as build from "../../dataconnect/build";
1010
import * as ensureApis from "../../dataconnect/ensureApis";
1111
import * as requireTosAcceptance from "../../requireTosAcceptance";
12-
import * as cloudbilling from "../../gcp/cloudbilling";
1312
import * as schemaMigration from "../../dataconnect/schemaMigration";
1413
import * as provisionCloudSql from "../../dataconnect/provisionCloudSql";
14+
import * as cloudbilling from "../../gcp/cloudbilling";
1515
import { FirebaseError } from "../../error";
1616

1717
describe("dataconnect prepare", () => {
1818
let sandbox: sinon.SinonSandbox;
1919
let loadAllStub: sinon.SinonStub;
2020
let buildStub: sinon.SinonStub;
21-
let checkBillingEnabledStub: sinon.SinonStub;
2221
let getResourceFiltersStub: sinon.SinonStub;
2322
let diffSchemaStub: sinon.SinonStub;
2423
let setupCloudSqlStub: sinon.SinonStub;
@@ -27,14 +26,14 @@ describe("dataconnect prepare", () => {
2726
sandbox = sinon.createSandbox();
2827
loadAllStub = sandbox.stub(load, "loadAll").resolves([]);
2928
buildStub = sandbox.stub(build, "build").resolves({} as any);
30-
checkBillingEnabledStub = sandbox.stub(cloudbilling, "checkBillingEnabled").resolves(true);
3129
sandbox.stub(ensureApis, "ensureApis").resolves();
3230
sandbox.stub(requireTosAcceptance, "requireTosAcceptance").returns(() => Promise.resolve());
3331
getResourceFiltersStub = sandbox.stub(filters, "getResourceFilters").returns(undefined);
3432
diffSchemaStub = sandbox.stub(schemaMigration, "diffSchema").resolves();
3533
setupCloudSqlStub = sandbox.stub(provisionCloudSql, "setupCloudSql").resolves();
3634
sandbox.stub(projectUtils, "needProjectId").returns("test-project");
3735
sandbox.stub(utils, "logLabeledBullet");
36+
sandbox.stub(cloudbilling, "checkBillingEnabled").resolves();
3837
});
3938

4039
afterEach(() => {
@@ -57,16 +56,6 @@ describe("dataconnect prepare", () => {
5756
});
5857
});
5958

60-
it("should throw an error if billing is not enabled", async () => {
61-
checkBillingEnabledStub.resolves(false);
62-
const context = {};
63-
const options = { config: {} } as any;
64-
await expect(prepare.default(context, options)).to.be.rejectedWith(
65-
FirebaseError,
66-
"To provision a CloudSQL Postgres instance on the Firebase Data Connect no-cost trial",
67-
);
68-
});
69-
7059
it("should build services", async () => {
7160
const serviceInfos = [{ sourceDirectory: "a" }, { sourceDirectory: "b" }];
7261
loadAllStub.resolves(serviceInfos as any);

src/deploy/dataconnect/prepare.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ import { ensureApis } from "../../dataconnect/ensureApis";
1111
import { requireTosAcceptance } from "../../requireTosAcceptance";
1212
import { DATA_CONNECT_TOS_ID } from "../../gcp/firedata";
1313
import { setupCloudSql } from "../../dataconnect/provisionCloudSql";
14-
import { checkBillingEnabled } from "../../gcp/cloudbilling";
1514
import { parseServiceName } from "../../dataconnect/names";
1615
import { FirebaseError } from "../../error";
1716
import { mainSchema, requiresVector } from "../../dataconnect/types";
1817
import { diffSchema } from "../../dataconnect/schemaMigration";
19-
import { upgradeInstructions } from "../../dataconnect/freeTrial";
18+
import { checkBillingEnabled } from "../../gcp/cloudbilling";
2019
import { Context, initDeployStats } from "./context";
2120

2221
/**
@@ -35,7 +34,6 @@ export default async function (context: Context, options: DeployOptions): Promis
3534
const { serviceInfos, filters, deployStats } = context.dataconnect;
3635
if (!(await checkBillingEnabled(projectId))) {
3736
deployStats.missingBilling = true;
38-
throw new FirebaseError(upgradeInstructions(projectId));
3937
}
4038
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
4139
for (const si of serviceInfos) {

src/experiments.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ export const ALL_EXPERIMENTS = experiments({
144144
default: false,
145145
public: true,
146146
},
147+
fdcift: {
148+
shortDescription: "Enable instrumentless trial for Data Connect",
149+
public: false,
150+
default: false,
151+
},
147152
apptesting: {
148153
shortDescription: "Adds experimental App Testing feature",
149154
public: true,

src/gcp/cloudsql/cloudsqladmin.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,15 @@ describe("cloudsqladmin", () => {
149149
});
150150

151151
describe("createInstance", () => {
152-
it("should create an instance", async () => {
152+
beforeEach(() => {
153+
sandbox = sinon.createSandbox();
154+
});
155+
156+
afterEach(() => {
157+
sandbox.restore();
158+
nock.cleanAll();
159+
});
160+
it("should create an paid instance", async () => {
153161
nock(cloudSQLAdminOrigin())
154162
.post(`/${API_VERSION}/projects/${PROJECT_ID}/instances`)
155163
.reply(200, {});
@@ -164,6 +172,22 @@ describe("cloudsqladmin", () => {
164172

165173
expect(nock.isDone()).to.be.true;
166174
});
175+
176+
it("should create a free instance.", async () => {
177+
nock(cloudSQLAdminOrigin())
178+
.post(`/${API_VERSION}/projects/${PROJECT_ID}/instances`)
179+
.reply(200, {});
180+
181+
await sqladmin.createInstance({
182+
projectId: PROJECT_ID,
183+
location: "us-central",
184+
instanceId: INSTANCE_ID,
185+
enableGoogleMlIntegration: false,
186+
freeTrialLabel: "ft",
187+
});
188+
189+
expect(nock.isDone()).to.be.true;
190+
});
167191
});
168192

169193
describe("updateInstanceForDataConnect", () => {

src/gcp/cloudsql/cloudsqladmin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Options } from "../../options";
88
import { logger } from "../../logger";
99
import { testIamPermissions } from "../iam";
1010
import { FirebaseError } from "../../error";
11+
1112
const API_VERSION = "v1";
1213

1314
const client = new Client({

src/init/features/dataconnect/index.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
promiseWithSpinner,
3030
logLabeledError,
3131
newUniqueId,
32+
logLabeledWarning,
33+
logLabeledSuccess,
3234
} from "../../../utils";
3335
import { isBillingEnabled } from "../../../gcp/cloudbilling";
3436
import * as sdk from "./sdk";
@@ -40,6 +42,7 @@ import {
4042
} from "../../../gemini/fdcExperience";
4143
import { configstore } from "../../../configstore";
4244
import { trackGA4 } from "../../../track";
45+
import { isEnabled } from "../../../experiments";
4346

4447
// Default GCP region for Data Connect
4548
export const FDC_DEFAULT_REGION = "us-east4";
@@ -126,7 +129,6 @@ export async function askQuestions(setup: Setup): Promise<void> {
126129
shouldProvisionCSQL: false,
127130
};
128131
if (setup.projectId) {
129-
const hasBilling = await isBillingEnabled(setup);
130132
await ensureApis(setup.projectId);
131133
await promptForExistingServices(setup, info);
132134
if (!info.serviceGql) {
@@ -154,11 +156,7 @@ export async function askQuestions(setup: Setup): Promise<void> {
154156
});
155157
}
156158
}
157-
if (hasBilling) {
158-
await promptForCloudSQL(setup, info);
159-
} else if (info.appDescription) {
160-
await promptForLocation(setup, info);
161-
}
159+
await promptForCloudSQL(setup, info);
162160
}
163161
setup.featureInfo = setup.featureInfo || {};
164162
setup.featureInfo.dataconnect = info;
@@ -216,9 +214,6 @@ export async function actuate(setup: Setup, config: Config, options: any): Promi
216214
https://console.firebase.google.com/project/${setup.projectId!}/dataconnect/locations/${info.locationId}/services/${info.serviceId}/schema`,
217215
);
218216
}
219-
if (!(await isBillingEnabled(setup))) {
220-
setup.instructions.push(upgradeInstructions(setup.projectId || "your-firebase-project"));
221-
}
222217
setup.instructions.push(
223218
`Install the Data Connect VS Code Extensions. You can explore Data Connect Query on local pgLite and Cloud SQL Postgres Instance.`,
224219
);
@@ -238,9 +233,7 @@ async function actuateWithInfo(
238233
}
239234

240235
await ensureApis(projectId, /* silent =*/ true);
241-
const provisionCSQL = info.shouldProvisionCSQL && (await isBillingEnabled(setup));
242-
if (provisionCSQL) {
243-
// Kicks off Cloud SQL provisioning if the project has billing enabled.
236+
if (info.shouldProvisionCSQL) {
244237
await setupCloudSql({
245238
projectId: projectId,
246239
location: info.locationId,
@@ -296,7 +289,7 @@ async function actuateWithInfo(
296289
projectId,
297290
info,
298291
schemaFiles,
299-
provisionCSQL,
292+
info.shouldProvisionCSQL,
300293
);
301294
await upsertSchema(saveSchemaGql);
302295
if (waitForCloudSQLProvision) {
@@ -695,6 +688,31 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
695688
if (!setup.projectId) {
696689
return;
697690
}
691+
const instrumentlessTrialEnabled = isEnabled("fdcift");
692+
const billingEnabled = await isBillingEnabled(setup);
693+
const freeTrialUsed = await checkFreeTrialInstanceUsed(setup.projectId);
694+
const freeTrialAvailable = !freeTrialUsed && (billingEnabled || instrumentlessTrialEnabled);
695+
696+
if (!billingEnabled && !instrumentlessTrialEnabled) {
697+
setup.instructions.push(upgradeInstructions(setup.projectId || "your-firebase-project", false));
698+
return;
699+
}
700+
701+
if (freeTrialUsed) {
702+
logLabeledWarning(
703+
"dataconnect",
704+
"CloudSQL no cost trial has already been used on this project.",
705+
);
706+
if (!billingEnabled) {
707+
setup.instructions.push(
708+
upgradeInstructions(setup.projectId || "your-firebase-project", true),
709+
);
710+
return;
711+
}
712+
} else if (instrumentlessTrialEnabled || billingEnabled) {
713+
logLabeledSuccess("dataconnect", "CloudSQL no cost trial available!");
714+
}
715+
698716
// Check for existing Cloud SQL instances, if we didn't already set one.
699717
if (info.cloudSqlInstanceId === "") {
700718
const instances = await cloudsql.listInstances(setup.projectId);
@@ -708,7 +726,7 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
708726
// If we've already chosen a region (ie service already exists), only list instances from that region.
709727
choices = choices.filter((c) => info.locationId === "" || info.locationId === c.location);
710728
if (choices.length) {
711-
if (!(await checkFreeTrialInstanceUsed(setup.projectId))) {
729+
if (freeTrialAvailable) {
712730
choices.push({ name: "Create a new free trial instance", value: "", location: "" });
713731
} else {
714732
choices.push({ name: "Create a new CloudSQL instance", value: "", location: "" });
@@ -737,7 +755,7 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
737755
if (info.locationId === "") {
738756
await promptForLocation(setup, info);
739757
info.shouldProvisionCSQL = await confirm({
740-
message: `Would you like to provision your Cloud SQL instance and database now?`,
758+
message: `Would you like to provision your ${freeTrialAvailable ? "free trial " : ""}Cloud SQL instance and database now?`,
741759
default: true,
742760
});
743761
}

0 commit comments

Comments
 (0)