Skip to content

Fdc brownfield setup #8150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 50 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
cf4feab
Helper functions and basic setup for dataconnect:sql:setup
tammam-g Jan 14, 2025
37bdfa5
Refactor setupIamUsers for better composability
tammam-g Jan 27, 2025
830e010
FDC MVP brownfield and greenfield to brownfield schema setup
tammam-g Jan 29, 2025
2f445be
Add required logic inside schemaMigration for handling brownfield
tammam-g Jan 29, 2025
ee20d66
Cleanup and fix bugs in brownfield setup
tammam-g Jan 30, 2025
f7edd16
Use firebasesuperuser instead of cloudsqlsuper user for brownfield mi…
tammam-g Jan 30, 2025
bf5c904
Add default permissions for brownfield
tammam-g Jan 31, 2025
d4d80e0
Fix lint/format
tammam-g Jan 31, 2025
4fce5d3
Refactor to allow setup reruns
tammam-g Jan 31, 2025
b1931e4
Fix small things and address comments
tammam-g Feb 12, 2025
e928a6b
Fix bug in role grants
tammam-g Feb 12, 2025
ff1b518
Add logging that database setup completed
tammam-g Feb 12, 2025
f8e4ec1
Make grant command not go through setup if roles can be granted in br…
tammam-g Feb 12, 2025
fc51dae
bug fix from changing the getting schema owner command
tammam-g Feb 12, 2025
f53e45b
Simplify getSchemaMetaData in permissions.ts
tammam-g Feb 12, 2025
61e66fc
Merge branch 'master' into fdc-brownfield-setup
tammam-g Feb 12, 2025
0c72a2b
Fix log statement
tammam-g Feb 12, 2025
17e3c58
Split permissions.ts into front facing permissions_setup.ts and keep …
tammam-g Feb 14, 2025
8e29299
No need to ask user if they want to rerun greenfield setup
tammam-g Feb 14, 2025
f52a6b1
Make setupSQLPermissions return a setup status instead of a boolean
tammam-g Feb 14, 2025
7038a28
Change an if statment to switch statement
tammam-g Feb 14, 2025
486ab5b
Keep upserting new user in grant command
tammam-g Feb 14, 2025
59e343a
Bump FDC local toolkit to v1.8.0. (#8210)
rosalyntan Feb 12, 2025
9209c10
First pass at auto generating sdk configs (#7833)
maneesht Feb 12, 2025
6f269bc
13.31.0
google-oss-bot Feb 12, 2025
405a876
[firebase-release] Removed change log and reset repo after 13.31.0 re…
google-oss-bot Feb 12, 2025
e717aca
FDC Emulator Update v1.8.1(#8216)
maneesht Feb 13, 2025
a5950df
13.31.1
google-oss-bot Feb 13, 2025
3ec0f2a
[firebase-release] Removed change log and reset repo after 13.31.1 re…
google-oss-bot Feb 13, 2025
543a785
Update formatting of connector evolution and insecure operation issue…
rosalyntan Feb 14, 2025
63e56d3
Use correct import path for data connect emulator (#8220)
joehan Feb 18, 2025
7b07e17
Don't surface insecure operations errors in VSCode. (#8215)
rosalyntan Feb 18, 2025
2052bc1
Add path information to formatted GraphqlError. (#8228)
rosalyntan Feb 18, 2025
d73ac95
App Hosting Emulator bug - apphosting emulator info is not complete w…
mathu97 Feb 19, 2025
409dde6
Bump FDC local toolkit to v1.8.2. (#8232)
rosalyntan Feb 19, 2025
6acc6ae
13.31.2
google-oss-bot Feb 19, 2025
e3a6e8c
[firebase-release] Removed change log and reset repo after 13.31.2 re…
google-oss-bot Feb 19, 2025
a958110
fix: #8168 - enforce webframeworks only when needed (#8169)
fivecar Feb 20, 2025
cf4f30e
Added env var to magically import data connect service from console (…
joehan Feb 20, 2025
0c009ea
Add initial delay when loading python functions (#8239)
taeold Feb 21, 2025
ddc0514
Update vscode to 0.13.1 (#8236)
hlshen Feb 24, 2025
8fcccce
Propagate overrides (#8253)
sarahec Feb 25, 2025
01bbf36
Print warning about --location removal from apphosting commands. (#8229)
annajowang Feb 25, 2025
f49703f
Fix issue where apps:init breaks on app creation (#8258)
maneesht Feb 26, 2025
c1aa045
Rename MetaData to Metadata
tammam-g Feb 26, 2025
8840d77
Change setup to set up in firebase error
tammam-g Feb 26, 2025
b5500fa
Improve logger message
tammam-g Feb 26, 2025
16d7144
Merge branch 'master' into fdc-brownfield-setup
tammam-g Mar 10, 2025
b962c71
Fix bugs in brownfield setup status checks
tammam-g Mar 10, 2025
916129b
fix lint issues
tammam-g Mar 10, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Fix webframeworks deployments when using `site` in `firebase.json`. (#8295)
- Add support for brownfield project onboard `dataconnect:sql:setup` (#8150)
2 changes: 1 addition & 1 deletion src/commands/dataconnect-sql-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { pickService } from "../dataconnect/fileUtils";
import { grantRoleToUserInSchema } from "../dataconnect/schemaMigration";
import { requireAuth } from "../requireAuth";
import { FirebaseError } from "../error";
import { fdcSqlRoleMap } from "../gcp/cloudsql/permissions";
import { fdcSqlRoleMap } from "../gcp/cloudsql/permissions_setup";

const allowedRoles = Object.keys(fdcSqlRoleMap);

Expand Down
38 changes: 38 additions & 0 deletions src/commands/dataconnect-sql-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Command } from "../command";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { pickService } from "../dataconnect/fileUtils";
import { FirebaseError } from "../error";
import { requireAuth } from "../requireAuth";
import { requirePermissions } from "../requirePermissions";
import { ensureApis } from "../dataconnect/ensureApis";
import { setupSQLPermissions, getSchemaMetadata } from "../gcp/cloudsql/permissions_setup";
import { DEFAULT_SCHEMA } from "../gcp/cloudsql/permissions";
import { getIdentifiers } from "../dataconnect/schemaMigration";

export const command = new Command("dataconnect:sql:setup [serviceId]")
.description("Setup your CloudSQL database")
.before(requirePermissions, [
"firebasedataconnect.services.list",
"firebasedataconnect.schemas.list",
"firebasedataconnect.schemas.update",
"cloudsql.instances.connect",
])
.before(requireAuth)
.action(async (serviceId: string, options: Options) => {
const projectId = needProjectId(options);
await ensureApis(projectId);
const serviceInfo = await pickService(projectId, options.config, serviceId);
const instanceId =
serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId;
if (!instanceId) {
throw new FirebaseError(
"dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId",
);
}

const { databaseId } = getIdentifiers(serviceInfo.schema);

const schemaInfo = await getSchemaMetadata(instanceId, databaseId, DEFAULT_SCHEMA, options);
await setupSQLPermissions(instanceId, databaseId, schemaInfo, options);
});
1 change: 1 addition & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
/**
* Loads all commands for our parser.
*/
export function load(client: any): any {

Check warning on line 5 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 5 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
function loadCommand(name: string) {

Check warning on line 6 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const t0 = process.hrtime.bigint();
const { command: cmd } = require(`./${name}`);

Check warning on line 8 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

Check warning on line 8 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Require statement not part of import statement
cmd.register(client);

Check warning on line 9 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .register on an `any` value

Check warning on line 9 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value
const t1 = process.hrtime.bigint();
const diffMS = (t1 - t0) / BigInt(1e6);
if (diffMS > 75) {
Expand All @@ -14,7 +14,7 @@
// console.error(`Loading ${name} took ${diffMS}ms`);
}

return cmd.runner();

Check warning on line 17 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe return of an `any` typed value

Check warning on line 17 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .runner on an `any` value

Check warning on line 17 in src/commands/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value
}

const t0 = process.hrtime.bigint();
Expand Down Expand Up @@ -221,6 +221,7 @@
client.dataconnect.services.list = loadCommand("dataconnect-services-list");
client.dataconnect.sql = {};
client.dataconnect.sql.diff = loadCommand("dataconnect-sql-diff");
client.dataconnect.sql.setup = loadCommand("dataconnect-sql-setup");
client.dataconnect.sql.migrate = loadCommand("dataconnect-sql-migrate");
client.dataconnect.sql.grant = loadCommand("dataconnect-sql-grant");
client.dataconnect.sql.shell = loadCommand("dataconnect-sql-shell");
Expand Down
64 changes: 51 additions & 13 deletions src/dataconnect/schemaMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,29 @@ import { format } from "sql-formatter";
import { IncompatibleSqlSchemaError, Diff, SCHEMA_ID, SchemaValidation } from "./types";
import { getSchema, upsertSchema, deleteConnector } from "./client";
import {
setupIAMUsers,
getIAMUser,
executeSqlCmdsAsIamUser,
executeSqlCmdsAsSuperUser,
toDatabaseUser,
} from "../gcp/cloudsql/connect";
import { needProjectId } from "../projectUtils";
import {
firebaseowner,
iamUserIsCSQLAdmin,
checkSQLRoleIsGranted,
fdcSqlRoleMap,
} from "../gcp/cloudsql/permissions";
import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
import { needProjectId } from "../projectUtils";
setupSQLPermissions,
getSchemaMetadata,
SchemaSetupStatus,
} from "../gcp/cloudsql/permissions_setup";
import { DEFAULT_SCHEMA, firebaseowner } from "../gcp/cloudsql/permissions";
import { promptOnce, confirm } from "../prompt";
import { logger } from "../logger";
import { Schema } from "./types";
import { Options } from "../options";
import { FirebaseError } from "../error";
import { logLabeledBullet, logLabeledWarning, logLabeledSuccess } from "../utils";
import { iamUserIsCSQLAdmin } from "../gcp/cloudsql/cloudsqladmin";
import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";

import * as errors from "./errors";

export async function diffSchema(
Expand Down Expand Up @@ -236,10 +239,34 @@ export async function grantRoleToUserInSchema(options: Options, schema: Schema)
);
}

// Run the database roles setup. This should be idempotent.
await setupIAMUsers(instanceId, databaseId, options);
// Make sure we have the right setup for the requested role grant.
const schemaInfo = await getSchemaMetadata(instanceId, databaseId, DEFAULT_SCHEMA, options);
let isGreenfieldSetup = schemaInfo.setupStatus === SchemaSetupStatus.GreenField;
switch (schemaInfo.setupStatus) {
case SchemaSetupStatus.NotSetup:
case SchemaSetupStatus.NotFound:
const newSetupStatus = await setupSQLPermissions(instanceId, databaseId, schemaInfo, options);
isGreenfieldSetup = newSetupStatus === SchemaSetupStatus.GreenField;
break;
default:
logger.info(
`Detected schema "${schemaInfo.name}" is setup in ${schemaInfo.setupStatus} mode. Skipping Setup.`,
);
break;
}

// Edge case: we can't grant firebase owner unless database is greenfield.
if (!isGreenfieldSetup && fdcSqlRole === firebaseowner(databaseId, DEFAULT_SCHEMA)) {
const newSetupStatus = await setupSQLPermissions(instanceId, databaseId, schemaInfo, options);

// Upsert user account into the database.
if (newSetupStatus !== SchemaSetupStatus.GreenField) {
throw new FirebaseError(
`Can't grant owner rule for brownfield databases. Consider fully migrating your database to FDC using 'firebase dataconnect:sql:setup'`,
);
}
}

// Upsert new user account into the database.
await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);

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

// TODO (tammam-g): at some point we would want to only run this after notifying the admin but
// until we confirm stability it's ok to run it on every migration by admin user.
if (userIsCSQLAdmin) {
await setupIAMUsers(instanceId, databaseId, options);
const schemaInfo = await getSchemaMetadata(instanceId, databaseId, DEFAULT_SCHEMA, options);
if (schemaInfo.setupStatus !== SchemaSetupStatus.GreenField) {
const newSetupStatus = await setupSQLPermissions(
instanceId,
databaseId,
schemaInfo,
options,
/* silent=*/ true,
);

if (newSetupStatus !== SchemaSetupStatus.GreenField) {
throw new FirebaseError(
`Can't migrate brownfield databases. Consider fully migrating your database to FDC using 'firebase dataconnect:sql:setup'`,
);
}
}

// Test if iam user has access to the roles required for this migration
Expand Down
22 changes: 22 additions & 0 deletions src/gcp/cloudsql/cloudsqladmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { Client, ClientResponse } from "../../apiv2";
import { cloudSQLAdminOrigin } from "../../api";
import * as operationPoller from "../../operation-poller";
import { Instance, Database, User, UserType, DatabaseFlag } from "./types";
import { needProjectId } from "../../projectUtils";
import { Options } from "../../options";
import { logger } from "../../logger";
import { testIamPermissions } from "../iam";
import { FirebaseError } from "../../error";
const API_VERSION = "v1";

Expand All @@ -16,6 +20,24 @@ interface Operation {
name: string;
}

export async function iamUserIsCSQLAdmin(options: Options): Promise<boolean> {
const projectId = needProjectId(options);
const requiredPermissions = [
"cloudsql.instances.connect",
"cloudsql.instances.get",
"cloudsql.users.create",
"cloudsql.users.update",
];

try {
const iamResult = await testIamPermissions(projectId, requiredPermissions);
return iamResult.passed;
} catch (err: any) {
logger.debug(`[iam] error while checking permissions, command may fail: ${err}`);
return false;
}
}

export async function listInstances(projectId: string): Promise<Instance[]> {
const res = await client.get<{ items: Instance[] }>(`projects/${projectId}/instances`);
return res.body.items ?? [];
Expand Down
27 changes: 7 additions & 20 deletions src/gcp/cloudsql/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { logger } from "../../logger";
import { FirebaseError } from "../../error";
import { Options } from "../../options";
import { FBToolsAuthClient } from "./fbToolsAuthClient";
import { setupSQLPermissions, firebaseowner, firebasewriter } from "./permissions";

export async function execute(
sqlStatements: string[],
Expand All @@ -23,7 +22,7 @@ export async function execute(
password?: string;
silent?: boolean;
},
) {
): Promise<pg.QueryResult[]> {
const logFn = opts.silent ? logger.debug : logger.info;
const instance = await cloudSqlAdminClient.getInstance(opts.projectId, opts.instanceId);
const user = await cloudSqlAdminClient.getUser(opts.projectId, opts.instanceId, opts.username);
Expand Down Expand Up @@ -92,11 +91,12 @@ export async function execute(
}

const conn = await pool.connect();
const results: pg.QueryResult[] = [];
logFn(`Logged in as ${opts.username}`);
for (const s of sqlStatements) {
logFn(`Executing: '${s}'`);
try {
await conn.query(s);
results.push(await conn.query(s));
} catch (err) {
throw new FirebaseError(`Error executing ${err}`);
}
Expand All @@ -105,6 +105,7 @@ export async function execute(
conn.release();
await pool.end();
connector.close();
return results;
}

export async function executeSqlCmdsAsIamUser(
Expand All @@ -113,7 +114,7 @@ export async function executeSqlCmdsAsIamUser(
databaseId: string,
cmds: string[],
silent = false,
): Promise<void> {
): Promise<pg.QueryResult[]> {
const projectId = needProjectId(options);
const { user: iamUser } = await getIAMUser(options);

Expand All @@ -135,7 +136,7 @@ export async function executeSqlCmdsAsSuperUser(
databaseId: string,
cmds: string[],
silent = false,
) {
): Promise<pg.QueryResult[]> {
const projectId = needProjectId(options);
// 1. Create a temporary builtin user
const superuser = "firebasesuperuser";
Expand All @@ -148,7 +149,7 @@ export async function executeSqlCmdsAsSuperUser(
temporaryPassword,
);

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

// 3. Setup FDC required SQL roles and permissions.
await setupSQLPermissions(instanceId, databaseId, options, true);

// 4. Apply necessary grants.
const grants = [
// Grant firebaseowner role to the current IAM user.
`GRANT "${firebaseowner(databaseId)}" TO "${user}"`,
// Grant firebaswriter to the FDC P4SA user
`GRANT "${firebasewriter(databaseId)}" TO "${fdcP4SAUser}"`,
];

await executeSqlCmdsAsSuperUser(options, instanceId, databaseId, grants, /** silent=*/ true);
return user;
}

Expand Down
Loading
Loading