Skip to content

Commit

Permalink
delete graphs before deleting org
Browse files Browse the repository at this point in the history
  • Loading branch information
thisisnithin committed Nov 5, 2024
1 parent c38b8e8 commit 862d419
Show file tree
Hide file tree
Showing 16 changed files with 255 additions and 37 deletions.
12 changes: 12 additions & 0 deletions controlplane/src/bin/deactivate-org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import process from 'node:process';
import { drizzle } from 'drizzle-orm/postgres-js';
import { pino } from 'pino';
import postgres from 'postgres';
import { createS3ClientConfig, extractS3BucketName } from 'src/core/util.js';
import { S3Client } from '@aws-sdk/client-s3';
import { S3BlobStorage } from 'src/core/blobstorage/s3.js';
import { buildDatabaseConnectionConfig } from '../core/plugins/database.js';
import { OrganizationRepository } from '../core/repositories/OrganizationRepository.js';
import * as schema from '../db/schema.js';
Expand All @@ -23,7 +26,15 @@ const {
databaseTlsKey,
organizationSlug,
redis,
s3Storage,
} = getConfig();

const bucketName = extractS3BucketName(s3Storage);
const s3Config = createS3ClientConfig(bucketName, s3Storage);

const s3Client = new S3Client(s3Config);
const blobStorage = new S3BlobStorage(s3Client, bucketName);

const organizationId = process.env.ORGANIZATION_ID || '';
const deactivationReason = process.env.ORGANIZATION_DEACTIVATION_REASON;

Expand Down Expand Up @@ -82,6 +93,7 @@ const worker = createDeleteOrganizationWorker({
logger,
keycloakClient,
keycloakRealm: realm,
blobStorage,
});

await orgRepo.deactivateOrganization({
Expand Down
23 changes: 18 additions & 5 deletions controlplane/src/bin/delete-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import process from 'node:process';
import { drizzle } from 'drizzle-orm/postgres-js';
import { pino } from 'pino';
import postgres from 'postgres';
import { createS3ClientConfig, extractS3BucketName } from 'src/core/util.js';
import { S3Client } from '@aws-sdk/client-s3';
import { S3BlobStorage } from 'src/core/blobstorage/s3.js';
import { PlatformEventName } from '@wundergraph/cosmo-connect/dist/notifications/events_pb';
import { buildDatabaseConnectionConfig } from '../core/plugins/database.js';
import { UserRepository } from '../core/repositories/UserRepository.js';
Expand All @@ -24,10 +27,17 @@ const {
databaseTlsKey,
webhookUrl,
webhookSecret,
s3Storage,
} = getConfig();

const userId = process.env.USER_ID || '';

const bucketName = extractS3BucketName(s3Storage);
const s3Config = createS3ClientConfig(bucketName, s3Storage);

const s3Client = new S3Client(s3Config);
const blobStorage = new S3BlobStorage(s3Client, bucketName);

// Establish database connection
const connectionConfig = await buildDatabaseConnectionConfig({
tls:
Expand Down Expand Up @@ -80,11 +90,14 @@ if (!isSafe) {
}

// Delete the user
await userRepo.deleteUser({
id: user.id,
keycloakClient,
keycloakRealm: realm,
});
await userRepo.deleteUser(
{
id: user.id,
keycloakClient,
keycloakRealm: realm,
},
blobStorage,
);

platformWebhooks.send(PlatformEventName.USER_DELETE_SUCCESS, {
user_id: user.id,
Expand Down
9 changes: 9 additions & 0 deletions controlplane/src/bin/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ const getConfig = () => {

webhookUrl: process.env.WEBHOOK_URL,
webhookSecret: process.env.WEBHOOK_SECRET,

s3Storage: {
url: process.env.S3_STORAGE_URL || 'http://minio:changeme@localhost:10000/cosmo',
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || 'auto',
username: process.env.S3_ACCESS_KEY_ID,
password: process.env.S3_SECRET_ACCESS_KEY,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === undefined ? true : process.env.S3_FORCE_PATH_STYLE === 'true',
},
};
};

Expand Down
13 changes: 12 additions & 1 deletion controlplane/src/bin/reactivate-org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import process from 'node:process';
import { drizzle } from 'drizzle-orm/postgres-js';
import { pino } from 'pino';
import postgres from 'postgres';
import { createS3ClientConfig, extractS3BucketName } from 'src/core/util.js';
import { S3Client } from '@aws-sdk/client-s3';
import { S3BlobStorage } from 'src/core/blobstorage/s3.js';
import { buildDatabaseConnectionConfig } from '../core/plugins/database.js';
import { OrganizationRepository } from '../core/repositories/OrganizationRepository.js';
import * as schema from '../db/schema.js';
Expand All @@ -23,9 +26,16 @@ const {
databaseTlsKey,
organizationSlug,
redis,
s3Storage,
} = getConfig();

const bucketName = extractS3BucketName(s3Storage);
const s3Config = createS3ClientConfig(bucketName, s3Storage);

const s3Client = new S3Client(s3Config);
const blobStorage = new S3BlobStorage(s3Client, bucketName);

const organizationId = process.env.ORGANIZATION_ID || '';
const deactivationReason = process.env.ORGANIZATION_DEACTIVATION_REASON;

// Establish database connection
const connectionConfig = await buildDatabaseConnectionConfig({
Expand Down Expand Up @@ -82,6 +92,7 @@ const worker = createDeleteOrganizationWorker({
logger,
keycloakClient,
keycloakRealm: realm,
blobStorage,
});

await orgRepo.reactivateOrganization({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function deleteOrganization(
});
}

await orgRepo.deleteOrganization(authContext.organizationId);
await orgRepo.deleteOrganization(authContext.organizationId, opts.blobStorage);

await opts.keycloakClient.deleteOrganizationGroup({
realm: opts.keycloakRealm,
Expand Down
13 changes: 8 additions & 5 deletions controlplane/src/core/bufservices/user/deleteUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ export function deleteUser(
await opts.keycloakClient.authenticateClient();

// Delete the user
await userRepo.deleteUser({
id: authContext.userId,
keycloakClient: opts.keycloakClient,
keycloakRealm: opts.keycloakRealm,
});
await userRepo.deleteUser(
{
id: authContext.userId,
keycloakClient: opts.keycloakClient,
keycloakRealm: opts.keycloakRealm,
},
opts.blobStorage,
);

opts.platformWebhooks.send(PlatformEventName.USER_DELETE_SUCCESS, {
user_id: authContext.userId,
Expand Down
13 changes: 8 additions & 5 deletions controlplane/src/core/bufservices/user/removeInvitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,14 @@ export function removeInvitation(
// this will happen only when the user was invited but the user didn't login and the admin removed that user,
// in this case the user will not have a personal org
if (userMemberships.length === 0 && userPendingInvitations.length === 0) {
await userRepo.deleteUser({
id: user.id,
keycloakClient: opts.keycloakClient,
keycloakRealm: opts.keycloakRealm,
});
await userRepo.deleteUser(
{
id: user.id,
keycloakClient: opts.keycloakClient,
keycloakRealm: opts.keycloakRealm,
},
opts.blobStorage,
);
}

await auditLogRepo.addAuditLog({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,14 @@ export function removeOrganizationMember(
// this will happen only when the user was invited but the user didn't login and the admin removed that user,
// in this case the user will not have a personal org
if (userMemberships.length === 0) {
await userRepo.deleteUser({
id: user.id,
keycloakClient: opts.keycloakClient,
keycloakRealm: opts.keycloakRealm,
});
await userRepo.deleteUser(
{
id: user.id,
keycloakClient: opts.keycloakClient,
keycloakRealm: opts.keycloakRealm,
},
opts.blobStorage,
);
}

await auditLogRepo.addAuditLog({
Expand Down
21 changes: 11 additions & 10 deletions controlplane/src/core/build-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ export default async function build(opts: BuildConfig) {
tls: opts.redis.tls,
});

if (!opts.s3Storage || !opts.s3Storage.url) {
throw new Error('S3 storage URL is required');
}

const bucketName = extractS3BucketName(opts.s3Storage);
const s3Config = createS3ClientConfig(bucketName, opts.s3Storage);

const s3Client = new S3Client(s3Config);
const blobStorage = new S3BlobStorage(s3Client, bucketName);

const readmeQueue = new AIGraphReadmeQueue(logger, fastify.redisForQueue);

if (opts.openaiAPIKey) {
Expand All @@ -308,6 +318,7 @@ export default async function build(opts: BuildConfig) {
logger,
keycloakClient,
keycloakRealm: opts.keycloak.realm,
blobStorage,
}),
);

Expand Down Expand Up @@ -350,16 +361,6 @@ export default async function build(opts: BuildConfig) {
});
}

if (!opts.s3Storage || !opts.s3Storage.url) {
throw new Error('S3 storage URL is required');
}

const bucketName = extractS3BucketName(opts.s3Storage);
const s3Config = createS3ClientConfig(bucketName, opts.s3Storage);

const s3Client = new S3Client(s3Config);
const blobStorage = new S3BlobStorage(s3Client, bucketName);

/**
* Controllers registration
*/
Expand Down
35 changes: 34 additions & 1 deletion controlplane/src/core/repositories/OrganizationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ import {
import { Feature, FeatureIds, OrganizationDTO, OrganizationMemberDTO, WebhooksConfigDTO } from '../../types/index.js';
import Keycloak from '../services/Keycloak.js';
import { DeleteOrganizationQueue } from '../workers/DeleteOrganizationWorker.js';
import { BlobStorage } from '../blobstorage/index.js';
import { BillingRepository } from './BillingRepository.js';
import { FederatedGraphRepository } from './FederatedGraphRepository.js';
import { OidcRepository } from './OidcRepository.js';
import { SubgraphRepository } from './SubgraphRepository.js';

/**
* Repository for organization related operations.
Expand Down Expand Up @@ -849,9 +851,40 @@ export class OrganizationRepository {
return result[0];
}

public deleteOrganization(organizationId: string) {
public deleteOrganization(organizationId: string, blobStorage: BlobStorage) {
return this.db.transaction(async (tx) => {
const oidcRepo = new OidcRepository(tx);
const fedGraphRepo = new FederatedGraphRepository(this.logger, tx, organizationId);
const subgraphRepo = new SubgraphRepository(this.logger, tx, organizationId);

const graphs = await fedGraphRepo.list({
limit: 0,
offset: 0,
});
const subgraphs = await subgraphRepo.list({
limit: 0,
offset: 0,
excludeFeatureSubgraphs: false,
});

// Delete graphs
const promises = [];
for (const graph of graphs) {
promises.push(fedGraphRepo.delete(graph.targetId));
}
for (const subgraph of subgraphs) {
promises.push(subgraphRepo.delete(subgraph.targetId));
}
await Promise.all(promises);

// Clean up blob storage
const blobPromises = [];
for (const graph of graphs) {
const blobStorageDirectory = `${organizationId}/${graph.id}`;
blobPromises.push(blobStorage.removeDirectory({ key: blobStorageDirectory }));
}
await Promise.allSettled(blobPromises);

await oidcRepo.deleteOidcProvider({ organizationId });

// Delete organization from db
Expand Down
8 changes: 6 additions & 2 deletions controlplane/src/core/repositories/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { users } from '../../db/schema.js';
import { UserDTO } from '../../types/index.js';
import Keycloak from '../services/Keycloak.js';
import OidcProvider from '../services/OidcProvider.js';
import { BlobStorage } from '../blobstorage/index.js';
import { OrganizationRepository } from './OrganizationRepository.js';
import { BillingRepository } from './BillingRepository.js';
import { OidcRepository } from './OidcRepository.js';
Expand Down Expand Up @@ -65,7 +66,10 @@ export class UserRepository {
.execute();
}

public async deleteUser(input: { id: string; keycloakClient: Keycloak; keycloakRealm: string }) {
public async deleteUser(
input: { id: string; keycloakClient: Keycloak; keycloakRealm: string },
blobStorage: BlobStorage,
) {
const orgRepo = new OrganizationRepository(this.logger, this.db);
const billingRepo = new BillingRepository(this.db);

Expand Down Expand Up @@ -114,7 +118,7 @@ export class UserRepository {
// Delete all solo organizations of the user
const deleteOrgs: Promise<void>[] = [];
for (const org of soloAdminSoloMemberOrgs) {
deleteOrgs.push(orgRepo.deleteOrganization(org.id));
deleteOrgs.push(orgRepo.deleteOrganization(org.id, blobStorage));
}
await Promise.all(deleteOrgs);

Expand Down
5 changes: 4 additions & 1 deletion controlplane/src/core/workers/DeleteOrganizationWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { OrganizationRepository } from '../repositories/OrganizationRepository.j
import Keycloak from '../services/Keycloak.js';
import { OidcRepository } from '../repositories/OidcRepository.js';
import OidcProvider from '../services/OidcProvider.js';
import { BlobStorage } from '../blobstorage/index.js';

const QueueName = 'organization.delete';
const WorkerName = 'DeleteOrganizationWorker';
Expand Down Expand Up @@ -62,6 +63,7 @@ class DeleteOrganizationWorker {
logger: pino.Logger;
keycloakClient: Keycloak;
keycloakRealm: string;
blobStorage: BlobStorage;
},
) {
this.input.logger = input.logger.child({ worker: WorkerName });
Expand Down Expand Up @@ -101,7 +103,7 @@ class DeleteOrganizationWorker {

await oidcRepo.deleteOidcProvider({ organizationId: job.data.organizationId });

await orgRepo.deleteOrganization(job.data.organizationId);
await orgRepo.deleteOrganization(job.data.organizationId, this.input.blobStorage);
});
} catch (err) {
this.input.logger.error(err, `Failed to delete organization with id ${job.data.organizationId}`);
Expand All @@ -116,6 +118,7 @@ export const createDeleteOrganizationWorker = (input: {
logger: pino.Logger;
keycloakClient: Keycloak;
keycloakRealm: string;
blobStorage: BlobStorage;
}) => {
const log = input.logger.child({ worker: WorkerName });
const worker = new Worker<DeleteOrganizationInput>(
Expand Down
2 changes: 1 addition & 1 deletion controlplane/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export const subgraphs = pgTable(
websocketSubprotocol: websocketSubprotocolEnum('websocket_subprotocol').notNull().default('auto'),
// This is the latest valid schema of the subgraph.
schemaVersionId: uuid('schema_version_id').references(() => schemaVersion.id, {
onDelete: 'set null',
onDelete: 'no action',
}),
targetId: uuid('target_id')
.notNull()
Expand Down
Loading

0 comments on commit 862d419

Please sign in to comment.