Skip to content

Commit 926f71b

Browse files
fredzqmyuchenshi
andauthored
[FDC] Support private IP Cloud SQL with VPC (#9200)
* allow private IP * m * remove log * changelog * comments * m * bold Cloud SQL instance name * m * m * m * m * m * m * m * m * m * m * m * timeout * clean * clean * clean * clean * Update CHANGELOG.md Co-authored-by: Yuchen Shi <[email protected]> --------- Co-authored-by: Yuchen Shi <[email protected]>
1 parent 76e0c4d commit 926f71b

File tree

6 files changed

+46
-41
lines changed

6 files changed

+46
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
- `firebase_update_environment` MCP tool supports accepting Gemini in Firebase Terms of Service.
44
- Fixed a bug when `firebase init dataconnect` failed to create a React app when launched from VS Code extension (#9171).
55
- Improved the clarity of the `firebase apptesting:execute` command when you have zero or multiple apps.
6+
- `firebase dataconnect:sql:migrate` now supports Cloud SQL instances with only private IPs. The command must be run in the same VPC of the instance to work. (##9200)

src/dataconnect/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export async function upsertSchema(
140140
apiOrigin: dataconnectOrigin(),
141141
apiVersion: DATACONNECT_API_VERSION,
142142
operationResourceName: op.body.name,
143-
masterTimeout: 120000,
143+
masterTimeout: 10000,
144144
});
145145
}
146146

src/dataconnect/provisionCloudSql.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
22
import * as utils from "../utils";
3+
import * as clc from "colorette";
34
import { grantRolesToCloudSqlServiceAccount } from "./checkIam";
45
import { Instance } from "../gcp/cloudsql/types";
56
import { promiseWithSpinner } from "../utils";
@@ -35,20 +36,23 @@ async function upsertInstance(args: {
3536
const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args;
3637
try {
3738
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
38-
utils.logLabeledBullet("dataconnect", `Found existing Cloud SQL instance ${instanceId}.`);
39+
utils.logLabeledBullet(
40+
"dataconnect",
41+
`Found existing Cloud SQL instance ${clc.bold(instanceId)}.`,
42+
);
3943
const why = getUpdateReason(existingInstance, requireGoogleMlIntegration);
4044
if (why) {
4145
if (dryRun) {
4246
utils.logLabeledBullet(
4347
"dataconnect",
44-
`Cloud SQL instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
48+
`Cloud SQL instance ${clc.bold(instanceId)} settings are not compatible with Firebase Data Connect. ` +
4549
`It will be updated on your next deploy.` +
4650
why,
4751
);
4852
} else {
4953
utils.logLabeledBullet(
5054
"dataconnect",
51-
`Cloud SQL instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
55+
`Cloud SQL instance ${clc.bold(instanceId)} settings are not compatible with Firebase Data Connect. ` +
5256
why,
5357
);
5458
await promiseWithSpinner(
@@ -83,7 +87,7 @@ async function createInstance(args: {
8387
if (dryRun) {
8488
utils.logLabeledBullet(
8589
"dataconnect",
86-
`Cloud SQL Instance ${instanceId} not found. It will be created on your next deploy.`,
90+
`Cloud SQL Instance ${clc.bold(instanceId)} not found. It will be created on your next deploy.`,
8791
);
8892
} else {
8993
await cloudSqlAdminClient.createInstance({
@@ -109,7 +113,7 @@ export function cloudSQLBeingCreated(
109113
includeFreeTrialToS?: boolean,
110114
): string {
111115
return (
112-
`Cloud SQL Instance ${instanceId} is being created.` +
116+
`Cloud SQL Instance ${clc.bold(instanceId)} is being created.` +
113117
(includeFreeTrialToS
114118
? `\nThis instance is provided under the terms of the Data Connect no-cost trial ${freeTrialTermsLink()}`
115119
: "") +
@@ -157,9 +161,19 @@ async function upsertDatabase(args: {
157161
export function getUpdateReason(instance: Instance, requireGoogleMlIntegration: boolean): string {
158162
let reason = "";
159163
const settings = instance.settings;
160-
// Cloud SQL instances must have public IP enabled to be used with Firebase Data Connect.
161164
if (!settings.ipConfiguration?.ipv4Enabled) {
162-
reason += "\n - to enable public IP.";
165+
utils.logLabeledWarning(
166+
"dataconnect",
167+
`Cloud SQL instance ${clc.bold(instance.name)} does not have a public IP.
168+
${clc.bold("firebase dataconnect:sql:migrate")} will only work within its VPC (e.g. GCE, GKE).`,
169+
);
170+
if (
171+
settings.ipConfiguration?.privateNetwork &&
172+
!settings.ipConfiguration?.enablePrivatePathForGoogleCloudServices
173+
) {
174+
// Cloud SQL instances with only private IP must enable PSC for Data Connect backend to connect to it.
175+
reason += "\n - to enable Private Path for Google Cloud Services.";
176+
}
163177
}
164178

165179
if (requireGoogleMlIntegration) {

src/gcp/cloudsql/cloudsqladmin.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Client } from "../../apiv2";
22
import { cloudSQLAdminOrigin } from "../../api";
3+
import * as clc from "colorette";
34
import * as operationPoller from "../../operation-poller";
45
import { Instance, Database, User, UserType, DatabaseFlag } from "./types";
56
import { needProjectId } from "../../projectUtils";
@@ -47,7 +48,7 @@ export async function getInstance(projectId: string, instanceId: string): Promis
4748
const res = await client.get<Instance>(`projects/${projectId}/instances/${instanceId}`);
4849
if (res.body.state === "FAILED") {
4950
throw new FirebaseError(
50-
`Cloud SQL instance ${instanceId} is in a failed state.\nGo to ${instanceConsoleLink(projectId, instanceId)} to repair or delete it.`,
51+
`Cloud SQL instance ${clc.bold(instanceId)} is in a failed state.\nGo to ${instanceConsoleLink(projectId, instanceId)} to repair or delete it.`,
5152
);
5253
}
5354
return res.body;
@@ -121,7 +122,8 @@ export async function updateInstanceForDataConnect(
121122
{
122123
settings: {
123124
ipConfiguration: {
124-
ipv4Enabled: true,
125+
enablePrivatePathForGoogleCloudServices:
126+
!!instance?.settings?.ipConfiguration?.privateNetwork,
125127
},
126128
databaseFlags: dbFlags,
127129
enableGoogleMlIntegration,

src/gcp/cloudsql/connect.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,39 +34,21 @@ export async function execute(
3434
);
3535
}
3636
let connector: Connector;
37-
let pool: pg.Pool;
37+
let authType: AuthTypes;
3838
switch (user.type) {
3939
case "CLOUD_IAM_USER": {
4040
connector = new Connector({
4141
auth: new FBToolsAuthClient(),
4242
});
43-
const clientOpts = await connector.getOptions({
44-
instanceConnectionName: connectionName,
45-
ipType: IpAddressTypes.PUBLIC,
46-
authType: AuthTypes.IAM,
47-
});
48-
pool = new pg.Pool({
49-
...clientOpts,
50-
user: opts.username,
51-
database: opts.databaseId,
52-
});
43+
authType = AuthTypes.IAM;
5344
break;
5445
}
5546
case "CLOUD_IAM_SERVICE_ACCOUNT": {
5647
connector = new Connector();
48+
authType = AuthTypes.IAM;
5749
// Currently, this only works with Application Default credentials
5850
// https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/issues/61 is an open
5951
// FR to add support for OAuth2 tokens.
60-
const clientOpts = await connector.getOptions({
61-
instanceConnectionName: connectionName,
62-
ipType: IpAddressTypes.PUBLIC,
63-
authType: AuthTypes.IAM,
64-
});
65-
pool = new pg.Pool({
66-
...clientOpts,
67-
user: opts.username,
68-
database: opts.databaseId,
69-
});
7052
break;
7153
}
7254
default: {
@@ -77,19 +59,24 @@ export async function execute(
7759
connector = new Connector({
7860
auth: new FBToolsAuthClient(),
7961
});
80-
const clientOpts = await connector.getOptions({
81-
instanceConnectionName: connectionName,
82-
ipType: IpAddressTypes.PUBLIC,
83-
});
84-
pool = new pg.Pool({
85-
...clientOpts,
86-
user: opts.username,
87-
password: opts.password,
88-
database: opts.databaseId,
89-
});
62+
authType = AuthTypes.PASSWORD;
9063
break;
9164
}
9265
}
66+
const connectionOpts = {
67+
instanceConnectionName: connectionName,
68+
ipType: instance.ipAddresses.some((ip) => ip.type === "PRIMARY")
69+
? IpAddressTypes.PUBLIC
70+
: IpAddressTypes.PRIVATE,
71+
authType: authType,
72+
};
73+
const pool = new pg.Pool({
74+
...(await connector.getOptions(connectionOpts)),
75+
connectionTimeoutMillis: 1000,
76+
password: opts.password,
77+
user: opts.username,
78+
database: opts.databaseId,
79+
});
9380

9481
const cleanUpFn = async () => {
9582
conn.release();

src/gcp/cloudsql/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface IpConfiguration {
2323
allowedConsumerProjects: string[];
2424
pscEnabled: boolean;
2525
};
26+
enablePrivatePathForGoogleCloudServices?: boolean;
2627
}
2728

2829
export interface InstanceSettings {

0 commit comments

Comments
 (0)