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

Dataconnect #7193

Merged
merged 11 commits into from
May 20, 2024
9 changes: 5 additions & 4 deletions firebase-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"Other"
],
"extensionDependencies": [
"graphql.vscode-graphql-syntax"
"graphql.vscode-graphql-syntax",
"redhat.vscode-yaml"
],
"activationEvents": [
"onStartupFinished",
Expand Down Expand Up @@ -110,7 +111,7 @@
"mono-firebase": {
"description": "Firebase icon",
"default": {
"fontPath": "./resources/Monicons.woff",
"fontPath": "./resources/monicons.woff",
"fontCharacter": "\\F101"
}
},
Expand Down Expand Up @@ -180,11 +181,11 @@
},
{
"fileMatch": "dataconnect.yaml",
"url": "./schema/dataconnect-yaml.json"
"url": "./dist/schema/dataconnect-yaml.json"
},
{
"fileMatch": "connector.yaml",
"url": "./schema/connector-yaml.json"
"url": "./dist/schema/connector-yaml.json"
}
]
},
Expand Down
Binary file modified firebase-vscode/resources/Monicons.woff
Binary file not shown.
Binary file modified firebase-vscode/resources/firebase_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 4 additions & 13 deletions firebase-vscode/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,10 @@ async function getServiceAccount() {
if (e.original?.message) {
errorMessage += ` (original: ${e.original.message})`;
}
if (process.env.MONOSPACE_ENV) {
// If it can't find a service account in Monospace, that's a blocking
// error and we should throw.
throw new Error(
`Unable to find service account. ` + `requireAuthError: ${errorMessage}`
);
} else {
// In other environments, it is common to not find a service account.
pluginLogger.debug(
`No service account found (this may be normal), ` +
`requireAuth error output: ${errorMessage}`
);
}
pluginLogger.debug(
`No service account found (this may be normal), ` +
`requireAuth error output: ${errorMessage}`
);
return null;
}
if (process.env.WORKSPACE_SERVICE_ACCOUNT_EMAIL) {
Expand Down
3 changes: 1 addition & 2 deletions firebase-vscode/src/core/project.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import vscode, { Disposable, ExtensionContext, QuickPickItem } from "vscode";
import vscode, { Disposable } from "vscode";
import { ExtensionBrokerImpl } from "../extension-broker";
import { computed, effect } from "@preact/signals-react";
import { firebaseRC, updateFirebaseRCProject } from "./config";
import { FirebaseProjectMetadata } from "../types/project";
import { currentUser, isServiceAccount } from "./user";
import { listProjects } from "../cli";
import { pluginLogger } from "../logger-wrapper";
import { currentOptions } from "../options";
import { globalSignal } from "../utils/globals";
import { firstWhereDefined } from "../utils/signal";

Expand Down
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
33 changes: 19 additions & 14 deletions src/dataconnect/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,27 @@ const PRECONDITION_ERROR_TYPESTRING = "type.googleapis.com/google.rpc.Preconditi
const INCOMPATIBLE_CONNECTOR_TYPE = "INCOMPATIBLE_CONNECTOR";

export function getIncompatibleSchemaError(err: any): IncompatibleSqlSchemaError | undefined {
const original = err.context?.body?.error || err.orignal;
if (!original) {
// If we can't get the original, rethrow so we don't cover up the original error.
throw err;
const incompatibles = errorDetails(err, INCOMPATIBLE_SCHEMA_ERROR_TYPESTRING);
if (incompatibles.length === 0) {
return undefined;
}
const details: any[] = original.details;
const incompatibles = details.filter((d) =>
d["@type"]?.includes(INCOMPATIBLE_SCHEMA_ERROR_TYPESTRING),
);
// Should never get multiple incompatible schema errors
return incompatibles[0];
const incompatible = incompatibles[0];
// Extract the violation type from the precondition error detail.
const preconditionErrs = errorDetails(err, PRECONDITION_ERROR_TYPESTRING);
const violationTypes = (incompatible.violationType = preconditionErrs
.flatMap((preCondErr) => preCondErr.violations)
.flatMap((viol) => viol.type)
.filter((type) => type === "INACCESSIBLE_SCHEMA" || type === "INCOMPATIBLE_SCHEMA"));
incompatible.violationType = violationTypes[0];
return incompatible;
}

// Note - the backend just includes file name, not the name of the connector resource in the GQLerror extensions.
// so we don't use this yet. Ideally, we'd just include connector name in the extensions.
export function getInvalidConnectors(err: any): string[] {
const preconditionErrs = errorDetails(err, PRECONDITION_ERROR_TYPESTRING);
const invalidConns: string[] = [];
const original = err.context?.body?.error || err?.orignal;
const details: any[] = original?.details;
const preconditionErrs = details?.filter((d) =>
d["@type"]?.includes(PRECONDITION_ERROR_TYPESTRING),
);
for (const preconditionErr of preconditionErrs) {
const incompatibleConnViolation = preconditionErr?.violations?.filter(
(v: { type: string }) => v.type === INCOMPATIBLE_CONNECTOR_TYPE,
Expand All @@ -36,3 +35,9 @@ export function getInvalidConnectors(err: any): string[] {
}
return invalidConns;
}

function errorDetails(err: any, ofType: string): any[] {
const original = err.context?.body?.error || err?.original;
const details: any[] = original?.details;
return details?.filter((d) => d["@type"]?.includes(ofType)) || [];
}
44 changes: 28 additions & 16 deletions src/dataconnect/provisionCloudSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ export async function provisionCloudSql(args: {
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
silent || utils.logLabeledBullet("dataconnect", `Found existing instance ${instanceId}.`);
connectionName = existingInstance?.connectionName || "";
if (!checkInstanceConfig(existingInstance, enableGoogleMlIntegration)) {
// TODO: Return message from checkInstanceConfig to explain exactly what changes are made
const why = getUpdateReason(existingInstance, enableGoogleMlIntegration);
if (why) {
silent ||
utils.logLabeledBullet(
"dataconnect",
`Instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
`Updating instance to enable Cloud IAM authentication and public IP. This may take a few minutes...`,
`Updating instance. This may take a few minutes...` +
why,
);
await promiseWithSpinner(
() =>
Expand Down Expand Up @@ -77,11 +78,21 @@ export async function provisionCloudSql(args: {
try {
await cloudSqlAdminClient.getDatabase(projectId, instanceId, databaseId);
silent || utils.logLabeledBullet("dataconnect", `Found existing database ${databaseId}.`);
} catch (err) {
silent ||
utils.logLabeledBullet("dataconnect", `Database ${databaseId} not found, creating it now...`);
await cloudSqlAdminClient.createDatabase(projectId, instanceId, databaseId);
silent || utils.logLabeledBullet("dataconnect", `Database ${databaseId} created.`);
} 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.`);
} else {
// Skip it if the database is not accessible.
// Possible that the CSQL instance is in the middle of something.
silent || utils.logLabeledWarning("dataconnect", `Database ${databaseId} is not accessible.`);
}
}
if (enableGoogleMlIntegration) {
await grantRolesToCloudSqlServiceAccount(projectId, instanceId, [GOOGLE_ML_INTEGRATION_ROLE]);
Expand All @@ -92,26 +103,24 @@ export async function provisionCloudSql(args: {
/**
* Validate that existing CloudSQL instances have the necessary settings.
*/
export function checkInstanceConfig(
instance: Instance,
requireGoogleMlIntegration: boolean,
): boolean {
export function getUpdateReason(instance: Instance, requireGoogleMlIntegration: boolean): string {
let reason = "";
const settings = instance.settings;
// CloudSQL instances must have public IP enabled to be used with Firebase Data Connect.
if (!settings.ipConfiguration?.ipv4Enabled) {
return false;
reason += "\n - to enable public IP.";
}

if (requireGoogleMlIntegration) {
if (!settings.enableGoogleMlIntegration) {
return false;
reason += "\n - to enable Google ML integration.";
}
if (
!settings.databaseFlags?.some(
(f) => f.name === "cloudsql.enable_google_ml_integration" && f.value === "on",
)
) {
return false;
reason += "\n - to enable Google ML integration database flag.";
}
}

Expand All @@ -120,6 +129,9 @@ export function checkInstanceConfig(
settings.databaseFlags?.some(
(f) => f.name === "cloudsql.iam_authentication" && f.value === "on",
) ?? false;
if (!isIamEnabled) {
reason += "\n - to enable IAM authentication database flag.";
}

return isIamEnabled;
return reason;
}
Loading
Loading