Skip to content
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

Error handling and logging when linking CSQL instance #7158

Merged
merged 14 commits into from
May 13, 2024
3 changes: 2 additions & 1 deletion src/dataconnect/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import { logger } from "../logger";

const DATACONNECT_API_VERSION = "v1alpha";
const dataconnectClient = () =>

Check warning on line 8 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
new Client({
urlPrefix: dataconnectOrigin(),
apiVersion: DATACONNECT_API_VERSION,
auth: true,
});

export async function listLocations(projectId: string): Promise<string[]> {

Check warning on line 15 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const res = await dataconnectClient().get<{
locations: {
name: string;
Expand All @@ -33,7 +33,7 @@
const locationServices = await listServices(projectId, l);
services = services.concat(locationServices);
} catch (err) {
logger.debug(`Unable to listServices in ${l}: ${err}`);

Check warning on line 36 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "unknown" of template literal expression
}
}),
);
Expand All @@ -41,7 +41,7 @@
return services;
}

export async function listServices(

Check warning on line 44 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
projectId: string,
locationId: string,
): Promise<types.Service[]> {
Expand All @@ -51,7 +51,7 @@
return res.body.services ?? [];
}

export async function createService(

Check warning on line 54 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
projectId: string,
locationId: string,
serviceId: string,
Expand All @@ -75,13 +75,14 @@
return pollRes;
}

export async function deleteService(

Check warning on line 78 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
projectId: string,
locationId: string,
serviceId: string,
): Promise<types.Service> {
// NOTE(fredzqm): Don't force delete yet. Backend would leave orphaned resources.
const op = await dataconnectClient().delete<types.Service>(
`projects/${projectId}/locations/${locationId}/services/${serviceId}?force=true`,
`projects/${projectId}/locations/${locationId}/services/${serviceId}`,
);
const pollRes = await operationPoller.pollOperation<types.Service>({
apiOrigin: dataconnectOrigin(),
Expand All @@ -93,16 +94,16 @@

/** Schema methods */

export async function getSchema(serviceName: string): Promise<types.Schema> {

Check warning on line 97 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const res = await dataconnectClient().get<types.Schema>(
`${serviceName}/schemas/${types.SCHEMA_ID}`,
);
return res.body;
}

export async function upsertSchema(

Check warning on line 104 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
schema: types.Schema,
validateOnly: boolean = false,

Check warning on line 106 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Type boolean trivially inferred from a boolean literal, remove type annotation
): Promise<types.Schema | undefined> {
const op = await dataconnectClient().patch<types.Schema, types.Schema>(`${schema.name}`, schema, {
queryParams: {
Expand All @@ -122,7 +123,7 @@

/** Connector methods */

export async function getConnector(name: string): Promise<types.Connector> {

Check warning on line 126 in src/dataconnect/client.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const res = await dataconnectClient().get<types.Connector>(name);
return res.body;
}
Expand Down
97 changes: 69 additions & 28 deletions src/dataconnect/schemaMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,40 @@ import { Schema } from "./types";
import { Options } from "../options";
import { FirebaseError } from "../error";
import { needProjectId } from "../projectUtils";
import { logLabeledWarning, logLabeledSuccess } from "../utils";
import { logLabeledBullet, logLabeledWarning, logLabeledSuccess } from "../utils";
import * as errors from "./errors";

export async function diffSchema(schema: Schema): Promise<Diff[]> {
const { serviceName, instanceName, databaseId } = getIdentifiers(schema);
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId);
await ensureServiceIsConnectedToCloudSql(
serviceName,
instanceName,
databaseId,
/* linkIfNotConnected=*/ false,
);
try {
await upsertSchema(schema, /** validateOnly=*/ true);
logLabeledSuccess("dataconnect", `Database schema is up to date.`);
} catch (err: any) {
if (err.status !== 400) {
throw err;
}
const invalidConnectors = errors.getInvalidConnectors(err);
const incompatible = errors.getIncompatibleSchemaError(err);
if (!incompatible && !invalidConnectors.length) {
// If we got a different type of error, throw it
throw err;
}

// Display failed precondition errors nicely.
if (invalidConnectors.length) {
displayInvalidConnectors(invalidConnectors);
}
const incompatible = errors.getIncompatibleSchemaError(err);
if (incompatible) {
displaySchemaChanges(incompatible);
return incompatible.diffs;
}
}
logLabeledSuccess("dataconnect", `Database schema is up to date.`);
return [];
}

Expand All @@ -42,17 +56,27 @@ export async function migrateSchema(args: {
const { options, schema, allowNonInteractiveMigration, validateOnly } = args;

const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId);
await ensureServiceIsConnectedToCloudSql(
serviceName,
instanceName,
databaseId,
/* linkIfNotConnected=*/ true,
);
try {
await upsertSchema(schema, validateOnly);
logger.debug(`Database schema was up to date for ${instanceId}:${databaseId}`);
} catch (err: any) {
if (err.status !== 400) {
throw err;
}
// Parse and handle failed precondition errors, then retry.
const incompatible = errors.getIncompatibleSchemaError(err);
const invalidConnectors = errors.getInvalidConnectors(err);
if (!incompatible && !invalidConnectors.length) {
// If we got a different type of error, throw it
throw err;
}

const shouldDeleteInvalidConnectors = await promptForInvalidConnectorError(
options,
invalidConnectors,
Expand Down Expand Up @@ -266,44 +290,61 @@ function displayInvalidConnectors(invalidConnectors: string[]) {

// If a service has never had a schema with schemaValidation=strict
// (ie when users create a service in console),
// the backend will not have the necesary permissions to check cSQL for differences.
// the backend will not have the necessary permissions to check cSQL for differences.
// We fix this by upserting the currently deployed schema with schemaValidation=strict,
async function ensureServiceIsConnectedToCloudSql(
serviceName: string,
instanceId: string,
databaseId: string,
linkIfNotConnected: boolean,
) {
let currentSchema: Schema;
try {
currentSchema = await getSchema(serviceName);
} catch (err: any) {
if (err.status === 404) {
// If no schema has been deployed yet, deploy an empty one to get connectivity.
currentSchema = {
name: `${serviceName}/schemas/${SCHEMA_ID}`,
source: {
files: [],
},
primaryDatasource: {
postgresql: {
database: databaseId,
cloudSql: {
instance: instanceId,
},
},
},
};
} else {
if (err.status !== 404) {
throw err;
}
if (!linkIfNotConnected) {
logLabeledWarning("dataconnect", `Not yet linked to the Cloud SQL instance.`);
return;
}
// TODO: make this prompt
// Should we upsert service here as well? so `database:sql:migrate` work for new service as well.
logLabeledBullet("dataconnect", `Linking the Cloud SQL instance...`);
// If no schema has been deployed yet, deploy an empty one to get connectivity.
currentSchema = {
name: `${serviceName}/schemas/${SCHEMA_ID}`,
source: {
files: [],
},
primaryDatasource: {
postgresql: {
database: databaseId,
cloudSql: {
instance: instanceId,
},
},
},
};
}
if (
!currentSchema.primaryDatasource.postgresql ||
currentSchema.primaryDatasource.postgresql.schemaValidation === "STRICT"
) {
const postgresql = currentSchema.primaryDatasource.postgresql;
if (postgresql?.cloudSql.instance !== instanceId) {
logLabeledWarning(
"dataconnect",
`Switching connected Cloud SQL instance\nFrom ${postgresql?.cloudSql.instance}\nTo ${instanceId}`,
);
}
if (postgresql?.database !== databaseId) {
logLabeledWarning(
"dataconnect",
`Switching connected Postgres database from ${postgresql?.database} to ${databaseId}`,
);
}
if (!postgresql || postgresql.schemaValidation === "STRICT") {
return;
}
currentSchema.primaryDatasource.postgresql.schemaValidation = "STRICT";
postgresql.schemaValidation = "STRICT";
try {
await upsertSchema(currentSchema, /** validateOnly=*/ false);
} catch (err: any) {
Expand Down
Loading