Skip to content

Commit a347f1a

Browse files
authored
Let users create paid CSQl instances (#8429)
* Let users create paid CSQl instances * Format
1 parent c232bb9 commit a347f1a

File tree

7 files changed

+67
-79
lines changed

7 files changed

+67
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
- Fixed an issue in the extensions emulator where parameter default values would not be substitued into resource definitions.
22
- Keep artifact registry dry run off for policy changes #8419
3+
- Allowed users to create paid Cloud SQL instances for Data Connect when the free trial has already been used.

src/dataconnect/freeTrial.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import * as clc from "colorette";
2+
13
import { queryTimeSeries, CmQuery } from "../gcp/cloudmonitoring";
24
import { listInstances } from "../gcp/cloudsql/cloudsqladmin";
35
import * as utils from "../utils";
4-
import * as clc from "colorette";
56

67
export function freeTrialTermsLink(): string {
78
return "https://firebase.google.com/pricing";

src/dataconnect/provisionCloudSql.ts

Lines changed: 34 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,13 @@ import { logger } from "../logger";
77

88
const GOOGLE_ML_INTEGRATION_ROLE = "roles/aiplatform.user";
99

10-
import {
11-
getFreeTrialInstanceId,
12-
freeTrialTermsLink,
13-
printFreeTrialUnavailable,
14-
isFreeTrialError,
15-
} from "./freeTrial";
16-
import { FirebaseError } from "../error";
10+
import { freeTrialTermsLink, checkFreeTrialInstanceUsed } from "./freeTrial";
1711

1812
export async function provisionCloudSql(args: {
1913
projectId: string;
20-
locationId: string;
14+
location: string;
2115
instanceId: string;
2216
databaseId: string;
23-
configYamlPath: string;
2417
enableGoogleMlIntegration: boolean;
2518
waitForCreation: boolean;
2619
silent?: boolean;
@@ -29,10 +22,9 @@ export async function provisionCloudSql(args: {
2922
let connectionName = ""; // Not used yet, will be used for schema migration
3023
const {
3124
projectId,
32-
locationId,
25+
location,
3326
instanceId,
3427
databaseId,
35-
configYamlPath,
3628
enableGoogleMlIntegration,
3729
waitForCreation,
3830
silent,
@@ -71,45 +63,41 @@ export async function provisionCloudSql(args: {
7163
}
7264
cmekWarning();
7365
const cta = dryRun ? "It will be created on your next deploy" : "Creating it now.";
66+
const freeTrialUsed = await checkFreeTrialInstanceUsed(projectId);
7467
silent ||
7568
utils.logLabeledBullet(
7669
"dataconnect",
77-
`CloudSQL instance '${instanceId}' not found.` +
78-
cta +
79-
`\nThis instance is provided under the terms of the Data Connect no-cost trial ${freeTrialTermsLink()}` +
80-
`\nMonitor the progress at ${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}`,
70+
`CloudSQL instance '${instanceId}' not found.` + cta + freeTrialUsed
71+
? ""
72+
: `\nThis instance is provided under the terms of the Data Connect no-cost trial ${freeTrialTermsLink()}` +
73+
dryRun
74+
? `\nMonitor the progress at ${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}`
75+
: "",
8176
);
77+
8278
if (!dryRun) {
83-
try {
84-
const newInstance = await promiseWithSpinner(
85-
() =>
86-
cloudSqlAdminClient.createInstance(
87-
projectId,
88-
locationId,
89-
instanceId,
90-
enableGoogleMlIntegration,
91-
waitForCreation,
92-
),
93-
"Creating your instance...",
94-
);
95-
if (newInstance) {
96-
silent || utils.logLabeledBullet("dataconnect", "Instance created");
97-
connectionName = newInstance?.connectionName || "";
98-
} else {
99-
silent ||
100-
utils.logLabeledBullet(
101-
"dataconnect",
102-
"Cloud SQL instance creation started - it should be ready shortly. Database and users will be created on your next deploy.",
103-
);
104-
return connectionName;
105-
}
106-
} catch (err: any) {
107-
if (await isFreeTrialError(err, projectId)) {
108-
const freeTrialInstanceId = await getFreeTrialInstanceId(projectId);
109-
printFreeTrialUnavailable(projectId, configYamlPath, freeTrialInstanceId);
110-
throw new FirebaseError("No-cost Cloud SQL trial has already been used on this project.");
111-
}
112-
throw err;
79+
const newInstance = await promiseWithSpinner(
80+
() =>
81+
cloudSqlAdminClient.createInstance({
82+
projectId,
83+
location,
84+
instanceId,
85+
enableGoogleMlIntegration,
86+
waitForCreation,
87+
freeTrial: !freeTrialUsed,
88+
}),
89+
"Creating your instance...",
90+
);
91+
if (newInstance) {
92+
silent || utils.logLabeledBullet("dataconnect", "Instance created");
93+
connectionName = newInstance?.connectionName || "";
94+
} else {
95+
silent ||
96+
utils.logLabeledBullet(
97+
"dataconnect",
98+
"Cloud SQL instance creation started - it should be ready shortly. Database and users will be created on your next deploy.",
99+
);
100+
return connectionName;
113101
}
114102
}
115103
}
@@ -186,7 +174,7 @@ export function getUpdateReason(instance: Instance, requireGoogleMlIntegration:
186174

187175
function cmekWarning() {
188176
const message =
189-
"The no-cost Cloud SQL trial instance does not support customer managed encryption keys.\n" +
177+
"Cloud SQL instances created via the Firebase CLI do not support customer managed encryption keys.\n" +
190178
"If you'd like to use a CMEK to encrypt your data, first create a CMEK encrypted instance (https://cloud.google.com/sql/docs/postgres/configure-cmek#createcmekinstance).\n" +
191179
"Then, edit your `dataconnect.yaml` file to use the encrypted instance and redeploy.";
192180
utils.logLabeledWarning("dataconnect", message);

src/deploy/dataconnect/deploy.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { parseServiceName } from "../../dataconnect/names";
88
import { ResourceFilter } from "../../dataconnect/filters";
99
import { vertexAIOrigin } from "../../api";
1010
import * as ensureApiEnabled from "../../ensureApiEnabled";
11-
import { join } from "node:path";
1211
import { confirm } from "../../prompt";
1312

1413
/**
@@ -94,10 +93,9 @@ export default async function (
9493
const enableGoogleMlIntegration = requiresVector(s.deploymentMetadata);
9594
return provisionCloudSql({
9695
projectId,
97-
locationId: parseServiceName(s.serviceName).location,
96+
location: parseServiceName(s.serviceName).location,
9897
instanceId,
9998
databaseId,
100-
configYamlPath: join(s.sourceDirectory, "dataconnect.yaml"),
10199
enableGoogleMlIntegration,
102100
waitForCreation: true,
103101
});

src/deploy/dataconnect/prepare.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { parseServiceName } from "../../dataconnect/names";
1717
import { FirebaseError } from "../../error";
1818
import { requiresVector } from "../../dataconnect/types";
1919
import { diffSchema } from "../../dataconnect/schemaMigration";
20-
import { join } from "node:path";
2120
import { upgradeInstructions } from "../../dataconnect/freeTrial";
2221

2322
/**
@@ -90,10 +89,9 @@ export default async function (context: any, options: DeployOptions): Promise<vo
9089
const enableGoogleMlIntegration = requiresVector(s.deploymentMetadata);
9190
return provisionCloudSql({
9291
projectId,
93-
locationId: parseServiceName(s.serviceName).location,
92+
location: parseServiceName(s.serviceName).location,
9493
instanceId,
9594
databaseId,
96-
configYamlPath: join(s.sourceDirectory, "dataconnect.yaml"),
9795
enableGoogleMlIntegration,
9896
waitForCreation: true,
9997
dryRun: options.dryRun,

src/gcp/cloudsql/cloudsqladmin.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,33 +58,34 @@ export function instanceConsoleLink(projectId: string, instanceId: string) {
5858
return `https://console.cloud.google.com/sql/instances/${instanceId}/overview?project=${projectId}`;
5959
}
6060

61-
export async function createInstance(
62-
projectId: string,
63-
location: string,
64-
instanceId: string,
65-
enableGoogleMlIntegration: boolean,
66-
waitForCreation: boolean,
67-
): Promise<Instance | undefined> {
61+
export async function createInstance(args: {
62+
projectId: string;
63+
location: string;
64+
instanceId: string;
65+
enableGoogleMlIntegration: boolean;
66+
waitForCreation: boolean;
67+
freeTrial: boolean;
68+
}): Promise<Instance | undefined> {
6869
const databaseFlags = [{ name: "cloudsql.iam_authentication", value: "on" }];
69-
if (enableGoogleMlIntegration) {
70+
if (args.enableGoogleMlIntegration) {
7071
databaseFlags.push({ name: "cloudsql.enable_google_ml_integration", value: "on" });
7172
}
7273
let op: ClientResponse<Operation>;
7374
try {
74-
op = await client.post<Partial<Instance>, Operation>(`projects/${projectId}/instances`, {
75-
name: instanceId,
76-
region: location,
75+
op = await client.post<Partial<Instance>, Operation>(`projects/${args.projectId}/instances`, {
76+
name: args.instanceId,
77+
region: args.location,
7778
databaseVersion: "POSTGRES_15",
7879
settings: {
7980
tier: "db-f1-micro",
8081
edition: "ENTERPRISE",
8182
ipConfiguration: {
8283
authorizedNetworks: [],
8384
},
84-
enableGoogleMlIntegration,
85+
enableGoogleMlIntegration: args.enableGoogleMlIntegration,
8586
databaseFlags,
8687
storageAutoResize: false,
87-
userLabels: { "firebase-data-connect": "ft" },
88+
userLabels: { "firebase-data-connect": args.freeTrial ? "ft" : "nt" },
8889
insightsConfig: {
8990
queryInsightsEnabled: true,
9091
queryPlansPerMinute: 5, // Match the default settings
@@ -93,13 +94,13 @@ export async function createInstance(
9394
},
9495
});
9596
} catch (err: any) {
96-
handleAllowlistError(err, location);
97+
handleAllowlistError(err, args.location);
9798
throw err;
9899
}
99-
if (!waitForCreation) {
100+
if (!args.waitForCreation) {
100101
return;
101102
}
102-
const opName = `projects/${projectId}/operations/${op.body.name}`;
103+
const opName = `projects/${args.projectId}/operations/${op.body.name}`;
103104
const pollRes = await operationPoller.pollOperation<Instance>({
104105
apiOrigin: cloudSQLAdminOrigin(),
105106
apiVersion: API_VERSION,

src/init/features/dataconnect/index.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,14 @@ async function askQuestions(setup: Setup, isBillingEnabled: boolean): Promise<Re
124124
info.locationId === "" ||
125125
info.cloudSqlDatabase === "";
126126
const shouldConfigureBackend =
127-
isBillingEnabled && requiredConfigUnset
128-
? await confirm({
129-
message: `Would you like to configure your backend resources now?`,
130-
// For Blaze Projects, configure Cloud SQL by default.
131-
// TODO: For Spark projects, allow them to configure Cloud SQL but deploy as unlinked Postgres.
132-
default: true,
133-
})
134-
: false;
127+
isBillingEnabled &&
128+
requiredConfigUnset &&
129+
(await confirm({
130+
message: `Would you like to configure your backend resources now?`,
131+
// For Blaze Projects, configure Cloud SQL by default.
132+
// TODO: For Spark projects, allow them to configure Cloud SQL but deploy as unlinked Postgres.
133+
default: true,
134+
}));
135135
if (shouldConfigureBackend) {
136136
info = await promptForService(info);
137137
info = await promptForCloudSQL(setup, info);
@@ -165,10 +165,9 @@ export async function actuate(setup: Setup, config: Config, info: RequiredInfo)
165165
if (setup.projectId && info.shouldProvisionCSQL) {
166166
await provisionCloudSql({
167167
projectId: setup.projectId,
168-
locationId: info.locationId,
168+
location: info.locationId,
169169
instanceId: info.cloudSqlInstanceId,
170170
databaseId: info.cloudSqlDatabase,
171-
configYamlPath: join(config.get("dataconnect.source"), "dataconnect.yaml"),
172171
enableGoogleMlIntegration: false,
173172
waitForCreation: false,
174173
});
@@ -370,6 +369,8 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<Requ
370369
if (choices.length) {
371370
if (!(await checkFreeTrialInstanceUsed(setup.projectId))) {
372371
choices.push({ name: "Create a new free trial instance", value: "", location: "" });
372+
} else {
373+
choices.push({ name: "Create a new CloudSQL instance", value: "", location: "" });
373374
}
374375
info.cloudSqlInstanceId = await promptOnce({
375376
message: `Which CloudSQL instance would you like to use?`,

0 commit comments

Comments
 (0)