Skip to content

Commit

Permalink
Merge branch 'master' into if-extsdk-2
Browse files Browse the repository at this point in the history
  • Loading branch information
ifielker committed Sep 20, 2024
2 parents 1e2fb16 + 3ac053f commit d6795e6
Show file tree
Hide file tree
Showing 24 changed files with 276 additions and 145 deletions.
9 changes: 1 addition & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,2 @@
- Added support for non-default Firestore databases in `firebase init`. (#7655)
- Updated supported range for Angular framework. (#7418)
- Fixed (Angular 17+) temporary change the PORT in Angular server.ts (#6651)
- Switched Data Connect API from `v1alpha` to `v1beta`. (#7589)
- Improved handling of Spark projects in `firebase init dataconnect`. (#7666)
- Updated Firebase Data Connect local toolkit version to v1.3.7, which adds support for `v1beta` gRPC APIs and the `OrderDirection` enum in Swift, and makes transactional queries and mutations opt-in with the `@transaction` directive. (#7679)
- Add dataconnect SQL grant command `firebase dataconnect:sql:grant -R <role> -E email`. (#7656)
- Updated Firebase Data Connect local toolkit version to v1.3.8, which adds support for `Any` scalar values in the Android SDK, strongly-typed variables in `@auth` expressions, and a minor internal fix for the Dart SDK code generation. (#7696)
- Added new command `firebase ext:sdk:install` to allow you to configure extensions in a functions codebase. See https://firebase.google.com/docs/extensions/install-extensions?interface=sdk for details.(#7581)
- Added better handling for paths with spaces in `getGlobalEsbuildVersion` function. (#7571)
4 changes: 4 additions & 0 deletions firebase-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## NEXT

## 0.9.0

- Updated internal firebase-tools dependency to 13.18.0

## 0.8.0

- Updated internal firebase-tools dependency to 13.17.0
Expand Down
4 changes: 2 additions & 2 deletions firebase-vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion firebase-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"publisher": "firebase",
"icon": "./resources/firebase_logo.png",
"description": "VSCode Extension for Firebase",
"version": "0.8.0",
"version": "0.9.0",
"engines": {
"vscode": "^1.69.0"
},
Expand Down
4 changes: 2 additions & 2 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firebase-tools",
"version": "13.17.0",
"version": "13.18.0",
"description": "Command-Line Interface for Firebase",
"main": "./lib/index.js",
"bin": {
Expand Down
5 changes: 5 additions & 0 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export const command = new Command("deploy")
'(e.g. "--only dataconnect:serviceId,dataconnect:serviceId:connectorId,dataconnect:serviceId:schema"). ',
)
.option("--except <targets>", 'deploy to all targets except specified (e.g. "database")')
.option(
"--dry-run",
"Perform a dry run of your deployment. Validates your changes and builds your code without deploying any changes to your project. " +
"In order to provide better validation, this may still enable APIs on the target project.",
)
.before(requireConfig)
.before((options) => {
options.filteredTargets = filterTargets(options, VALID_DEPLOY_TARGETS);
Expand Down
104 changes: 61 additions & 43 deletions src/dataconnect/provisionCloudSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export async function provisionCloudSql(args: {
enableGoogleMlIntegration: boolean;
waitForCreation: boolean;
silent?: boolean;
dryRun?: boolean;
}): Promise<string> {
let connectionName = ""; // Not used yet, will be used for schema migration
const {
Expand All @@ -32,29 +33,33 @@ export async function provisionCloudSql(args: {
enableGoogleMlIntegration,
waitForCreation,
silent,
dryRun,
} = args;
try {
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
silent || utils.logLabeledBullet("dataconnect", `Found existing instance ${instanceId}.`);
connectionName = existingInstance?.connectionName || "";
const why = getUpdateReason(existingInstance, enableGoogleMlIntegration);
if (why) {
const cta = dryRun
? `It will be updated on your next deploy.`
: `Updating instance. This may take a few minutes...`;
silent ||
utils.logLabeledBullet(
"dataconnect",
`Instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
`Updating instance. This may take a few minutes...` +
why,
`Instance ${instanceId} settings not compatible with Firebase Data Connect. ` + cta + why,
);
await promiseWithSpinner(
() =>
cloudSqlAdminClient.updateInstanceForDataConnect(
existingInstance,
enableGoogleMlIntegration,
),
"Updating your instance...",
);
silent || utils.logLabeledBullet("dataconnect", "Instance updated");
if (!dryRun) {
await promiseWithSpinner(
() =>
cloudSqlAdminClient.updateInstanceForDataConnect(
existingInstance,
enableGoogleMlIntegration,
),
"Updating your instance...",
);
silent || utils.logLabeledBullet("dataconnect", "Instance updated");
}
}
} catch (err: any) {
// We only should catch NOT FOUND errors
Expand All @@ -66,57 +71,70 @@ export async function provisionCloudSql(args: {
printFreeTrialUnavailable(projectId, freeTrialInstanceId);
throw new FirebaseError("Free trial unavailable.");
}
const cta = dryRun ? "It will be created on your next deploy" : "Creating it now.";
silent ||
utils.logLabeledBullet(
"dataconnect",
`CloudSQL instance '${instanceId}' not found, creating it.` +
`CloudSQL instance '${instanceId}' not found.` +
cta +
`\nThis instance is provided under the terms of the Data Connect free trial ${freeTrialTermsLink()}` +
`\nMonitor the progress at ${cloudSqlAdminClient.instanceConsoleLink(projectId, instanceId)}`,
);
const newInstance = await promiseWithSpinner(
() =>
cloudSqlAdminClient.createInstance(
projectId,
locationId,
instanceId,
enableGoogleMlIntegration,
waitForCreation,
),
"Creating your instance...",
);
if (newInstance) {
silent || utils.logLabeledBullet("dataconnect", "Instance created");
connectionName = newInstance?.connectionName || "";
} else {
silent ||
utils.logLabeledBullet(
"dataconnect",
"Cloud SQL instance creation started - it should be ready shortly. Database and users will be created on your next deploy.",
);
return connectionName;
if (!dryRun) {
const newInstance = await promiseWithSpinner(
() =>
cloudSqlAdminClient.createInstance(
projectId,
locationId,
instanceId,
enableGoogleMlIntegration,
waitForCreation,
),
"Creating your instance...",
);
if (newInstance) {
silent || utils.logLabeledBullet("dataconnect", "Instance created");
connectionName = newInstance?.connectionName || "";
} else {
silent ||
utils.logLabeledBullet(
"dataconnect",
"Cloud SQL instance creation started - it should be ready shortly. Database and users will be created on your next deploy.",
);
return connectionName;
}
}
}

try {
await cloudSqlAdminClient.getDatabase(projectId, instanceId, databaseId);
silent || utils.logLabeledBullet("dataconnect", `Found existing database ${databaseId}.`);
} catch (err: any) {
if (err.status === 404) {
// Create the database if not found.
silent ||
utils.logLabeledBullet(
"dataconnect",
`Database ${databaseId} not found, creating it now...`,
);
await cloudSqlAdminClient.createDatabase(projectId, instanceId, databaseId);
silent || utils.logLabeledBullet("dataconnect", `Database ${databaseId} created.`);
if (dryRun) {
silent ||
utils.logLabeledBullet(
"dataconnect",
`Database ${databaseId} not found. It will be created on your next deploy.`,
);
} else {
// Create the database if not found.
silent ||
utils.logLabeledBullet(
"dataconnect",
`Database ${databaseId} not found, creating it now...`,
);
await cloudSqlAdminClient.createDatabase(projectId, instanceId, databaseId);
silent || utils.logLabeledBullet("dataconnect", `Database ${databaseId} created.`);
}
} else {
// Skip it if the database is not accessible.
// Possible that the CSQL instance is in the middle of something.
logger.debug(`Unexpected error from CloudSQL: ${err}`);
silent || utils.logLabeledWarning("dataconnect", `Database ${databaseId} is not accessible.`);
}
}
if (enableGoogleMlIntegration) {
if (enableGoogleMlIntegration && !dryRun) {
await grantRolesToCloudSqlServiceAccount(projectId, instanceId, [GOOGLE_ML_INTEGRATION_ROLE]);
}
return connectionName;
Expand Down
24 changes: 11 additions & 13 deletions src/dataconnect/schemaMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,10 @@ async function promptForSchemaMigration(
}
// `firebase deploy` and `firebase dataconnect:sql:migrate` always prompt for any SQL migration changes.
// Destructive migrations are too potentially dangerous to not prompt for with --force
const message =
validationMode === "STRICT_AFTER_COMPATIBLE"
? `Would you like to execute these optional changes against ${databaseId} in your CloudSQL instance ${instanceName}?`
: `Would you like to execute these changes against ${databaseId} in your CloudSQL instance ${instanceName}?`;
let executeChangePrompt = "Execute changes";
if (validationMode === "STRICT_AFTER_COMPATIBLE") {
executeChangePrompt = "Execute optional changes";
Expand All @@ -442,7 +446,7 @@ async function promptForSchemaMigration(
];
const defaultValue = validationMode === "STRICT_AFTER_COMPATIBLE" ? "none" : "all";
return await promptOnce({
message: `Would you like to execute these changes against ${databaseId}?`,
message: message,
type: "list",
choices,
default: defaultValue,
Expand Down Expand Up @@ -478,14 +482,8 @@ async function promptForInvalidConnectorError(
}
displayInvalidConnectors(invalidConnectors);
if (validateOnly) {
if (options.force) {
// `firebase dataconnect:sql:migrate --force` ignores invalid connectors.
return false;
}
// `firebase dataconnect:sql:migrate` aborts if there are invalid connectors.
throw new FirebaseError(
`Command aborted. If you'd like to migrate it anyway, you may override with --force.`,
);
// `firebase dataconnect:sql:migrate` ignores invalid connectors.
return false;
}
if (options.force) {
// `firebase deploy --force` will delete invalid connectors without prompting.
Expand Down Expand Up @@ -601,11 +599,11 @@ function displaySchemaChanges(
let message;
if (validationMode === "COMPATIBLE") {
message =
"Your new application schema is incompatible with the schema of your PostgreSQL database " +
"Your PostgreSQL database " +
databaseId +
" in your CloudSQL instance " +
instanceName +
". " +
" must be migrated in order to be compatible with your application schema. " +
"The following SQL statements will migrate your database schema to be compatible with your new Data Connect schema.\n" +
error.diffs.map(toString).join("\n");
} else if (validationMode === "STRICT_AFTER_COMPATIBLE") {
Expand All @@ -619,11 +617,11 @@ function displaySchemaChanges(
error.diffs.map(toString).join("\n");
} else {
message =
"Your new application schema does not match the schema of your PostgreSQL database " +
"Your PostgreSQL database " +
databaseId +
" in your CloudSQL instance " +
instanceName +
". " +
" must be migrated in order to match your application schema. " +
"The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
error.diffs.map(toString).join("\n");
}
Expand Down
4 changes: 2 additions & 2 deletions src/deploy/database/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { FirebaseError } from "../../error";
import { parseBoltRules } from "../../parseBoltRules";
import * as rtdb from "../../rtdb";
import * as utils from "../../utils";
import { Options } from "../../options";
import * as dbRulesConfig from "../../database/rulesConfig";
import { DeployOptions } from "..";

export function prepare(context: any, options: Options): Promise<any> {
export function prepare(context: any, options: DeployOptions): Promise<any> {
const rulesConfig = dbRulesConfig.getRulesConfig(context.projectId, options);
const next = Promise.resolve();

Expand Down
43 changes: 41 additions & 2 deletions src/deploy/dataconnect/prepare.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as clc from "colorette";

import { Options } from "../../options";
import { DeployOptions } from "../";
import { load } from "../../dataconnect/load";
import { readFirebaseJson } from "../../dataconnect/fileUtils";
import { logger } from "../../logger";
Expand All @@ -11,14 +11,18 @@ import { build } from "../../dataconnect/build";
import { ensureApis } from "../../dataconnect/ensureApis";
import { requireTosAcceptance } from "../../requireTosAcceptance";
import { DATA_CONNECT_TOS_ID } from "../../gcp/firedata";
import { provisionCloudSql } from "../../dataconnect/provisionCloudSql";
import { parseServiceName } from "../../dataconnect/names";
import { FirebaseError } from "../../error";
import { requiresVector } from "../../dataconnect/types";
import { diffSchema } from "../../dataconnect/schemaMigration";

/**
* Prepares for a Firebase DataConnect deployment by loading schemas and connectors from file.
* @param context The deploy context.
* @param options The CLI options object.
*/
export default async function (context: any, options: Options): Promise<void> {
export default async function (context: any, options: DeployOptions): Promise<void> {
const projectId = needProjectId(options);
await ensureApis(projectId);
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
Expand Down Expand Up @@ -55,6 +59,41 @@ export default async function (context: any, options: Options): Promise<void> {
filters,
};
utils.logLabeledBullet("dataconnect", `Successfully prepared schema and connectors`);
if (options.dryRun) {
for (const si of serviceInfos) {
await diffSchema(
si.schema,
si.dataConnectYaml.schema.datasource.postgresql?.schemaValidation,
);
}
utils.logLabeledBullet("dataconnect", "Checking for CloudSQL resources...");
await Promise.all(
serviceInfos
.filter((si) => {
return !filters || filters?.some((f) => si.dataConnectYaml.serviceId === f.serviceId);
})
.map(async (s) => {
const postgresDatasource = s.schema.datasources.find((d) => d.postgresql);
if (postgresDatasource) {
const instanceId = postgresDatasource.postgresql?.cloudSql.instance.split("/").pop();
const databaseId = postgresDatasource.postgresql?.database;
if (!instanceId || !databaseId) {
return Promise.resolve();
}
const enableGoogleMlIntegration = requiresVector(s.deploymentMetadata);
return provisionCloudSql({
projectId,
locationId: parseServiceName(s.serviceName).location,
instanceId,
databaseId,
enableGoogleMlIntegration,
waitForCreation: true,
dryRun: options.dryRun,
});
}
}),
);
}
logger.debug(JSON.stringify(context.dataconnect, null, 2));
return;
}
Loading

0 comments on commit d6795e6

Please sign in to comment.