diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index a9ba5250..b5586b84 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -436,6 +436,19 @@ export class Database { return Boolean(updateResult.affected); } + async deleteDeploymentById (deploymentId: string): Promise { + const deploymentRepository = this.dataSource.getRepository(Deployment); + const deployment = await deploymentRepository.findOneOrFail({ + where: { + id: deploymentId + } + }); + + const deleteResult = await deploymentRepository.softRemove(deployment); + + return Boolean(deleteResult); + } + async addProject (user: User, organizationId: string, data: DeepPartial): Promise { const projectRepository = this.dataSource.getRepository(Project); diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 655e6d74..d61ad141 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -12,7 +12,7 @@ import { import { Project } from './Project'; import { Domain } from './Domain'; import { User } from './User'; -import { AppDeploymentRecordAttributes } from '../types'; +import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types'; export enum Environment { Production = 'Production', @@ -24,6 +24,7 @@ export enum DeploymentStatus { Building = 'Building', Ready = 'Ready', Error = 'Error', + Deleting = 'Deleting', } export interface ApplicationDeploymentRequest { @@ -41,6 +42,12 @@ export interface ApplicationDeploymentRemovalRequest { deployment: string; } +export interface ApplicationDeploymentRemovalRequest { + type: string; + version: string; + deployment: string; +} + export interface ApplicationRecord { type: string; version: string; @@ -104,6 +111,18 @@ export class Deployment { @Column('simple-json', { nullable: true }) applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null; + + @Column('varchar', { nullable: true }) + applicationDeploymentRemovalRequestId!: string | null; + + @Column('simple-json', { nullable: true }) + applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null; + + @Column('varchar', { nullable: true }) + applicationDeploymentRemovalRecordId!: string | null; + + @Column('simple-json', { nullable: true }) + applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null; @Column({ enum: Environment diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 6e9f8966..72423196 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -12,7 +12,7 @@ import { ApplicationDeploymentRequest, ApplicationDeploymentRemovalRequest } from './entity/Deployment'; -import { AppDeploymentRecord, PackageJSON } from './types'; +import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types'; import { sleep } from './utils'; const log = debug('snowball:registry'); @@ -21,6 +21,7 @@ const APP_RECORD_TYPE = 'ApplicationRecord'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; +const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'; const SLEEP_DURATION = 1000; // TODO: Move registry code to laconic-sdk/watcher-ts @@ -231,6 +232,30 @@ export class Registry { ); } + /** + * Fetch ApplicationDeploymentRemovalRecords for deployments + */ + async getDeploymentRemovalRecords ( + deployments: Deployment[] + ): Promise { + // Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments + const records = await this.registry.queryRecords( + { + type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE + }, + true + ); + + // Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs + return records.filter((record: AppDeploymentRemovalRecord) => + deployments.some( + (deployment) => + deployment.applicationDeploymentRemovalRequestId === record.attributes.request && + deployment.applicationDeploymentRecordId === record.attributes.deployment + ) + ); + } + async createApplicationDeploymentRemovalRequest (data: { deploymentId: string; }): Promise<{ diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 0067ef5e..b939325e 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -15,7 +15,7 @@ import { Permission, ProjectMember } from './entity/ProjectMember'; import { User } from './entity/User'; import { Registry } from './registry'; import { GitHubConfig, RegistryConfig } from './config'; -import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types'; +import { AppDeploymentRecord, AppDeploymentRemovalRecord, GitPushEventPayload, PackageJSON } from './types'; import { Role } from './entity/UserOrganization'; const log = debug('snowball:service'); @@ -63,14 +63,13 @@ export class Service { /** * Checks for ApplicationDeploymentRecord and update corresponding deployments - * Continues check in loop after a delay of DEPLOY_RECORD_CHECK_DELAY_MS + * Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay */ async checkDeployRecordsAndUpdate (): Promise { // Fetch deployments in building state const deployments = await this.db.getDeployments({ where: { status: DeploymentStatus.Building - // TODO: Fetch and check records for recent deployments } }); @@ -116,6 +115,38 @@ export class Service { }, this.config.registryConfig.fetchDeploymentRecordDelay); } + /** + * Checks for ApplicationDeploymentRemovalRecord and remove corresponding deployments + * Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay + */ + async checkDeploymentRemovalRecordsAndUpdate (): Promise { + // Fetch deployments in deleting state + const deployments = await this.db.getDeployments({ + where: { + status: DeploymentStatus.Deleting + } + }); + + if (deployments.length) { + log( + `Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state` + ); + + // Fetch ApplicationDeploymentRemovalRecords for deployments + const records = await this.registry.getDeploymentRemovalRecords(deployments); + log(`Found ${records.length} ApplicationDeploymentRemovalRecords`); + + // Update deployments for which ApplicationDeploymentRemovalRecords were returned + if (records.length) { + await this.deleteDeploymentsWithRecordData(records, deployments); + } + } + + this.deployRecordCheckTimeout = setTimeout(() => { + this.checkDeploymentRemovalRecordsAndUpdate(); + }, this.config.registryConfig.fetchDeploymentRecordDelay); + } + /** * Update deployments with ApplicationDeploymentRecord data */ @@ -178,6 +209,45 @@ export class Service { await Promise.all(deploymentUpdatePromises); } + /** + * Delete deployments with ApplicationDeploymentRemovalRecord data + */ + async deleteDeploymentsWithRecordData ( + records: AppDeploymentRemovalRecord[], + deployments: Deployment[], + ): Promise { + const removedApplicationDeploymentRecordIds = records.map(record => record.attributes.deployment); + + // Get removed deployments for ApplicationDeploymentRecords + const removedDeployments = deployments.filter(deployment => removedApplicationDeploymentRecordIds.includes(deployment.applicationDeploymentRecordId!)) + + const recordToDeploymentsMap = removedDeployments.reduce( + (acc: { [key: string]: Deployment }, deployment) => { + acc[deployment.applicationDeploymentRecordId!] = deployment; + return acc; + }, + {} + ); + + // Update deployment data for ApplicationDeploymentRecords and delete + const deploymentUpdatePromises = records.map(async (record) => { + const deployment = recordToDeploymentsMap[record.attributes.deployment]; + + await this.db.updateDeploymentById(deployment.id, { + applicationDeploymentRemovalRecordId: record.id, + applicationDeploymentRemovalRecordData: record.attributes, + }); + + log( + `Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}` + ); + + await this.db.deleteDeploymentById(deployment.id) + }); + + await Promise.all(deploymentUpdatePromises); + } + async getUser (userId: string): Promise { return this.db.getUser({ where: { @@ -725,10 +795,22 @@ export class Service { id: deploymentId } }); + if (deployment && deployment.applicationDeploymentRecordId) { const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deployment.applicationDeploymentRecordId }); + + await this.db.updateDeploymentById( + deployment.id, + { + status: DeploymentStatus.Deleting, + applicationDeploymentRemovalRequestId: result.applicationDeploymentRemovalRequestId, + applicationDeploymentRemovalRequestData: result.applicationDeploymentRemovalRequestData + } + ); + return (result !== undefined || result !== null); } + return false; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 80941346..fecb4a61 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -38,6 +38,13 @@ export interface AppDeploymentRecordAttributes { version: string; } +export interface AppDeploymentRemovalRecordAttributes { + deployment: string; + request: string; + type: "ApplicationDeploymentRemovalRecord"; + version: string; +} + interface RegistryRecord { id: string; names: string[] | null; @@ -50,3 +57,7 @@ interface RegistryRecord { export interface AppDeploymentRecord extends RegistryRecord { attributes: AppDeploymentRecordAttributes; } + +export interface AppDeploymentRemovalRecord extends RegistryRecord { + attributes: AppDeploymentRemovalRecordAttributes; +}