From 3cbea57294cf0cffdbf828aad1384e50ea0cf9e6 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Wed, 27 Mar 2024 12:41:53 -0400 Subject: [PATCH 01/13] feat(registry): deployment removal request --- packages/backend/src/entity/Deployment.ts | 6 ++++ packages/backend/src/registry.ts | 35 ++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 8e0a3246..655e6d74 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -35,6 +35,12 @@ export interface ApplicationDeploymentRequest { meta: string; } +export interface ApplicationDeploymentRemovalRequest { + type: string; + version: string; + deployment: string; +} + export interface ApplicationRecord { type: string; version: string; diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index ed30af9f..71091f1e 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -9,7 +9,8 @@ import { RegistryConfig } from './config'; import { ApplicationRecord, Deployment, - ApplicationDeploymentRequest + ApplicationDeploymentRequest, + ApplicationDeploymentRemovalRequest } from './entity/Deployment'; import { AppDeploymentRecord, PackageJSON } from './types'; import { sleep } from './utils'; @@ -18,6 +19,7 @@ const log = debug('snowball:registry'); 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 SLEEP_DURATION = 1000; @@ -229,6 +231,37 @@ export class Registry { ); } + async createApplicationDeploymentRemovalRequest (data: { + deployment: Deployment + }): Promise<{ + applicationDeploymentRemovalRequestId: string; + applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest; + }> { + const applicationDeploymentRemovalRequest = { + type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE, + version: '1.0.0', + deployment: data.deployment.id + }; + + const result = await this.registry.setRecord( + { + privateKey: this.registryConfig.privateKey, + record: applicationDeploymentRemovalRequest, + bondId: this.registryConfig.bondId + }, + '', + this.registryConfig.fee + ); + + log(`Application deployment removal request record published: ${result.data.id}`); + log('Application deployment removal request data:', applicationDeploymentRemovalRequest); + + return { + applicationDeploymentRemovalRequestId: result.data.id, + applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest + }; + } + getCrn (appName: string): string { assert(this.registryConfig.authority, "Authority doesn't exist"); return `crn://${this.registryConfig.authority}/applications/${appName}`; From 953c3fc10bffd5e042273b8d98ee4f77d86b8d05 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Wed, 27 Mar 2024 12:54:34 -0400 Subject: [PATCH 02/13] feat(gql): deployment removal request --- packages/backend/src/registry.ts | 4 ++-- packages/backend/src/resolvers.ts | 14 ++++++++++++++ packages/backend/src/schema.gql | 1 + packages/backend/src/service.ts | 5 +++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 71091f1e..6e9f8966 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -232,7 +232,7 @@ export class Registry { } async createApplicationDeploymentRemovalRequest (data: { - deployment: Deployment + deploymentId: string; }): Promise<{ applicationDeploymentRemovalRequestId: string; applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest; @@ -240,7 +240,7 @@ export class Registry { const applicationDeploymentRemovalRequest = { type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE, version: '1.0.0', - deployment: data.deployment.id + deployment: data.deploymentId }; const result = await this.registry.setRecord( diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index e2e3339d..5e40c74a 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -255,6 +255,20 @@ export const createResolvers = async (service: Service): Promise => { } }, + deleteDeployment: async ( + _: any, + { + deploymentId + }: { deploymentId: string; } + ) => { + try { + return await service.deleteDeployment(deploymentId); + } catch (err) { + log(err); + return false; + } + }, + addDomain: async ( _: any, { projectId, data }: { projectId: string; data: { name: string } } diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index c22db8db..5ba963c8 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -209,6 +209,7 @@ type Mutation { deleteProject(projectId: String!): Boolean! deleteDomain(domainId: String!): Boolean! rollbackDeployment(projectId: String!, deploymentId: String!): Boolean! + deleteDeployment(deploymentId: String!): Boolean! addDomain(projectId: String!, data: AddDomainInput!): Boolean! updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean! authenticateGitHub(code: String!): AuthResult! diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 5211585c..daa86c40 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -694,6 +694,11 @@ export class Service { return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate; } + async deleteDeployment (deploymentId: string): Promise { + const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId }); + return (result !== undefined || result !== null); + } + async addDomain ( projectId: string, data: { name: string } From a8d93732ced8e9d34b82453ccbf26cd3294c975f Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Wed, 27 Mar 2024 12:58:56 -0400 Subject: [PATCH 03/13] feat(gql-client): deployment removal request --- packages/gql-client/src/client.ts | 11 +++++++++++ packages/gql-client/src/mutations.ts | 6 ++++++ packages/gql-client/src/types.ts | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 74c7683e..f238869e 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -276,6 +276,17 @@ export class GQLClient { return data; } + async deleteDeployment (deploymentId: string): Promise { + const { data } = await this.client.mutate({ + mutation: mutations.deleteDeployment, + variables: { + deploymentId + } + }); + + return data; + } + async addDomain (projectId: string, data: types.AddDomainInput): Promise { const result = await this.client.mutate({ mutation: mutations.addDomain, diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index fd7a9c2e..be8d96d9 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -82,6 +82,12 @@ mutation ($projectId: String! ,$deploymentId: String!) { } `; +export const deleteDeployment = gql` +mutation ($deploymentId: String!) { + deleteDeployment(deploymentId: $deploymentId) +} +`; + export const addDomain = gql` mutation ($projectId: String!, $data: AddDomainInput!) { addDomain(projectId: $projectId, data: $data) diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 71c44c00..786b7519 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -269,6 +269,10 @@ export type RollbackDeploymentResponse = { rollbackDeployment: boolean } +export type DeleteDeploymentResponse = { + deleteDeployment: boolean +} + export type AddDomainInput = { name: string } From 096fd04a226650ea95c45743227a3a626bf5d336 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Fri, 29 Mar 2024 13:24:53 -0400 Subject: [PATCH 04/13] fix: project card settings links --- .../projects/ProjectCard/ProjectCard.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx b/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx index 84a34b4f..fe9a693c 100644 --- a/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx +++ b/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx @@ -53,6 +53,16 @@ export const ProjectCard = ({ navigate(`projects/${project.id}`); }, [project.id, navigate]); + const navigateToSettingsOnClick = useCallback( + ( + e: React.MouseEvent | React.MouseEvent, + ) => { + e.stopPropagation(); + navigate(`projects/${project.id}/settings`); + }, + [project.id, navigate], + ); + return (
- Project settings - Delete project + + Project settings + + + Delete project +
From c2158510d9ea922677dd1fea39c64e19209f866d Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Fri, 29 Mar 2024 13:45:54 -0400 Subject: [PATCH 05/13] feat: clean up stuck builds --- packages/backend/src/service.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index daa86c40..64223f27 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -22,6 +22,9 @@ const log = debug('snowball:service'); const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository'; +// Define a constant for an hour in milliseconds +const HOUR = 1000 * 60 * 60; + interface Config { gitHubConfig: GitHubConfig; registryConfig: RegistryConfig; @@ -76,6 +79,28 @@ export class Service { `Found ${deployments.length} deployments in ${DeploymentStatus.Building} state` ); + // Calculate a timestamp for one hour ago + const anHourAgo = Date.now() - HOUR; + + // Filter out deployments started more than an hour ago and mark them as Error + const oldDeploymentsToUpdate = deployments.filter( + deployment => (Number(deployment.updatedAt) < anHourAgo) + ) + .map((deployment) => { + return this.db.updateDeploymentById(deployment.id, { + status: DeploymentStatus.Error, + isCurrent: false + }); + }); + + // If there are old deployments to update, log and perform the updates + if (oldDeploymentsToUpdate.length > 0) { + log( + `Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour` + ); + await Promise.all(oldDeploymentsToUpdate); + } + // Fetch ApplicationDeploymentRecord for deployments const records = await this.registry.getDeploymentRecords(deployments); log(`Found ${records.length} ApplicationDeploymentRecords`); From cd2dce24043e80f958eb3d8f81d82a439b43c754 Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Fri, 29 Mar 2024 14:41:08 -0400 Subject: [PATCH 06/13] fix: don't use two refs --- .../frontend/src/components/shared/UserSelect/UserSelect.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx b/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx index 139f6a17..d9faa103 100644 --- a/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx +++ b/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx @@ -107,7 +107,6 @@ export const UserSelect = ({ options, value }: UserSelectProps) => { ref: inputWrapperRef, suppressRefError: true, })} - ref={inputWrapperRef} onClick={() => !dropdownOpen && openMenu()} className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm" > From 8210512eea6b8c97a9d495b25251a4f2a14d808f Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Fri, 29 Mar 2024 15:03:34 -0400 Subject: [PATCH 07/13] fix: missing key --- .../projects/create/RepositoryList/RepositoryList.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx index 3e6adee8..8f7c7811 100644 --- a/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx +++ b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx @@ -155,13 +155,13 @@ export const RepositoryList = () => { {Boolean(repositoryDetails.length) ? (
{repositoryDetails.map((repo, index) => ( - <> - +
+ {/* Horizontal line */} {index !== repositoryDetails.length - 1 && (
)} - +
))}
) : ( From 328da7fdc8a361c23bd6905897be8d41bb81af9e Mon Sep 17 00:00:00 2001 From: Eric Lewis Date: Fri, 29 Mar 2024 15:31:49 -0400 Subject: [PATCH 08/13] feat: submit delete deployment request doesn't appear to work when the deployment is current? --- packages/backend/src/service.ts | 12 ++++++++-- .../project/deployments/DeploymentMenu.tsx | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 64223f27..0067ef5e 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -720,8 +720,16 @@ export class Service { } async deleteDeployment (deploymentId: string): Promise { - const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId }); - return (result !== undefined || result !== null); + const deployment = await this.db.getDeployment({ + where: { + id: deploymentId + } + }); + if (deployment && deployment.applicationDeploymentRecordId) { + const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deployment.applicationDeploymentRecordId }); + return (result !== undefined || result !== null); + } + return false; } async addDomain ( diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx index c2b62fd1..7e6fdd7d 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -9,6 +9,7 @@ import { RefreshIcon, RocketIcon, UndoIcon, + CrossCircleIcon, } from 'components/shared/CustomIcon'; import { Menu, @@ -79,6 +80,16 @@ export const DeploymentMenu = ({ } }; + const deleteDeployment = async () => { + const isDeleted = await client.deleteDeployment(deployment.id); + if (isDeleted) { + await onUpdate(); + toast.success('Deleted deployment'); + } else { + toast.error('Unable to delete deployment'); + } + }; + return ( <>
@@ -147,6 +158,18 @@ export const DeploymentMenu = ({ > Rollback to this version + deleteDeployment()} + disabled={ + deployment.isCurrent || + deployment.environment !== Environment.Production || + !Boolean(currentDeployment) + } + placeholder={''} + > + Delete deployment +
From 4fa6f418baca5289a380db04d69b2377b0514496 Mon Sep 17 00:00:00 2001 From: Nabarun Date: Mon, 15 Apr 2024 18:24:06 +0530 Subject: [PATCH 09/13] Lint fix --- README.md | 18 +++--------------- .../project/deployments/DeploymentMenu.tsx | 1 - 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d5129ff3..ed2506a6 100644 --- a/README.md +++ b/README.md @@ -99,25 +99,13 @@ Let us assume the following domains for backend and frontend - Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml) ```bash - laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe" + laconic-so deployment --dir laconic-loaded-deployment exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe" # WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y # 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81 ``` - - - Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml) - - ```bash - # For registryConfig.restEndpoint - laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317 - # 0.0.0.0:32777 - - # For registryConfig.gqlEndpoint - laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473 - # 0.0.0.0:32771 - ``` - + - Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml) - + - Run the script to create bond, reserve the authority and set authority bond ```bash diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx index 7e6fdd7d..1d6d33b0 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -166,7 +166,6 @@ export const DeploymentMenu = ({ deployment.environment !== Environment.Production || !Boolean(currentDeployment) } - placeholder={''} > Delete deployment From f290b5c0b515fdd22ef60c37eacc893c0293fee2 Mon Sep 17 00:00:00 2001 From: Nabarun Date: Mon, 15 Apr 2024 19:36:04 +0530 Subject: [PATCH 10/13] Implement checking for deployment removal records in intervals --- packages/backend/src/database.ts | 13 ++++ packages/backend/src/entity/Deployment.ts | 21 +++++- packages/backend/src/registry.ts | 27 ++++++- packages/backend/src/service.ts | 88 ++++++++++++++++++++++- packages/backend/src/types.ts | 11 +++ 5 files changed, 155 insertions(+), 5 deletions(-) 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; +} From b53e12b94b2e2777f56025ff5ba36b3146975c8e Mon Sep 17 00:00:00 2001 From: Nabarun Date: Wed, 24 Apr 2024 12:08:56 +0530 Subject: [PATCH 11/13] Add script for publishing ApplicationDeploymentRemovalRecord record --- packages/backend/package.json | 1 + packages/backend/src/entity/Deployment.ts | 6 +- packages/backend/src/schema.gql | 1 + packages/backend/src/service.ts | 2 + .../publish-deployment-removal-records.ts | 67 +++++++++++++++++++ 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/backend/test/publish-deployment-removal-records.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index be7f3372..0d6d216c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -41,6 +41,7 @@ "lint": "tsc --noEmit", "test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts", "test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts", + "test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts", "test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts", "test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts" }, diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index d61ad141..4535ea27 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -6,7 +6,8 @@ import { UpdateDateColumn, ManyToOne, OneToOne, - JoinColumn + JoinColumn, + DeleteDateColumn } from 'typeorm'; import { Project } from './Project'; @@ -146,4 +147,7 @@ export class Deployment { @UpdateDateColumn() updatedAt!: Date; + + @DeleteDateColumn() + deletedAt!: Date | null; } diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 5ba963c8..7aa47991 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -19,6 +19,7 @@ enum DeploymentStatus { Building Ready Error + Deleting } enum DomainStatus { diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index b939325e..cba99a48 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -52,6 +52,8 @@ export class Service { init (): void { // Start check for ApplicationDeploymentRecords asynchronously this.checkDeployRecordsAndUpdate(); + // Start check for ApplicationDeploymentRemovalRecords asynchronously + this.checkDeploymentRemovalRecordsAndUpdate(); } /** diff --git a/packages/backend/test/publish-deployment-removal-records.ts b/packages/backend/test/publish-deployment-removal-records.ts new file mode 100644 index 00000000..d8b026a3 --- /dev/null +++ b/packages/backend/test/publish-deployment-removal-records.ts @@ -0,0 +1,67 @@ +import debug from 'debug'; +import { DataSource } from 'typeorm'; +import path from 'path'; + +import { Registry } from '@cerc-io/laconic-sdk'; + +import { Config } from '../src/config'; +import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; +import { getConfig } from '../src/utils'; +import { Deployment, DeploymentStatus } from '../src/entity/Deployment'; + +const log = debug('snowball:publish-deployment-removal-records'); + +async function main () { + const { registryConfig, database, misc } = await getConfig(DEFAULT_CONFIG_FILE_PATH); + + const registry = new Registry( + registryConfig.gqlEndpoint, + registryConfig.restEndpoint, + registryConfig.chainId + ); + + const dataSource = new DataSource({ + type: 'better-sqlite3', + database: database.dbPath, + synchronize: true, + entities: [path.join(__dirname, '../src/entity/*')] + }); + + await dataSource.initialize(); + + const deploymentRepository = dataSource.getRepository(Deployment); + const deployments = await deploymentRepository.find({ + relations: { + project: true + }, + where: { + status: DeploymentStatus.Deleting + } + }); + + for await (const deployment of deployments) { + const applicationDeploymentRemovalRecord = { + type: "ApplicationDeploymentRemovalRecord", + version: "1.0.0", + deployment: deployment.applicationDeploymentRecordId, + request: deployment.applicationDeploymentRemovalRequestId, + } + + const result = await registry.setRecord( + { + privateKey: registryConfig.privateKey, + record: applicationDeploymentRemovalRecord, + bondId: registryConfig.bondId + }, + '', + registryConfig.fee + ); + + log('Application deployment removal record data:', applicationDeploymentRemovalRecord); + log(`Application deployment removal record published: ${result.data.id}`); + } +} + +main().catch((err) => { + log(err); +}); From 5f4be30799a4b880335899225c150517de818a45 Mon Sep 17 00:00:00 2001 From: Nabarun Date: Thu, 25 Apr 2024 16:51:22 +0530 Subject: [PATCH 12/13] Remove current deployment and publish ApplicationDeploymentRemovalRequest for project DNS deployment --- packages/backend/src/registry.ts | 13 ++++++ packages/backend/src/service.ts | 40 ++++++++++++++----- .../backend/test/publish-deploy-records.ts | 19 ++++++++- .../deployments/DeploymentDetailsCard.tsx | 3 +- .../project/deployments/DeploymentMenu.tsx | 5 --- packages/gql-client/src/types.ts | 1 + 6 files changed, 64 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 72423196..d15b6985 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -232,6 +232,19 @@ export class Registry { ); } + /** + * Fetch ApplicationDeploymentRecords by filter + */ + async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise { + return this.registry.queryRecords( + { + type: APP_DEPLOYMENT_RECORD_TYPE, + ...filter + }, + true + ); + } + /** * Fetch ApplicationDeploymentRemovalRecords for deployments */ diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index cba99a48..306afd38 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -553,17 +553,10 @@ export class Service { return acc; }, {} as { [key: string]: string }); - const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest( - { - deployment: newDeployment, - appName: repo, - repository: repoUrl, - environmentVariables: environmentVariablesObj, - dns: `${newDeployment.project.name}-${newDeployment.id}` - }); - // To set project DNS if (data.environment === Environment.Production) { + // On deleting deployment later, project DNS deployment is also deleted + // So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later await this.registry.createApplicationDeploymentRequest( { deployment: newDeployment, @@ -574,6 +567,15 @@ export class Service { }); } + const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest( + { + deployment: newDeployment, + appName: repo, + repository: repoUrl, + environmentVariables: environmentVariablesObj, + dns: `${newDeployment.project.name}-${newDeployment.id}` + }); + await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData }); return newDeployment; @@ -795,10 +797,30 @@ export class Service { const deployment = await this.db.getDeployment({ where: { id: deploymentId + }, + relations: { + project: true } }); if (deployment && deployment.applicationDeploymentRecordId) { + // If deployment is current, remove deployment for project subdomain as well + if (deployment.isCurrent) { + const currentDeploymentURL = `https://${deployment.project.subDomain}`; + + const deploymentRecords = await this.registry.getDeploymentRecordsByFilter({ + application: deployment.applicationRecordId, + url: currentDeploymentURL + }) + + if (!deploymentRecords.length) { + log(`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`); + return false; + } + + await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deploymentRecords[0].id }); + } + const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deployment.applicationDeploymentRecordId }); await this.db.updateDeploymentById( diff --git a/packages/backend/test/publish-deploy-records.ts b/packages/backend/test/publish-deploy-records.ts index 0819a91c..02bd1e4a 100644 --- a/packages/backend/test/publish-deploy-records.ts +++ b/packages/backend/test/publish-deploy-records.ts @@ -7,7 +7,7 @@ import { Registry } from '@cerc-io/laconic-sdk'; import { Config } from '../src/config'; import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; import { getConfig } from '../src/utils'; -import { Deployment, DeploymentStatus } from '../src/entity/Deployment'; +import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment'; const log = debug('snowball:publish-deploy-records'); @@ -40,7 +40,7 @@ async function main () { }); for await (const deployment of deployments) { - const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`; + const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`; const applicationDeploymentRecord = { type: 'ApplicationDeploymentRecord', @@ -71,6 +71,21 @@ async function main () { registryConfig.fee ); + // Remove deployment for project subdomain if deployment is for production environment + if (deployment.environment === Environment.Production) { + applicationDeploymentRecord.url = `https://${deployment.project.subDomain}` + + await registry.setRecord( + { + privateKey: registryConfig.privateKey, + record: applicationDeploymentRecord, + bondId: registryConfig.bondId + }, + '', + registryConfig.fee + ); + } + log('Application deployment record data:', applicationDeploymentRecord); log(`Application deployment record published: ${result.data.id}`); } diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index 4c55e246..0e605bc5 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -38,6 +38,7 @@ const STATUS_COLORS: { [DeploymentStatus.Building]: 'emphasized', [DeploymentStatus.Ready]: 'positive', [DeploymentStatus.Error]: 'negative', + [DeploymentStatus.Deleting]: 'neutral', }; const DeploymentDetailsCard = ({ @@ -48,7 +49,7 @@ const DeploymentDetailsCard = ({ prodBranchDomains, }: DeployDetailsCardProps) => { const getIconByDeploymentStatus = (status: DeploymentStatus) => { - if (status === DeploymentStatus.Building) { + if (status === DeploymentStatus.Building || status === DeploymentStatus.Deleting) { return ; } if (status === DeploymentStatus.Ready) { diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx index 1d6d33b0..5dbb95aa 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -161,11 +161,6 @@ export const DeploymentMenu = ({ deleteDeployment()} - disabled={ - deployment.isCurrent || - deployment.environment !== Environment.Production || - !Boolean(currentDeployment) - } > Delete deployment diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 786b7519..ac6481e0 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -21,6 +21,7 @@ export enum DeploymentStatus { Building = 'Building', Ready = 'Ready', Error = 'Error', + Deleting = 'Deleting' } export enum DomainStatus { From 8b39f664ad4fb2b663c040c1e2dfa1d1b50ce12c Mon Sep 17 00:00:00 2001 From: Nabarun Date: Thu, 25 Apr 2024 16:55:39 +0530 Subject: [PATCH 13/13] Replace REACT_APP env with VITE --- README.md | 20 +++++++++---------- build-webapp.sh | 10 +++++----- .../src/context/Web3ModalProvider.tsx | 2 +- packages/frontend/src/index.tsx | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ed2506a6..557b96ea 100644 --- a/README.md +++ b/README.md @@ -145,14 +145,14 @@ Let us assume the following domains for backend and frontend - Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file ```env - REACT_APP_GITHUB_CLIENT_ID = + VITE_GITHUB_CLIENT_ID = ``` - - Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file + - Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file ```env - REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo - REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo + VITE_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo + VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo ``` - Production @@ -160,17 +160,17 @@ Let us assume the following domains for backend and frontend - Set the following values in [.env](packages/frontend/.env) file ```env - REACT_APP_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint + VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint ``` - Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID - Create a project and add information to use wallet connect SDK - Add project name and select project type as `App` - Set project home page URL to `https://dashboard.snowballtools.com` - - On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file + - On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file ```env - REACT_APP_WALLET_CONNECT_ID = + VITE_WALLET_CONNECT_ID = ``` - Build the React application @@ -190,17 +190,17 @@ Let us assume the following domains for backend and frontend - Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend` ```env - REACT_APP_SERVER_URL = 'http://localhost:8000' + VITE_SERVER_URL = 'http://localhost:8000' ``` - Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID. - Create a project and add information to use wallet connect SDK - Add project name and select project type as `App` - Project home page URL is not required to be set - - On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file + - On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file ```env - REACT_APP_WALLET_CONNECT_ID = + VITE_WALLET_CONNECT_ID = ``` - The React application will be running in `http://localhost:3000/` diff --git a/build-webapp.sh b/build-webapp.sh index d350c0dc..d84fd6ba 100755 --- a/build-webapp.sh +++ b/build-webapp.sh @@ -10,11 +10,11 @@ if [[ -d "$DEST_DIR" ]]; then fi cat > $PKG_DIR/.env <