diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 4535ea27..3e3caaa6 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -28,6 +28,12 @@ export enum DeploymentStatus { Deleting = 'Deleting', } +export interface ApplicationDeploymentAuction { + application: string; + auction: string; + type: string; +} + export interface ApplicationDeploymentRequest { type: string; version: string; @@ -112,13 +118,13 @@ 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; @@ -147,7 +153,7 @@ export class Deployment { @UpdateDateColumn() updatedAt!: Date; - + @DeleteDateColumn() deletedAt!: Date | null; } diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index daab9ed9..743c052d 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -10,14 +10,16 @@ import { ApplicationRecord, Deployment, ApplicationDeploymentRequest, - ApplicationDeploymentRemovalRequest + ApplicationDeploymentRemovalRequest, + ApplicationDeploymentAuction } from './entity/Deployment'; -import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types'; +import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionData, PackageJSON } from './types'; import { sleep } from './utils'; const log = debug('snowball:registry'); const APP_RECORD_TYPE = 'ApplicationRecord'; +const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; @@ -148,10 +150,94 @@ export class Registry { }; } + async createApplicationDeploymentAuction (data: { + deployment: Deployment, + appName: string, + }, + auctionData: AuctionData, +): Promise<{ + applicationDeploymentAuctionId: string; + applicationDeploymentAuctionData: ApplicationDeploymentAuction; + deployerLrns: string[]; + }> { + const lrn = this.getLrn(data.appName); + const records = await this.registry.resolveNames([lrn]); + const applicationRecord = records[0]; + + if (!applicationRecord) { + throw new Error(`No record found for ${lrn}`); + } + + const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); + + // TODO: Take auction params from user + const auctionResult = await this.registry.createProviderAuction( + { + commitFee: auctionData.commitFee, + commitsDuration: auctionData.commitsDuration, + revealFee: auctionData.revealFee, + revealsDuration: auctionData.revealsDuration, + denom: auctionData.denom, + maxPrice: auctionData.maxPrice, + numProviders: auctionData.numProviders, + }, + this.registryConfig.privateKey, + fee + ) + + if (!auctionResult.auction) { + throw new Error('Error creating auction'); + } + + // Create record of type applicationDeploymentAuction and publish + const applicationDeploymentAuction = { + application: `${lrn}@${applicationRecord.attributes.app_version}`, + auction: auctionResult.auction.id, + type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE, + }; + + await sleep(SLEEP_DURATION); + + const result = await this.registry.setRecord( + { + privateKey: this.registryConfig.privateKey, + record: applicationDeploymentAuction, + bondId: this.registryConfig.bondId + }, + this.registryConfig.privateKey, + fee + ); + log(`Application deployment auction record published: ${result.id}`); + log('Application deployment auction data:', applicationDeploymentAuction); + + let deployerLrns = []; + const { winnerAddresses } = auctionResult.auction; + + for (const auctionWinner of winnerAddresses) { + const deployerRecord = await this.registry.queryRecords( + { + paymentAddress: auctionWinner, + }, + true + ); + + const lrn = deployerRecord.names.length > 0 ? deployerRecord.names[0] : null; + deployerLrns.push(lrn); + } + + return { + applicationDeploymentAuctionId: auctionResult.auction.id, + applicationDeploymentAuctionData: applicationDeploymentAuction, + deployerLrns + }; + } + async createApplicationDeploymentRequest (data: { deployment: Deployment, appName: string, repository: string, + auctionId?: string, + lrn?: string, environmentVariables: { [key: string]: string }, dns: string, }): Promise<{ @@ -175,8 +261,6 @@ export class Registry { dns: data.dns, // TODO: Not set in test-progressive-web-app CI - // deployment: '$CERC_REGISTRY_DEPLOYMENT_LRN', - // https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b config: JSON.stringify({ env: data.environmentVariables @@ -187,7 +271,9 @@ export class Registry { )}`, repository: data.repository, repository_ref: data.deployment.commitHash - }) + }), + ...(data.lrn && { deployer: data.lrn }), + ...(data.auctionId && { auction: data.auctionId }), }; await sleep(SLEEP_DURATION); diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 6eba446d..14361d5f 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -19,6 +19,7 @@ import { AddProjectFromTemplateInput, AppDeploymentRecord, AppDeploymentRemovalRecord, + AuctionData, GitPushEventPayload, PackageJSON, } from './types'; @@ -644,10 +645,145 @@ export class Service { return newDeployment; } + async createDeploymentFromAuction( + userId: string, + octokit: Octokit, + data: DeepPartial, + auctionData: AuctionData + ): Promise { + assert(data.project?.repository, 'Project repository not found'); + log( + `Creating deployment in project ${data.project.name} from branch ${data.branch}`, + ); + const [owner, repo] = data.project.repository.split('/'); + + const { data: packageJSONData } = await octokit.rest.repos.getContent({ + owner, + repo, + path: 'package.json', + ref: data.commitHash, + }); + + if (!packageJSONData) { + throw new Error('Package.json file not found'); + } + + assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file'); + const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content)); + + assert(packageJSON.name, "name field doesn't exist in package.json"); + + const repoUrl = ( + await octokit.rest.repos.get({ + owner, + repo, + }) + ).data.html_url; + + // TODO: Set environment variables for each deployment (environment variables can`t be set in application record) + const { applicationRecordId, applicationRecordData } = + await this.registry.createApplicationRecord({ + appName: repo, + packageJSON, + appType: data.project!.template!, + commitHash: data.commitHash!, + repoUrl, + }); + + // Update previous deployment with prod branch domain + // TODO: Fix unique constraint error for domain + if (data.domain) { + await this.db.updateDeployment( + { + domainId: data.domain.id, + }, + { + domain: null, + }, + ); + } + + const newDeployment = await this.db.addDeployment({ + project: data.project, + branch: data.branch, + commitHash: data.commitHash, + commitMessage: data.commitMessage, + environment: data.environment, + status: DeploymentStatus.Building, + applicationRecordId, + applicationRecordData, + domain: data.domain, + createdBy: Object.assign(new User(), { + id: userId, + }), + }); + + log( + `Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`, + ); + + const deploymentAuctionData = await this.registry.createApplicationDeploymentAuction({ + deployment: newDeployment, + appName: repo + }, auctionData + ); + + const deploymentAuctionId = deploymentAuctionData.applicationDeploymentAuctionId; + + const environmentVariables = + await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { + environment: Environment.Production, + }); + + const environmentVariablesObj = environmentVariables.reduce( + (acc, env) => { + acc[env.key] = env.value; + + return acc; + }, + {} as { [key: string]: string }, + ); + + // 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, + appName: repo, + repository: repoUrl, + environmentVariables: environmentVariablesObj, + dns: `${newDeployment.project.name}`, + }); + } + + for (const deployer in deploymentAuctionData.deployerLrns) { + const { applicationDeploymentRequestId, applicationDeploymentRequestData } = + // Create requests for all the deployers + await this.registry.createApplicationDeploymentRequest({ + deployment: newDeployment, + appName: repo, + repository: repoUrl, + auctionId: deploymentAuctionId, + lrn: deployer, + environmentVariables: environmentVariablesObj, + dns: `${newDeployment.project.name}-${newDeployment.id}`, + }); + + await this.db.updateDeploymentById(newDeployment.id, { + applicationDeploymentRequestId, + applicationDeploymentRequestData, + }); + } + + return newDeployment; + } + async addProjectFromTemplate( user: User, organizationSlug: string, data: AddProjectFromTemplateInput, + auctionData?: AuctionData ): Promise { try { const octokit = await this.getOctokit(user.id); @@ -678,7 +814,7 @@ export class Service { repository: gitRepo.data.full_name, // TODO: Set selected template template: 'webapp', - }); + }, auctionData); if (!project || !project.id) { throw new Error('Failed to create project from template'); @@ -695,6 +831,7 @@ export class Service { user: User, organizationSlug: string, data: DeepPartial, + auctiondata?: AuctionData ): Promise { const organization = await this.db.getOrganization({ where: { @@ -720,14 +857,18 @@ export class Service { }); // Create deployment with prod branch and latest commit - const deployment = await this.createDeployment(user.id, octokit, { + const deploymentData = { project, branch: project.prodBranch, environment: Environment.Production, domain: null, commitHash: latestCommit.sha, commitMessage: latestCommit.commit.message, - }); + }; + + const deployment = auctiondata + ? await this.createDeploymentFromAuction(user.id, octokit, deploymentData, auctiondata) + : await this.createDeployment(user.id, octokit, deploymentData); await this.createRepoHook(octokit, project); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 12a02776..66b10783 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -69,3 +69,13 @@ export interface AddProjectFromTemplateInput { name: string; isPrivate: boolean; } + +export interface AuctionData { + commitFee: string, + commitsDuration: string, + revealFee: string, + revealsDuration: string, + denom: string, + maxPrice: string, + numProviders: number, +}