Skip to content

Commit 1d45035

Browse files
tammam-grosalyntanmaneeshtjoehangoogle-oss-bot
authored
Fdc brownfield setup (#8150)
* Helper functions and basic setup for dataconnect:sql:setup * Refactor setupIamUsers for better composability * FDC MVP brownfield and greenfield to brownfield schema setup * Add required logic inside schemaMigration for handling brownfield * Cleanup and fix bugs in brownfield setup * Use firebasesuperuser instead of cloudsqlsuper user for brownfield migration success * Add default permissions for brownfield * Fix lint/format * Refactor to allow setup reruns * Fix small things and address comments * Fix bug in role grants * Add logging that database setup completed * Make grant command not go through setup if roles can be granted in brownfield * bug fix from changing the getting schema owner command * Simplify getSchemaMetaData in permissions.ts * Fix log statement * Split permissions.ts into front facing permissions_setup.ts and keep backend permissions there * No need to ask user if they want to rerun greenfield setup * Make setupSQLPermissions return a setup status instead of a boolean * Change an if statment to switch statement * Keep upserting new user in grant command * Bump FDC local toolkit to v1.8.0. (#8210) * Bump FDC local toolkit to v1.8.0. * Update changelog. * First pass at auto generating sdk configs (#7833) * First pass at auto generating sdk configs * Fixed formatting issues * Removed extra command * Deleted unnecessary files * Fixed more linting' * Removed test assertion * Fixed formatting * Updated erros * Misc * Updated platforms list * Undid last changes * Addressed comments * Fixed client test * Driveby type fixing * missed a spot * Fixed test * Fix issue where if a user passes in an empty 'out' parameter, the CLI crashes * Added intelligent sensing where app should be * Fixed formatting * Fixed lint * Fixed app dir * Misc * Wrote tests * Reverted apps sdkconfig changes * Fixed formatting * Small changes * Revert shrinkwrap changes * Updated test:management script * Fixed apps-sdkconfig boolean check * Fixed more boolean * Fixed formatting * Added changelog * Added new options * Removed unused var * Added experimental flag * Moved apps:init behind a flag * Added apps:init command * Removed unnecessary experiments * Addressed comments --------- Co-authored-by: Joe Hanley <[email protected]> * 13.31.0 * [firebase-release] Removed change log and reset repo after 13.31.0 release * FDC Emulator Update v1.8.1(#8216) * 13.31.1 * [firebase-release] Removed change log and reset repo after 13.31.1 release * Update formatting of connector evolution and insecure operation issues. (#8204) * Format INTERACTIVE_ACK issues as table as well and add extra "type" column to table. * Update warning and prompt wording to reflect insecure operations as well as connector evolution issues. * Wording. * Sort issues in table by category and some formatting fixes. * Use correct import path for data connect emulator (#8220) * Use correct import path for data connect emulator * Actually fix it this time * fix the thing i broke and format * Update src/emulator/dataconnect/pgliteServer.ts Co-authored-by: Maneesh Tewani <[email protected]> --------- Co-authored-by: Maneesh Tewani <[email protected]> * Don't surface insecure operations errors in VSCode. (#8215) * Don't surface connector evolution or insecure operation issues in VSCode.' * Fix * Filter by "INSECURE" substring rather than warningLevel. * Add path information to formatted GraphqlError. (#8228) * App Hosting Emulator bug - apphosting emulator info is not complete when env vars for emulators are set (#8231) * fix bug where apphosting emulator info is not complete when env vars for other emulators are set * add proper fix and test * fix * remove async from non-async test func * address comments * Bump FDC local toolkit to v1.8.2. (#8232) * Bump FDC local toolkit to v1.8.2. * Update changelog. * 13.31.2 * [firebase-release] Removed change log and reset repo after 13.31.2 release * fix: #8168 - enforce webframeworks only when needed (#8169) * fix: #8168 - enforce webframeworks only when needed In deployments where `--only hosting:boo` is used, enforce webframeworks enablement only when the target actually uses webframeworks. * remove console * add changelog, add tests * Add matchesHostingTarget to improve readability --------- Co-authored-by: Chalo Salvador <[email protected]> * Added env var to magically import data connect service from console (#8237) * Added env var to magically import data connect service from console * actually check location too * formats * Formats * Add initial delay when loading python functions (#8239) * Improve robustness of function discovery for python Anecdotally, python function discovery is flakey. We propose 2 change in this PR: 1. For python discovery, add a small initial delay for python's admin server to boot. 2. Add a request timeout to retry call to retrieve trigger information. Previously, the default timeout would've been set to OS-level TCP timeout, which in my laptop was between 20~30s. * Add changelog. * Remove per-req timeout to accomodate loading large/slow main.py. * Update changelog. * Revert timeout bump. * Update vscode to 0.13.1 (#8236) * update vscode to 0.13.1 * remove changelog line * Propagate overrides (#8253) * Apply ajv and ajv-formats overrides to shrinkwrap * Apply whatwg-url override to shrinkwrap * npm i to stabilize shrinkwrap --------- Co-authored-by: Joe Hanley <[email protected]> * Print warning about --location removal from apphosting commands. (#8229) `--location` will be removed from apphosting commands in the next major CLI release. Before then, the CLI will print a warning about this removal whenever `--location` is used. * Fix issue where apps:init breaks on app creation (#8258) * Rename MetaData to Metadata * Change setup to set up in firebase error * Improve logger message * Fix bugs in brownfield setup status checks * fix lint issues --------- Co-authored-by: Rosalyn Tan <[email protected]> Co-authored-by: Maneesh Tewani <[email protected]> Co-authored-by: Joe Hanley <[email protected]> Co-authored-by: Google Open Source Bot <[email protected]> Co-authored-by: Mathusan Selvarajah <[email protected]> Co-authored-by: Philip Su <[email protected]> Co-authored-by: Chalo Salvador <[email protected]> Co-authored-by: Daniel Lee <[email protected]> Co-authored-by: Harold Shen <[email protected]> Co-authored-by: Sarah Clark <[email protected]> Co-authored-by: annajowang <[email protected]>
1 parent 34674ac commit 1d45035

9 files changed

+567
-186
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
- Fix webframeworks deployments when using `site` in `firebase.json`. (#8295)
2+
- Add support for brownfield project onboard `dataconnect:sql:setup` (#8150)

src/commands/dataconnect-sql-grant.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { pickService } from "../dataconnect/fileUtils";
77
import { grantRoleToUserInSchema } from "../dataconnect/schemaMigration";
88
import { requireAuth } from "../requireAuth";
99
import { FirebaseError } from "../error";
10-
import { fdcSqlRoleMap } from "../gcp/cloudsql/permissions";
10+
import { fdcSqlRoleMap } from "../gcp/cloudsql/permissions_setup";
1111

1212
const allowedRoles = Object.keys(fdcSqlRoleMap);
1313

src/commands/dataconnect-sql-setup.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Command } from "../command";
2+
import { Options } from "../options";
3+
import { needProjectId } from "../projectUtils";
4+
import { pickService } from "../dataconnect/fileUtils";
5+
import { FirebaseError } from "../error";
6+
import { requireAuth } from "../requireAuth";
7+
import { requirePermissions } from "../requirePermissions";
8+
import { ensureApis } from "../dataconnect/ensureApis";
9+
import { setupSQLPermissions, getSchemaMetadata } from "../gcp/cloudsql/permissions_setup";
10+
import { DEFAULT_SCHEMA } from "../gcp/cloudsql/permissions";
11+
import { getIdentifiers } from "../dataconnect/schemaMigration";
12+
13+
export const command = new Command("dataconnect:sql:setup [serviceId]")
14+
.description("Setup your CloudSQL database")
15+
.before(requirePermissions, [
16+
"firebasedataconnect.services.list",
17+
"firebasedataconnect.schemas.list",
18+
"firebasedataconnect.schemas.update",
19+
"cloudsql.instances.connect",
20+
])
21+
.before(requireAuth)
22+
.action(async (serviceId: string, options: Options) => {
23+
const projectId = needProjectId(options);
24+
await ensureApis(projectId);
25+
const serviceInfo = await pickService(projectId, options.config, serviceId);
26+
const instanceId =
27+
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
28+
if (!instanceId) {
29+
throw new FirebaseError(
30+
"dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId",
31+
);
32+
}
33+
34+
const { databaseId } = getIdentifiers(serviceInfo.schema);
35+
36+
const schemaInfo = await getSchemaMetadata(instanceId, databaseId, DEFAULT_SCHEMA, options);
37+
await setupSQLPermissions(instanceId, databaseId, schemaInfo, options);
38+
});

src/commands/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export function load(client: any): any {
221221
client.dataconnect.services.list = loadCommand("dataconnect-services-list");
222222
client.dataconnect.sql = {};
223223
client.dataconnect.sql.diff = loadCommand("dataconnect-sql-diff");
224+
client.dataconnect.sql.setup = loadCommand("dataconnect-sql-setup");
224225
client.dataconnect.sql.migrate = loadCommand("dataconnect-sql-migrate");
225226
client.dataconnect.sql.grant = loadCommand("dataconnect-sql-grant");
226227
client.dataconnect.sql.shell = loadCommand("dataconnect-sql-shell");

src/dataconnect/schemaMigration.ts

+51-13
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,29 @@ import { format } from "sql-formatter";
44
import { IncompatibleSqlSchemaError, Diff, SCHEMA_ID, SchemaValidation } from "./types";
55
import { getSchema, upsertSchema, deleteConnector } from "./client";
66
import {
7-
setupIAMUsers,
87
getIAMUser,
98
executeSqlCmdsAsIamUser,
109
executeSqlCmdsAsSuperUser,
1110
toDatabaseUser,
1211
} from "../gcp/cloudsql/connect";
12+
import { needProjectId } from "../projectUtils";
1313
import {
14-
firebaseowner,
15-
iamUserIsCSQLAdmin,
1614
checkSQLRoleIsGranted,
1715
fdcSqlRoleMap,
18-
} from "../gcp/cloudsql/permissions";
19-
import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
20-
import { needProjectId } from "../projectUtils";
16+
setupSQLPermissions,
17+
getSchemaMetadata,
18+
SchemaSetupStatus,
19+
} from "../gcp/cloudsql/permissions_setup";
20+
import { DEFAULT_SCHEMA, firebaseowner } from "../gcp/cloudsql/permissions";
2121
import { promptOnce, confirm } from "../prompt";
2222
import { logger } from "../logger";
2323
import { Schema } from "./types";
2424
import { Options } from "../options";
2525
import { FirebaseError } from "../error";
2626
import { logLabeledBullet, logLabeledWarning, logLabeledSuccess } from "../utils";
27+
import { iamUserIsCSQLAdmin } from "../gcp/cloudsql/cloudsqladmin";
28+
import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
29+
2730
import * as errors from "./errors";
2831

2932
export async function diffSchema(
@@ -236,10 +239,34 @@ export async function grantRoleToUserInSchema(options: Options, schema: Schema)
236239
);
237240
}
238241

239-
// Run the database roles setup. This should be idempotent.
240-
await setupIAMUsers(instanceId, databaseId, options);
242+
// Make sure we have the right setup for the requested role grant.
243+
const schemaInfo = await getSchemaMetadata(instanceId, databaseId, DEFAULT_SCHEMA, options);
244+
let isGreenfieldSetup = schemaInfo.setupStatus === SchemaSetupStatus.GreenField;
245+
switch (schemaInfo.setupStatus) {
246+
case SchemaSetupStatus.NotSetup:
247+
case SchemaSetupStatus.NotFound:
248+
const newSetupStatus = await setupSQLPermissions(instanceId, databaseId, schemaInfo, options);
249+
isGreenfieldSetup = newSetupStatus === SchemaSetupStatus.GreenField;
250+
break;
251+
default:
252+
logger.info(
253+
`Detected schema "${schemaInfo.name}" is setup in ${schemaInfo.setupStatus} mode. Skipping Setup.`,
254+
);
255+
break;
256+
}
257+
258+
// Edge case: we can't grant firebase owner unless database is greenfield.
259+
if (!isGreenfieldSetup && fdcSqlRole === firebaseowner(databaseId, DEFAULT_SCHEMA)) {
260+
const newSetupStatus = await setupSQLPermissions(instanceId, databaseId, schemaInfo, options);
241261

242-
// Upsert user account into the database.
262+
if (newSetupStatus !== SchemaSetupStatus.GreenField) {
263+
throw new FirebaseError(
264+
`Can't grant owner rule for brownfield databases. Consider fully migrating your database to FDC using 'firebase dataconnect:sql:setup'`,
265+
);
266+
}
267+
}
268+
269+
// Upsert new user account into the database.
243270
await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);
244271

245272
// Grant the role to the user.
@@ -351,10 +378,21 @@ async function handleIncompatibleSchemaError(args: {
351378
${commandsToExecuteBySuperUser.join("\n")}`);
352379
}
353380

354-
// TODO (tammam-g): at some point we would want to only run this after notifying the admin but
355-
// until we confirm stability it's ok to run it on every migration by admin user.
356-
if (userIsCSQLAdmin) {
357-
await setupIAMUsers(instanceId, databaseId, options);
381+
const schemaInfo = await getSchemaMetadata(instanceId, databaseId, DEFAULT_SCHEMA, options);
382+
if (schemaInfo.setupStatus !== SchemaSetupStatus.GreenField) {
383+
const newSetupStatus = await setupSQLPermissions(
384+
instanceId,
385+
databaseId,
386+
schemaInfo,
387+
options,
388+
/* silent=*/ true,
389+
);
390+
391+
if (newSetupStatus !== SchemaSetupStatus.GreenField) {
392+
throw new FirebaseError(
393+
`Can't migrate brownfield databases. Consider fully migrating your database to FDC using 'firebase dataconnect:sql:setup'`,
394+
);
395+
}
358396
}
359397

360398
// Test if iam user has access to the roles required for this migration

src/gcp/cloudsql/cloudsqladmin.ts

+22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { Client, ClientResponse } from "../../apiv2";
22
import { cloudSQLAdminOrigin } from "../../api";
33
import * as operationPoller from "../../operation-poller";
44
import { Instance, Database, User, UserType, DatabaseFlag } from "./types";
5+
import { needProjectId } from "../../projectUtils";
6+
import { Options } from "../../options";
7+
import { logger } from "../../logger";
8+
import { testIamPermissions } from "../iam";
59
import { FirebaseError } from "../../error";
610
const API_VERSION = "v1";
711

@@ -16,6 +20,24 @@ interface Operation {
1620
name: string;
1721
}
1822

23+
export async function iamUserIsCSQLAdmin(options: Options): Promise<boolean> {
24+
const projectId = needProjectId(options);
25+
const requiredPermissions = [
26+
"cloudsql.instances.connect",
27+
"cloudsql.instances.get",
28+
"cloudsql.users.create",
29+
"cloudsql.users.update",
30+
];
31+
32+
try {
33+
const iamResult = await testIamPermissions(projectId, requiredPermissions);
34+
return iamResult.passed;
35+
} catch (err: any) {
36+
logger.debug(`[iam] error while checking permissions, command may fail: ${err}`);
37+
return false;
38+
}
39+
}
40+
1941
export async function listInstances(projectId: string): Promise<Instance[]> {
2042
const res = await client.get<{ items: Instance[] }>(`projects/${projectId}/instances`);
2143
return res.body.items ?? [];

src/gcp/cloudsql/connect.ts

+7-20
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { logger } from "../../logger";
1111
import { FirebaseError } from "../../error";
1212
import { Options } from "../../options";
1313
import { FBToolsAuthClient } from "./fbToolsAuthClient";
14-
import { setupSQLPermissions, firebaseowner, firebasewriter } from "./permissions";
1514

1615
export async function execute(
1716
sqlStatements: string[],
@@ -23,7 +22,7 @@ export async function execute(
2322
password?: string;
2423
silent?: boolean;
2524
},
26-
) {
25+
): Promise<pg.QueryResult[]> {
2726
const logFn = opts.silent ? logger.debug : logger.info;
2827
const instance = await cloudSqlAdminClient.getInstance(opts.projectId, opts.instanceId);
2928
const user = await cloudSqlAdminClient.getUser(opts.projectId, opts.instanceId, opts.username);
@@ -92,11 +91,12 @@ export async function execute(
9291
}
9392

9493
const conn = await pool.connect();
94+
const results: pg.QueryResult[] = [];
9595
logFn(`Logged in as ${opts.username}`);
9696
for (const s of sqlStatements) {
9797
logFn(`Executing: '${s}'`);
9898
try {
99-
await conn.query(s);
99+
results.push(await conn.query(s));
100100
} catch (err) {
101101
throw new FirebaseError(`Error executing ${err}`);
102102
}
@@ -105,6 +105,7 @@ export async function execute(
105105
conn.release();
106106
await pool.end();
107107
connector.close();
108+
return results;
108109
}
109110

110111
export async function executeSqlCmdsAsIamUser(
@@ -113,7 +114,7 @@ export async function executeSqlCmdsAsIamUser(
113114
databaseId: string,
114115
cmds: string[],
115116
silent = false,
116-
): Promise<void> {
117+
): Promise<pg.QueryResult[]> {
117118
const projectId = needProjectId(options);
118119
const { user: iamUser } = await getIAMUser(options);
119120

@@ -135,7 +136,7 @@ export async function executeSqlCmdsAsSuperUser(
135136
databaseId: string,
136137
cmds: string[],
137138
silent = false,
138-
) {
139+
): Promise<pg.QueryResult[]> {
139140
const projectId = needProjectId(options);
140141
// 1. Create a temporary builtin user
141142
const superuser = "firebasesuperuser";
@@ -148,7 +149,7 @@ export async function executeSqlCmdsAsSuperUser(
148149
temporaryPassword,
149150
);
150151

151-
return await execute([`SET ROLE = cloudsqlsuperuser`, ...cmds], {
152+
return await execute([`SET ROLE = '${superuser}'`, ...cmds], {
152153
projectId,
153154
instanceId,
154155
databaseId,
@@ -177,8 +178,6 @@ export async function getIAMUser(options: Options): Promise<{ user: string; mode
177178
// Steps:
178179
// 1. Create an IAM user for the current identity
179180
// 2. Create an IAM user for FDC P4SA
180-
// 3. Run setupSQLPermissions to setup the SQL database roles and permissions.
181-
// 4. Connect to the DB as the temporary user and run the necessary grants
182181
export async function setupIAMUsers(
183182
instanceId: string,
184183
databaseId: string,
@@ -200,18 +199,6 @@ export async function setupIAMUsers(
200199
);
201200
await cloudSqlAdminClient.createUser(projectId, instanceId, fdcP4SAmode, fdcP4SAUser);
202201

203-
// 3. Setup FDC required SQL roles and permissions.
204-
await setupSQLPermissions(instanceId, databaseId, options, true);
205-
206-
// 4. Apply necessary grants.
207-
const grants = [
208-
// Grant firebaseowner role to the current IAM user.
209-
`GRANT "${firebaseowner(databaseId)}" TO "${user}"`,
210-
// Grant firebaswriter to the FDC P4SA user
211-
`GRANT "${firebasewriter(databaseId)}" TO "${fdcP4SAUser}"`,
212-
];
213-
214-
await executeSqlCmdsAsSuperUser(options, instanceId, databaseId, grants, /** silent=*/ true);
215202
return user;
216203
}
217204

0 commit comments

Comments
 (0)