diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index f20eb455..82bed68d 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -17,13 +17,14 @@ import { DatabaseConfig } from './config'; import { User } from './entity/User'; import { Organization } from './entity/Organization'; import { Project } from './entity/Project'; -import { Deployment } from './entity/Deployment'; +import { Deployment, DeploymentStatus } from './entity/Deployment'; import { ProjectMember } from './entity/ProjectMember'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { Domain } from './entity/Domain'; import { getEntities, loadAndSaveData } from './utils'; import { UserOrganization } from './entity/UserOrganization'; import { Deployer } from './entity/Deployer'; +import { DNSRecordAttributes } from './types'; const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'; @@ -60,7 +61,7 @@ export class Database { // Hotfix for updating old DB data if (organizations[0].slug === 'snowball-tools-1') { const [orgEntity] = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH)); - + await this.updateOrganization( organizations[0].id, { @@ -157,10 +158,9 @@ export class Database { .leftJoinAndSelect( 'project.deployments', 'deployments', - 'deployments.isCurrent = true' + 'deployments.isCurrent = true AND deployments.isCanonical = true' ) .leftJoinAndSelect('deployments.createdBy', 'user') - .leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.deployer', 'deployer') .leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.deployers', 'deployers') @@ -202,9 +202,8 @@ export class Database { .leftJoinAndSelect( 'project.deployments', 'deployments', - 'deployments.isCurrent = true' + 'deployments.isCurrent = true AND deployments.isCanonical = true' ) - .leftJoinAndSelect('deployments.domain', 'domain') .leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.organization', 'organization') .where( @@ -235,7 +234,6 @@ export class Database { return this.getDeployments({ relations: { project: true, - domain: true, createdBy: true, deployer: true, }, @@ -250,6 +248,25 @@ export class Database { }); } + async getNonCanonicalDeploymentsByProjectId(projectId: string): Promise { + return this.getDeployments({ + relations: { + project: true, + createdBy: true, + deployer: true, + }, + where: { + project: { + id: projectId + }, + isCanonical: false + }, + order: { + createdAt: 'DESC' + } + }); + } + async getDeployment( options: FindOneOptions ): Promise { @@ -602,6 +619,49 @@ export class Database { return domains; } + async getOldestDomainByProjectId( + projectId: string, + ): Promise { + const domainRepository = this.dataSource.getRepository(Domain); + + const domain = await domainRepository.findOne({ + where: { + project: { + id: projectId + }, + }, + order: { + createdAt: 'ASC' + } + }); + + return domain; + } + + async getLatestDNSRecordByProjectId( + projectId: string, + ): Promise { + const deploymentRepository = this.dataSource.getRepository(Deployment); + + const deployment = await deploymentRepository.findOne({ + where: { + project: { + id: projectId, + }, + status: DeploymentStatus.Ready, + }, + order: { + createdAt: 'DESC' + } + }); + + if (deployment === null) { + throw new Error(`No deployment found for project ${projectId}`); + } + + return deployment.dnsRecordData; + } + async addDeployer(data: DeepPartial): Promise { const deployerRepository = this.dataSource.getRepository(Deployer); const newDomain = await deployerRepository.save(data); diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 5c772b32..882233f7 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -14,7 +14,7 @@ import { Project } from './Project'; import { Domain } from './Domain'; import { User } from './User'; import { Deployer } from './Deployer'; -import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types'; +import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes, DNSRecordAttributes } from '../types'; export enum Environment { Production = 'Production', @@ -39,6 +39,7 @@ export interface ApplicationDeploymentRequest { config: string; meta: string; payment?: string; + dns?: string; } export interface ApplicationDeploymentRemovalRequest { @@ -77,13 +78,6 @@ export class Deployment { @JoinColumn({ name: 'projectId' }) project!: Project; - @Column({ nullable: true }) - domainId!: string | null; - - @OneToOne(() => Domain) - @JoinColumn({ name: 'domainId' }) - domain!: Domain | null; - @Column('varchar') branch!: string; @@ -126,6 +120,9 @@ export class Deployment { @Column('simple-json', { nullable: true }) applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null; + @Column('simple-json', { nullable: true }) + dnsRecordData!: DNSRecordAttributes | null; + @ManyToOne(() => Deployer) @JoinColumn({ name: 'deployerLrn' }) deployer!: Deployer; @@ -138,6 +135,9 @@ export class Deployment { @Column('boolean', { default: false }) isCurrent!: boolean; + @Column('boolean', { default: false }) + isCanonical!: boolean; + @Column({ enum: DeploymentStatus }) diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 4847bd6d..c7d8b8e0 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -16,7 +16,7 @@ import { ApplicationDeploymentRequest, ApplicationDeploymentRemovalRequest } from './entity/Deployment'; -import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types'; +import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord, RegistryRecord } from './types'; import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils'; const log = debug('snowball:registry'); @@ -27,7 +27,7 @@ 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 WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer' +const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'; const SLEEP_DURATION = 1000; // TODO: Move registry code to registry-sdk/watcher-ts @@ -108,19 +108,7 @@ export class Registry { ...(packageJSON.version && { app_version: packageJSON.version }) }; - const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); - - const result = await registryTransactionWithRetry(() => - this.registry.setRecord( - { - privateKey: this.registryConfig.privateKey, - record: applicationRecord, - bondId: this.registryConfig.bondId - }, - this.registryConfig.privateKey, - fee - ) - ); + const result = await this.publishRecord(applicationRecord); log(`Published application record ${result.id}`); log('Application record data:', applicationRecord); @@ -129,6 +117,8 @@ export class Registry { const lrn = this.getLrn(repo); log(`Setting name: ${lrn} for record ID: ${result.id}`); + const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); + await sleep(SLEEP_DURATION); await registryTransactionWithRetry(() => this.registry.setName( @@ -220,17 +210,7 @@ export class Registry { type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE, }; - const result = await registryTransactionWithRetry(() => - this.registry.setRecord( - { - privateKey: this.registryConfig.privateKey, - record: applicationDeploymentAuction, - bondId: this.registryConfig.bondId - }, - this.registryConfig.privateKey, - fee - ) - ); + const result = await this.publishRecord(applicationDeploymentAuction); log(`Application deployment auction created: ${auctionResult.auction.id}`); log(`Application deployment auction record published: ${result.id}`); @@ -265,12 +245,15 @@ export class Registry { throw new Error(`No record found for ${lrn}`); } - const hash = await this.generateConfigHash( - data.environmentVariables, - data.requesterAddress, - data.publicKey, - data.apiUrl, - ); + let hash; + if (Object.keys(data.environmentVariables).length !== 0) { + hash = await this.generateConfigHash( + data.environmentVariables, + data.requesterAddress, + data.publicKey, + data.apiUrl, + ); + } // Create record of type ApplicationDeploymentRequest and publish const applicationDeploymentRequest = { @@ -281,9 +264,7 @@ export class Registry { dns: data.dns, // https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b - config: JSON.stringify({ - ref: hash, - }), + config: JSON.stringify(hash ? { ref: hash } : {}), meta: JSON.stringify({ note: `Added by Snowball @ ${DateTime.utc().toFormat( "EEE LLL dd HH:mm:ss 'UTC' yyyy" @@ -298,19 +279,7 @@ export class Registry { await sleep(SLEEP_DURATION); - const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); - - const result = await registryTransactionWithRetry(() => - this.registry.setRecord( - { - privateKey: this.registryConfig.privateKey, - record: applicationDeploymentRequest, - bondId: this.registryConfig.bondId - }, - this.registryConfig.privateKey, - fee - ) - ); + const result = await this.publishRecord(applicationDeploymentRequest); log(`Application deployment request record published: ${result.id}`); log('Application deployment request data:', applicationDeploymentRequest); @@ -382,12 +351,11 @@ export class Registry { true ); - // Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL + // Filter records with ApplicationDeploymentRequestId ID return records.filter((record: AppDeploymentRecord) => deployments.some( (deployment) => - deployment.applicationDeploymentRequestId === record.attributes.request && - record.attributes.url.includes(deployment.id) + deployment.applicationDeploymentRequestId === record.attributes.request ) ); } @@ -442,6 +410,14 @@ export class Registry { ); } + /** + * Fetch record by Id + */ + async getRecordById(id: string): Promise { + const [record] = await this.registry.getRecordsByIds([id]); + return record ?? null; + } + async createApplicationDeploymentRemovalRequest(data: { deploymentId: string; deployerLrn: string; @@ -460,18 +436,8 @@ export class Registry { ...(data.payment && { payment: data.payment }), }; - const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); - - const result = await registryTransactionWithRetry(() => - this.registry.setRecord( - { - privateKey: this.registryConfig.privateKey, - record: applicationDeploymentRemovalRequest, - bondId: this.registryConfig.bondId - }, - this.registryConfig.privateKey, - fee - ) + const result = await this.publishRecord( + applicationDeploymentRemovalRequest, ); log(`Application deployment removal request record published: ${result.id}`); @@ -497,6 +463,27 @@ export class Registry { return completedAuctions; } + async publishRecord(recordData: any): Promise { + const fee = parseGasAndFees( + this.registryConfig.fee.gas, + this.registryConfig.fee.fees, + ); + + const result = await registryTransactionWithRetry(() => + this.registry.setRecord( + { + privateKey: this.registryConfig.privateKey, + record: recordData, + bondId: this.registryConfig.bondId, + }, + this.registryConfig.privateKey, + fee, + ), + ); + + return result; + } + async getRecordsByName(name: string): Promise { return this.registry.resolveNames([name]); } diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index e20b7e0c..d9f9128d 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -38,7 +38,7 @@ export const createResolvers = async (service: Service): Promise => { }, deployments: async (_: any, { projectId }: { projectId: string }) => { - return service.getDeploymentsByProjectId(projectId); + return service.getNonCanonicalDeploymentsByProjectId(projectId); }, environmentVariables: async ( @@ -95,6 +95,13 @@ export const createResolvers = async (service: Service): Promise => { ) => { return service.verifyTx(txHash, amount, senderAddress); }, + + latestDNSRecord: async ( + _: any, + { projectId }: { projectId: string }, + ) => { + return service.getLatestDNSRecordByProjectId(projectId); + }, }, // TODO: Return error in GQL response diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index ff51098a..400e36ef 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -100,7 +100,6 @@ type ProjectMember { type Deployment { id: String! - domain: Domain branch: String! commitHash: String! commitMessage: String! @@ -108,6 +107,7 @@ type Deployment { environment: Environment! deployer: Deployer applicationDeploymentRequestId: String + applicationDeploymentRecordData: AppDeploymentRecordAttributes isCurrent: Boolean! baseDomain: String status: DeploymentStatus! @@ -249,6 +249,27 @@ type Auction { bids: [Bid!]! } +type DNSRecordAttributes { + name: String + value: String + request: String + resourceType: String + version: String +} + +type AppDeploymentRecordAttributes { + application: String + auction: String + deployer: String + dns: String + meta: String + name: String + request: String + type: String + url: String + version: String +} + input AuctionParams { maxPrice: String, numProviders: Int, @@ -265,6 +286,7 @@ type Query { projectMembers(projectId: String!): [ProjectMember!] searchProjects(searchText: String!): [Project!] getAuctionData(auctionId: String!): Auction! + latestDNSRecord(projectId: String!): DNSRecordAttributes domains(projectId: String!, filter: FilterDomainsInput): [Domain] deployers: [Deployer] address: String! diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index aa8736a0..0321df01 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -1,7 +1,8 @@ import assert from 'assert'; import debug from 'debug'; -import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm'; +import { DeepPartial, FindOptionsWhere } from 'typeorm'; import { Octokit, RequestError } from 'octokit'; +import { DateTime } from 'luxon'; import { OAuthApp } from '@octokit/oauth-app'; @@ -22,6 +23,8 @@ import { AppDeploymentRemovalRecord, AuctionParams, DeployerRecord, + DNSRecord, + DNSRecordAttributes, EnvironmentVariables, GitPushEventPayload, } from './types'; @@ -199,52 +202,106 @@ export class Service { if (!deployment.project) { log(`Project ${deployment.projectId} not found`); return; - } else { - deployment.applicationDeploymentRecordId = record.id; - deployment.applicationDeploymentRecordData = record.attributes; - deployment.url = record.attributes.url; - deployment.status = DeploymentStatus.Ready; - deployment.isCurrent = deployment.environment === Environment.Production; - - await this.db.updateDeploymentById(deployment.id, deployment); - - // Release deployer funds on successful deployment - if (!deployment.project.fundsReleased) { - const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId); - - // Return remaining amount to owner - await this.returnUserFundsByProjectId(deployment.projectId, true); - - await this.db.updateProjectById(deployment.projectId, { - fundsReleased, - }); - } - - log( - `Updated deployment ${deployment.id} with URL ${record.attributes.url}`, - ); } + + const registryRecord = await this.laconicRegistry.getRecordById(record.attributes.dns); + + if (!registryRecord) { + log(`DNS record not found for deployment ${deployment.id}`); + return; + } + + const dnsRecord = registryRecord as DNSRecord; + + const dnsRecordData: DNSRecordAttributes = { + name: dnsRecord.attributes.name, + request: dnsRecord.attributes.request, + resourceType: dnsRecord.attributes.resource_type, + value: dnsRecord.attributes.value, + version: dnsRecord.attributes.version, + } + + deployment.applicationDeploymentRecordId = record.id; + deployment.applicationDeploymentRecordData = record.attributes; + deployment.url = record.attributes.url; + deployment.status = DeploymentStatus.Ready; + deployment.isCurrent = deployment.environment === Environment.Production; + deployment.dnsRecordData = dnsRecordData; + + if (deployment.isCanonical) { + const previousCanonicalDeployment = await this.db.getDeployment({ + where: { + projectId: deployment.project.id, + deployer: deployment.deployer, + isCanonical: true, + isCurrent: true, + }, + relations: { + project: true, + deployer: true, + } + }); + + if (previousCanonicalDeployment) { + // Send removal request for the previous canonical deployment and delete DB entry + if (previousCanonicalDeployment.url !== deployment.url) { + await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ + deploymentId: previousCanonicalDeployment.applicationDeploymentRecordId!, + deployerLrn: previousCanonicalDeployment.deployer.deployerLrn, + auctionId: previousCanonicalDeployment.project.auctionId, + payment: previousCanonicalDeployment.project.txHash + }); + } + + await this.db.deleteDeploymentById(previousCanonicalDeployment.id); + } + } + + await this.db.updateDeploymentById(deployment.id, deployment); + + // Release deployer funds on successful deployment + if (!deployment.project.fundsReleased) { + const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId); + + // Return remaining amount to owner + await this.returnUserFundsByProjectId(deployment.projectId, true); + + await this.db.updateProjectById(deployment.projectId, { + fundsReleased, + }); + } + + log( + `Updated deployment ${deployment.id} with URL ${record.attributes.url}`, + ); }); await Promise.all(deploymentUpdatePromises); // Get deployments that are in production environment - const prodDeployments = Object.values(recordToDeploymentsMap).filter(deployment => deployment.isCurrent); - + const prodDeployments = Object.values(recordToDeploymentsMap).filter( + (deployment) => deployment.isCurrent, + ); // Set the isCurrent state to false for the old deployments for (const deployment of prodDeployments) { - const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId); - const oldDeployments = projectDeployments - .filter(projectDeployment => projectDeployment.deployer.deployerLrn === deployment.deployer.deployerLrn && projectDeployment.id !== deployment.id); + const projectDeployments = await this.db.getDeploymentsByProjectId( + deployment.projectId, + ); + + const oldDeployments = projectDeployments.filter( + (projectDeployment) => + projectDeployment.deployer.deployerLrn === + deployment.deployer.deployerLrn && + projectDeployment.id !== deployment.id && + projectDeployment.isCanonical == deployment.isCanonical, + ); for (const oldDeployment of oldDeployments) { await this.db.updateDeployment( { id: oldDeployment.id }, - { isCurrent: false } + { isCurrent: false }, ); } } - - await Promise.all(deploymentUpdatePromises); } /** @@ -428,9 +485,14 @@ export class Service { return dbProjects; } - async getDeploymentsByProjectId(projectId: string): Promise { - const dbDeployments = await this.db.getDeploymentsByProjectId(projectId); - return dbDeployments; + async getNonCanonicalDeploymentsByProjectId(projectId: string): Promise { + const nonCanonicalDeployments = await this.db.getNonCanonicalDeploymentsByProjectId(projectId); + return nonCanonicalDeployments; + } + + async getLatestDNSRecordByProjectId(projectId: string): Promise { + const dnsRecord = await this.db.getLatestDNSRecordByProjectId(projectId); + return dnsRecord; } async getEnvironmentVariablesByProjectId( @@ -572,6 +634,7 @@ export class Service { where: { id: deploymentId }, relations: { project: true, + deployer: true, }, }); @@ -579,18 +642,12 @@ export class Service { throw new Error('Deployment does not exist'); } - const prodBranchDomains = await this.db.getDomainsByProjectId( - oldDeployment.project.id, - { branch: oldDeployment.project.prodBranch }, - ); - const octokit = await this.getOctokit(user.id); const newDeployment = await this.createDeployment(user.id, octokit, { project: oldDeployment.project, branch: oldDeployment.branch, environment: Environment.Production, - domain: prodBranchDomains[0], commitHash: oldDeployment.commitHash, commitMessage: oldDeployment.commitMessage, deployer: oldDeployment.deployer @@ -619,19 +676,6 @@ export class Service { commitHash: data.commitHash!, }); - // 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, - }, - ); - } - let deployer; if (deployerLrn) { deployer = await this.db.getDeployerByLRN(deployerLrn); @@ -639,51 +683,62 @@ export class Service { deployer = data.deployer; } - const newDeployment = await this.createDeploymentFromData(userId, data, deployer!.deployerLrn!, applicationRecordId, applicationRecordData); + const deployment = await this.createDeploymentFromData(userId, data, deployer!.deployerLrn!, applicationRecordId, applicationRecordData, false); const address = await this.getAddress(); const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash); const environmentVariablesObj = await this.getEnvVariables(data.project!.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.laconicRegistry.createApplicationDeploymentRequest({ - deployment: newDeployment, - appName: repo, - repository: repoUrl, - environmentVariables: environmentVariablesObj, - dns: `${newDeployment.project.name}`, - lrn: deployer!.deployerLrn!, - apiUrl: deployer!.deployerApiUrl!, - payment: data.project.txHash, - auctionId: data.project.auctionId, - requesterAddress: address, - publicKey: deployer!.publicKey! + const canonicalDeployment = await this.createDeploymentFromData(userId, data, deployer!.deployerLrn!, applicationRecordId, applicationRecordData, true); + // If a custom domain is present then use that as the DNS in the deployment request + const customDomain = await this.db.getOldestDomainByProjectId(data.project!.id!); + + // On deleting deployment later, project canonical deployment is also deleted + // So publish project canonical deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later + const { applicationDeploymentRequestData, applicationDeploymentRequestId } = + await this.laconicRegistry.createApplicationDeploymentRequest({ + deployment: canonicalDeployment, + appName: repo, + repository: repoUrl, + environmentVariables: environmentVariablesObj, + dns: customDomain?.name ?? `${canonicalDeployment.project.name}`, + lrn: deployer!.deployerLrn!, + apiUrl: deployer!.deployerApiUrl!, + payment: data.project.txHash, + auctionId: data.project.auctionId, + requesterAddress: address, + publicKey: deployer!.publicKey! + }); + + await this.db.updateDeploymentById(canonicalDeployment.id, { + applicationDeploymentRequestId, + applicationDeploymentRequestData, }); } const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.laconicRegistry.createApplicationDeploymentRequest({ - deployment: newDeployment, + deployment: deployment, appName: repo, repository: repoUrl, lrn: deployer!.deployerLrn!, apiUrl: deployer!.deployerApiUrl!, environmentVariables: environmentVariablesObj, - dns: `${newDeployment.project.name}-${newDeployment.id}`, + dns: `${deployment.project.name}-${deployment.id}`, payment: data.project.txHash, auctionId: data.project.auctionId, requesterAddress: address, publicKey: deployer!.publicKey! }); - await this.db.updateDeploymentById(newDeployment.id, { + await this.db.updateDeploymentById(deployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData, }); - return newDeployment; + return deployment; } async createDeploymentFromAuction( @@ -726,49 +781,59 @@ export class Service { commitMessage: latestCommit.commit.message, }; - const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData); + const deployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData, false); const address = await this.getAddress(); const environmentVariablesObj = await this.getEnvVariables(project!.id!); // To set project DNS if (deploymentData.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.laconicRegistry.createApplicationDeploymentRequest({ - deployment: newDeployment, - appName: repo, - repository: repoUrl, - environmentVariables: environmentVariablesObj, - dns: `${newDeployment.project.name}`, - auctionId: project.auctionId!, - lrn: deployerLrn, - apiUrl: deployer!.deployerApiUrl!, - requesterAddress: address, - publicKey: deployer!.publicKey! + const canonicalDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData, true); + // If a custom domain is present then use that as the DNS in the deployment request + const customDomain = await this.db.getOldestDomainByProjectId(project!.id!); + + // On deleting deployment later, project canonical deployment is also deleted + // So publish project canonical deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later + const { applicationDeploymentRequestId, applicationDeploymentRequestData } = + await this.laconicRegistry.createApplicationDeploymentRequest({ + deployment: canonicalDeployment, + appName: repo, + repository: repoUrl, + environmentVariables: environmentVariablesObj, + dns: customDomain?.name ?? `${canonicalDeployment.project.name}`, + auctionId: project.auctionId!, + lrn: deployerLrn, + apiUrl: deployer!.deployerApiUrl!, + requesterAddress: address, + publicKey: deployer!.publicKey! + }); + + await this.db.updateDeploymentById(canonicalDeployment.id, { + applicationDeploymentRequestId, + applicationDeploymentRequestData, }); } const { applicationDeploymentRequestId, applicationDeploymentRequestData } = // Create requests for all the deployers await this.laconicRegistry.createApplicationDeploymentRequest({ - deployment: newDeployment, + deployment: deployment, appName: repo, repository: repoUrl, auctionId: project.auctionId!, lrn: deployerLrn, apiUrl: deployer!.deployerApiUrl!, environmentVariables: environmentVariablesObj, - dns: `${newDeployment.project.name}-${newDeployment.id}`, + dns: `${deployment.project.name}-${deployment.id}`, requesterAddress: address, publicKey: deployer!.publicKey! }); - await this.db.updateDeploymentById(newDeployment.id, { + await this.db.updateDeploymentById(deployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData, }); - return newDeployment; + return deployment; } async createDeploymentFromData( @@ -777,6 +842,7 @@ export class Service { deployerLrn: string, applicationRecordId: string, applicationRecordData: ApplicationRecord, + isCanonical: boolean, ): Promise { const newDeployment = await this.db.addDeployment({ project: data.project, @@ -787,13 +853,13 @@ export class Service { status: DeploymentStatus.Building, applicationRecordId, applicationRecordData, - domain: data.domain, createdBy: Object.assign(new User(), { id: userId, }), deployer: Object.assign(new Deployer(), { deployerLrn, }), + isCanonical }); log(`Created deployment ${newDeployment.id}`); @@ -1026,9 +1092,6 @@ export class Service { for await (const project of projects) { const octokit = await this.getOctokit(project.ownerId); - const [domain] = await this.db.getDomainsByProjectId(project.id, { - branch, - }); const deployers = project.deployers; if (!deployers) { @@ -1046,7 +1109,6 @@ export class Service { project.prodBranch === branch ? Environment.Production : Environment.Preview, - domain, commitHash: headCommit.id, commitMessage: headCommit.message, deployer: deployer @@ -1088,7 +1150,6 @@ export class Service { const oldDeployment = await this.db.getDeployment({ relations: { project: true, - domain: true, deployer: true, createdBy: true, }, @@ -1114,7 +1175,6 @@ export class Service { // TODO: Put isCurrent field in project branch: oldDeployment.branch, environment: Environment.Production, - domain: oldDeployment.domain, commitHash: oldDeployment.commitHash, commitMessage: oldDeployment.commitMessage, deployer: oldDeployment.deployer @@ -1132,31 +1192,79 @@ export class Service { // TODO: Implement transactions const oldCurrentDeployment = await this.db.getDeployment({ relations: { - domain: true, + project: true, + deployer: true, }, where: { project: { id: projectId, }, isCurrent: true, + isCanonical: false, }, }); if (!oldCurrentDeployment) { - throw new Error('Current deployment doesnot exist'); + throw new Error('Current deployment does not exist'); } const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById( oldCurrentDeployment.id, - { isCurrent: false, domain: null }, + { isCurrent: false }, ); const newCurrentDeploymentUpdate = await this.db.updateDeploymentById( deploymentId, - { isCurrent: true, domain: oldCurrentDeployment?.domain }, + { isCurrent: true }, ); - return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate; + if (!newCurrentDeploymentUpdate || !oldCurrentDeploymentUpdate){ + return false; + } + + const newCurrentDeployment = await this.db.getDeployment({ where: { id: deploymentId }, relations: { project: true, deployer: true } }); + + if (!newCurrentDeployment) { + throw new Error(`Deployment with Id ${deploymentId} not found`); + } + + const applicationDeploymentRequestData = newCurrentDeployment.applicationDeploymentRequestData; + + const customDomain = await this.db.getOldestDomainByProjectId(projectId); + + if (customDomain && applicationDeploymentRequestData) { + applicationDeploymentRequestData.dns = customDomain.name + } + + // Create a canonical deployment for the new current deployment + const canonicalDeployment = await this.createDeploymentFromData( + newCurrentDeployment.project.ownerId, + newCurrentDeployment, + newCurrentDeployment.deployer!.deployerLrn!, + newCurrentDeployment.applicationRecordId, + newCurrentDeployment.applicationRecordData, + true, + ); + + applicationDeploymentRequestData!.meta = JSON.stringify({ + ...JSON.parse(applicationDeploymentRequestData!.meta), + note: `Updated by Snowball @ ${DateTime.utc().toFormat( + "EEE LLL dd HH:mm:ss 'UTC' yyyy" + )}` + }); + + const result = await this.laconicRegistry.publishRecord( + applicationDeploymentRequestData, + ); + + log(`Application deployment request record published: ${result.id}`) + + const updateResult = await this.db.updateDeploymentById(canonicalDeployment.id, { + applicationDeploymentRequestId: result.id, + applicationDeploymentRequestData, + }); + + return updateResult; } async deleteDeployment(deploymentId: string): Promise { @@ -1173,33 +1281,65 @@ export class Service { if (deployment && deployment.applicationDeploymentRecordId) { // If deployment is current, remove deployment for project subdomain as well if (deployment.isCurrent) { - const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.deployer.baseDomain}`; + const canonicalDeployment = await this.db.getDeployment({ + where: { + projectId: deployment.project.id, + deployer: deployment.deployer, + isCanonical: true + }, + relations: { + project: true, + deployer: true, + }, + }) - // TODO: Store the latest DNS deployment record - const deploymentRecords = - await this.laconicRegistry.getDeploymentRecordsByFilter({ - application: deployment.applicationRecordId, - url: currentDeploymentURL, + // If the canonical deployment is not present then query the chain for the deployment record for backward compatibility + if (!canonicalDeployment) { + log(`Canonical deployment for deployment with id ${deployment.id} not found, querying the chain..`); + const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.deployer.baseDomain}`; + + const deploymentRecords = + await this.laconicRegistry.getDeploymentRecordsByFilter({ + application: deployment.applicationRecordId, + url: currentDeploymentURL, + }); + + if (!deploymentRecords.length) { + log( + `No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`, + ); + + return false; + } + + // Multiple records are fetched, take the latest record + const latestRecord = deploymentRecords + .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())[0]; + + await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ + deploymentId: latestRecord.id, + deployerLrn: deployment.deployer.deployerLrn, + auctionId: deployment.project.auctionId, + payment: deployment.project.txHash }); + } else { + // If canonical deployment is found in the DB, then send the removal request with that deployment record Id + const result = + await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ + deploymentId: canonicalDeployment.applicationDeploymentRecordId!, + deployerLrn: canonicalDeployment.deployer.deployerLrn, + auctionId: canonicalDeployment.project.auctionId, + payment: canonicalDeployment.project.txHash + }); - if (!deploymentRecords.length) { - log( - `No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`, - ); - - return false; + await this.db.updateDeploymentById(canonicalDeployment.id, { + status: DeploymentStatus.Deleting, + applicationDeploymentRemovalRequestId: + result.applicationDeploymentRemovalRequestId, + applicationDeploymentRemovalRequestData: + result.applicationDeploymentRemovalRequestData, + }); } - - // Multiple records are fetched, take the latest record - const latestRecord = deploymentRecords - .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())[0]; - - await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ - deploymentId: latestRecord.id, - deployerLrn: deployment.deployer.deployerLrn, - auctionId: deployment.project.auctionId, - payment: deployment.project.txHash - }); } const result = @@ -1229,7 +1369,7 @@ export class Service { data: { name: string }, ): Promise<{ primaryDomain: Domain; - redirectedDomain: Domain; + // redirectedDomain: Domain; }> { const currentProject = await this.db.getProjectById(projectId); @@ -1245,22 +1385,22 @@ export class Service { const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails); - const domainArr = data.name.split('www.'); + // const domainArr = data.name.split('www.'); - const redirectedDomainDetails = { - name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`, - branch: currentProject.prodBranch, - project: currentProject, - redirectTo: savedPrimaryDomain, - }; + // const redirectedDomainDetails = { + // name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`, + // branch: currentProject.prodBranch, + // project: currentProject, + // redirectTo: savedPrimaryDomain, + // }; - const savedRedirectedDomain = await this.db.addDomain( - redirectedDomainDetails, - ); + // const savedRedirectedDomain = await this.db.addDomain( + // redirectedDomainDetails, + // ); return { primaryDomain: savedPrimaryDomain, - redirectedDomain: savedRedirectedDomain, + // redirectedDomain: savedRedirectedDomain, }; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 83c810ae..f02998ff 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -40,6 +40,22 @@ export interface AppDeploymentRecordAttributes { version: string; } +export interface DNSRecordAttributes { + name: string; + value: string; + request: string; + resourceType: string; + version: string; +} + +export interface RegistryDNSRecordAttributes { + name: string; + value: string; + request: string; + resource_type: string; + version: string; +} + export interface AppDeploymentRemovalRecordAttributes { deployment: string; request: string; @@ -47,7 +63,7 @@ export interface AppDeploymentRemovalRecordAttributes { version: string; } -interface RegistryRecord { +export interface RegistryRecord { id: string; names: string[] | null; owners: string[]; @@ -64,6 +80,10 @@ export interface AppDeploymentRemovalRecord extends RegistryRecord { attributes: AppDeploymentRemovalRecordAttributes; } +export interface DNSRecord extends RegistryRecord { + attributes: RegistryDNSRecordAttributes +} + export interface AddProjectFromTemplateInput { templateOwner: string; templateRepo: string; diff --git a/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx b/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx index c7798279..11b28506 100644 --- a/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx +++ b/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx @@ -4,7 +4,11 @@ import { MenuItem, MenuList, } from '@snowballtools/material-tailwind-react-fork'; -import { ComponentPropsWithoutRef, MouseEvent, useCallback } from 'react'; +import { + ComponentPropsWithoutRef, + MouseEvent, + useCallback, +} from 'react'; import { useNavigate } from 'react-router-dom'; import { Project } from 'gql-client'; import { Avatar } from 'components/shared/Avatar'; @@ -83,7 +87,7 @@ export const ProjectCard = ({

{project.name}

- {project.deployments[0]?.domain?.name ?? 'No domain'} + {project.deployments[0]?.applicationDeploymentRecordData?.url ?? 'No domain'}

{/* Icons */} diff --git a/packages/frontend/src/components/projects/create/Configure.tsx b/packages/frontend/src/components/projects/create/Configure.tsx index 98675186..66a9c60a 100644 --- a/packages/frontend/src/components/projects/create/Configure.tsx +++ b/packages/frontend/src/components/projects/create/Configure.tsx @@ -566,7 +566,7 @@ const Configure = () => { - - - - ); -}; - -export default AssignDomainDialog; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx index 118dca39..cb87aa98 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -10,7 +10,6 @@ import { import { Deployment, Domain, Environment, Project } from 'gql-client'; import { Button } from 'components/shared/Button'; import { - GlobeIcon, HorizontalDotIcon, LinkIcon, RefreshIcon, @@ -18,7 +17,6 @@ import { UndoIcon, CrossCircleIcon, } from 'components/shared/CustomIcon'; -import AssignDomainDialog from './AssignDomainDialog'; import { useGQLClient } from 'context/GQLClientContext'; import { cn } from 'utils/classnames'; import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog'; @@ -49,8 +47,8 @@ export const DeploymentMenu = ({ const [redeployToProduction, setRedeployToProduction] = useState(false); const [deleteDeploymentDialog, setDeleteDeploymentDialog] = useState(false); const [isConfirmDeleteLoading, setIsConfirmDeleteLoading] = useState(false); + const [isConfirmUpdateLoading, setIsConfirmUpdateLoading] = useState(false); const [rollbackDeployment, setRollbackDeployment] = useState(false); - const [assignDomainDialog, setAssignDomainDialog] = useState(false); const [isConfirmButtonLoading, setConfirmButtonLoadingLoading] = useState(false); @@ -58,6 +56,8 @@ export const DeploymentMenu = ({ const isUpdated = await client.updateDeploymentToProd(deployment.id); if (isUpdated.updateDeploymentToProd) { await onUpdate(); + setIsConfirmUpdateLoading(false); + toast({ id: 'deployment_changed_to_production', title: 'Deployment changed to production', @@ -102,6 +102,8 @@ export const DeploymentMenu = ({ ); if (isRollbacked.rollbackDeployment) { await onUpdate(); + setIsConfirmUpdateLoading(false); + toast({ id: 'deployment_rolled_back', title: 'Deployment rolled back', @@ -173,12 +175,6 @@ export const DeploymentMenu = ({ Visit - setAssignDomainDialog(!assignDomainDialog)} - > - Assign domain - setChangeToProduction(!changeToProduction)} @@ -226,9 +222,11 @@ export const DeploymentMenu = ({ handleCancel={() => setChangeToProduction((preVal) => !preVal)} open={changeToProduction} handleConfirm={async () => { + setIsConfirmUpdateLoading(true); await updateDeployment(); setChangeToProduction((preVal) => !preVal); }} + isConfirmButtonLoading={isConfirmUpdateLoading} deployment={deployment} domains={prodBranchDomains} /> @@ -243,7 +241,7 @@ export const DeploymentMenu = ({ setRedeployToProduction((preVal) => !preVal); }} deployment={deployment} - domains={deployment.domain ? [deployment.domain] : []} + domains={prodBranchDomains} isConfirmButtonLoading={isConfirmButtonLoading} /> {Boolean(currentDeployment) && ( @@ -253,18 +251,16 @@ export const DeploymentMenu = ({ open={rollbackDeployment} confirmButtonTitle="Rollback" handleConfirm={async () => { + setIsConfirmUpdateLoading(true); await rollbackDeploymentHandler(); setRollbackDeployment((preVal) => !preVal); }} deployment={currentDeployment} newDeployment={deployment} - domains={currentDeployment.domain ? [currentDeployment.domain] : []} + domains={prodBranchDomains} + isConfirmButtonLoading={isConfirmUpdateLoading} /> )} - setAssignDomainDialog(!assignDomainDialog)} - /> { diff --git a/packages/frontend/src/components/projects/project/settings/DomainCard.tsx b/packages/frontend/src/components/projects/project/settings/DomainCard.tsx index 28775e96..2fe0b05d 100644 --- a/packages/frontend/src/components/projects/project/settings/DomainCard.tsx +++ b/packages/frontend/src/components/projects/project/settings/DomainCard.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { Domain, DomainStatus, Project } from 'gql-client'; +import { useEffect, useState } from 'react'; +import { DNSRecordAttributes, Domain, DomainStatus, Project } from 'gql-client'; import { Typography, @@ -14,22 +14,27 @@ import EditDomainDialog from './EditDomainDialog'; import { useGQLClient } from 'context/GQLClientContext'; import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog'; import { useToast } from 'components/shared/Toast'; -import { Tag } from 'components/shared/Tag'; -import { - CheckIcon, - CrossIcon, - GearIcon, - LoadingIcon, -} from 'components/shared/CustomIcon'; +import { GearIcon } from 'components/shared/CustomIcon'; import { Heading } from 'components/shared/Heading'; import { Button } from 'components/shared/Button'; +import { useParams } from 'react-router-dom'; -enum RefreshStatus { - IDLE, - CHECKING, - CHECK_SUCCESS, - CHECK_FAIL, -} +// NOTE: Commented code for verify domain functionality +// import { Tag } from 'components/shared/Tag'; +// import { +// CheckIcon, +// CrossIcon, +// LoadingIcon, +// } from 'components/shared/CustomIcon'; + +// enum RefreshStatus { +// IDLE, +// CHECKING, +// CHECK_SUCCESS, +// CHECK_FAIL, +// } + +// const CHECK_FAIL_TIMEOUT = 5000; // In milliseconds interface DomainCardProps { domains: Domain[]; @@ -39,14 +44,6 @@ interface DomainCardProps { onUpdate: () => Promise; } -const CHECK_FAIL_TIMEOUT = 5000; // In milliseconds - -// TODO: Get domain record -const DOMAIN_RECORD = { - type: 'A', - name: '@', - value: '56.49.19.21', -}; const DomainCard = ({ domains, @@ -56,9 +53,11 @@ const DomainCard = ({ onUpdate, }: DomainCardProps) => { const { toast, dismiss } = useToast(); - const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE); + const { id } = useParams(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false); + const [dnsRecord, setDnsRecord] = useState(null); + // const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE); const client = useGQLClient(); @@ -83,13 +82,33 @@ const DomainCard = ({ } }; + useEffect(() => { + const fetchDNSData = async () => { + if (id === undefined) { + toast({ + id: 'domain_cannot_find_project', + title: 'Cannot find project', + variant: 'error', + onDismiss: dismiss, + }); + return; + } + + const dnsRecordResponse = await client.getLatestDNSRecordByProjectId(id); + + setDnsRecord(dnsRecordResponse.latestDNSRecord); + }; + + fetchDNSData(); + }, [id, client]); + return ( <>
{domain.name}{' '} - {domain.status} - + */}
- { @@ -122,7 +141,7 @@ const DomainCard = ({ ) : ( 'L' )} - + */}
- )} + )} */} @@ -189,11 +208,15 @@ const DomainCard = ({ - - - - - + {dnsRecord ? ( + + + + + + ) : ( +

DNS record data not available

+ )}
{DOMAIN_RECORD.type}{DOMAIN_RECORD.name}{DOMAIN_RECORD.value}
{dnsRecord.resourceType}@{dnsRecord.value ?? 'Not Configured'}
diff --git a/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx b/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx index 3607c6b1..a677d0bd 100644 --- a/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx +++ b/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx @@ -1,11 +1,15 @@ -import { useCallback, useEffect, useMemo } from 'react'; -import { Controller, useForm, SubmitHandler } from 'react-hook-form'; +import { + useCallback, + useEffect, +} from 'react'; +import { + useForm, + SubmitHandler, +} from 'react-hook-form'; import { Domain } from 'gql-client'; import { Typography, - Select, - Option, } from '@snowballtools/material-tailwind-react-fork'; import { useGQLClient } from 'context/GQLClientContext'; @@ -14,7 +18,15 @@ import { Button } from 'components/shared/Button'; import { Input } from 'components/shared/Input'; import { useToast } from 'components/shared/Toast'; -const DEFAULT_REDIRECT_OPTIONS = ['none']; +// NOTE: Commented code for redirect domain functionality +// import { +// Select, +// Option, +// } from '@snowballtools/material-tailwind-react-fork'; +// import { Controller } from 'react-hook-form'; +// import { useMemo } from 'react'; + +// const DEFAULT_REDIRECT_OPTIONS = ['none']; interface EditDomainDialogProp { domains: Domain[]; @@ -28,7 +40,7 @@ interface EditDomainDialogProp { type EditDomainValues = { name: string; branch: string; - redirectedTo: string; + // redirectedTo: string; }; const EditDomainDialog = ({ @@ -42,58 +54,58 @@ const EditDomainDialog = ({ const client = useGQLClient(); const { toast, dismiss } = useToast(); - const getRedirectUrl = (domain: Domain) => { - const redirectDomain = domain.redirectTo; + // const getRedirectUrl = (domain: Domain) => { + // const redirectDomain = domain.redirectTo; - if (redirectDomain !== null) { - return redirectDomain?.name; - } else { - return 'none'; - } - }; + // if (redirectDomain !== null) { + // return redirectDomain?.name; + // } else { + // return 'none'; + // } + // }; - const redirectOptions = useMemo(() => { - const domainNames = domains - .filter((domainData) => domainData.id !== domain.id) - .map((domain) => domain.name); - return ['none', ...domainNames]; - }, [domain, domains]); + // const redirectOptions = useMemo(() => { + // const domainNames = domains + // .filter((domainData) => domainData.id !== domain.id) + // .map((domain) => domain.name); + // return ['none', ...domainNames]; + // }, [domain, domains]); - const domainRedirectedFrom = useMemo(() => { - return domains.find( - (domainData) => domainData.redirectTo?.id === domain.id, - ); - }, [domains, domain]); + // const domainRedirectedFrom = useMemo(() => { + // return domains.find( + // (domainData) => domainData.redirectTo?.id === domain.id, + // ); + // }, [domains, domain]); - const isDisableDropdown = useMemo(() => { - return domainRedirectedFrom !== undefined; - }, [domain, domains]); + // const isDisableDropdown = useMemo(() => { + // return domainRedirectedFrom !== undefined; + // }, [domain, domains]); const { handleSubmit, register, - control, - watch, + // control, + // watch, reset, formState: { isValid, isDirty }, } = useForm({ defaultValues: { name: domain.name, branch: domain.branch, - redirectedTo: getRedirectUrl(domain), + // redirectedTo: getRedirectUrl(domain), }, }); const updateDomainHandler: SubmitHandler = useCallback( async (data) => { - const domainRedirectTo = domains.find( - (domainData) => data.redirectedTo === domainData.name, - ); + // const domainRedirectTo = domains.find( + // (domainData) => data.redirectedTo === domainData.name, + // ); const updates = { name: data.name ? data.name : domain.name, branch: data.branch ? data.branch : domain.branch, - redirectToId: domainRedirectTo ? domainRedirectTo.id : null, + // redirectToId: domainRedirectTo ? domainRedirectTo.id : null, }; const { updateDomain } = await client.updateDomain(domain.id, updates); @@ -125,7 +137,7 @@ const EditDomainDialog = ({ reset({ name: domain.name, branch: domain.branch, - redirectedTo: getRedirectUrl(domain), + // redirectedTo: getRedirectUrl(domain), }); }, [domain]); @@ -137,7 +149,7 @@ const EditDomainDialog = ({ Domain name - Redirect to + {/* Redirect to - )} + )} */} Git branch {!isValid && ( diff --git a/packages/frontend/src/components/projects/project/settings/SetupDomain.tsx b/packages/frontend/src/components/projects/project/settings/SetupDomain.tsx index 03c9b331..ed3ab87d 100644 --- a/packages/frontend/src/components/projects/project/settings/SetupDomain.tsx +++ b/packages/frontend/src/components/projects/project/settings/SetupDomain.tsx @@ -1,12 +1,14 @@ -import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useForm } from 'react-hook-form'; import { Heading } from 'components/shared/Heading'; -import { InlineNotification } from 'components/shared/InlineNotification'; import { Input } from 'components/shared/Input'; import { Button } from 'components/shared/Button'; -import { Radio } from 'components/shared/Radio'; + +// NOTE: Commented code for redirect domain functionality +// import { useEffect, useState } from 'react'; +// import { InlineNotification } from 'components/shared/InlineNotification'; +// import { Radio } from 'components/shared/Radio'; interface SetupDomainFormValues { domainName: string; @@ -18,47 +20,45 @@ const SetupDomain = () => { register, handleSubmit, formState: { isValid }, - watch, - setValue, + // watch, + // setValue, } = useForm({ defaultValues: { domainName: '', - isWWW: 'false', + // isWWW: 'false', }, mode: 'onChange', }); - const [domainStr, setDomainStr] = useState(''); const navigate = useNavigate(); - const isWWWRadioOptions = [ - { label: domainStr, value: 'false' }, - { label: `www.${domainStr}`, value: 'true' }, - ]; + // const [domainStr, setDomainStr] = useState(''); + // const isWWWRadioOptions = [ + // { label: domainStr, value: 'false' }, + // { label: `www.${domainStr}`, value: 'true' }, + // ]; - useEffect(() => { - const subscription = watch((value, { name }) => { - if (name === 'domainName' && value.domainName) { - const domainArr = value.domainName.split('www.'); - const cleanedDomain = - domainArr.length > 1 ? domainArr[1] : domainArr[0]; - setDomainStr(cleanedDomain); + // useEffect(() => { + // const subscription = watch((value, { name }) => { + // if (name === 'domainName' && value.domainName) { + // const domainArr = value.domainName.split('www.'); + // const cleanedDomain = + // domainArr.length > 1 ? domainArr[1] : domainArr[0]; + // setDomainStr(cleanedDomain); - setValue( - 'isWWW', - value.domainName.startsWith('www.') ? 'true' : 'false', - ); - } - }); + // setValue( + // 'isWWW', + // value.domainName.startsWith('www.') ? 'true' : 'false', + // ); + // } + // }); - return () => subscription.unsubscribe(); - }, [watch, setValue]); + // return () => subscription.unsubscribe(); + // }, [watch, setValue]); return (
{ - watch('isWWW') === 'true' - ? navigate(`config?name=www.${domainStr}`) - : navigate(`config?name=${domainStr}`); + onSubmit={handleSubmit((e) => { + navigate(`config?name=${e.domainName}`) })} className="flex flex-col gap-6 w-full" > @@ -67,7 +67,7 @@ const SetupDomain = () => { Setup domain name

- Add your domain and setup redirects + Add your domain {/* and setup redirects */}

@@ -80,7 +80,7 @@ const SetupDomain = () => { label="Domain name" /> - {isValid && ( + {/* {isValid && (
Primary domain @@ -99,7 +99,7 @@ const SetupDomain = () => { }. Redirect preferences can be changed later`} />
- )} + )} */}
diff --git a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx index 7732a6e6..52618aa8 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx @@ -1,39 +1,54 @@ import { useEffect, useState } from 'react'; -import { Link, useNavigate, useOutletContext } from 'react-router-dom'; +import { + Link, + useOutletContext, +} from 'react-router-dom'; import { RequestError } from 'octokit'; -import { useOctokit } from 'context/OctokitContext'; -import { GitCommitWithBranch, OutletContextType } from '../../../../types'; -import { useGQLClient } from 'context/GQLClientContext'; -import { Button, Heading, Avatar, Tag } from 'components/shared'; -import { getInitials } from 'utils/geInitials'; +import { + Heading, + Avatar, +} from 'components/shared'; import { BranchStrokeIcon, - CheckRoundFilledIcon, - ClockIcon, CursorBoxIcon, GithubStrokeIcon, - GlobeIcon, LinkIcon, CalendarDaysIcon, } from 'components/shared/CustomIcon'; +import { useOctokit } from 'context/OctokitContext'; +import { GitCommitWithBranch, OutletContextType } from '../../../../types'; +import { getInitials } from 'utils/geInitials'; import { Activity } from 'components/projects/project/overview/Activity'; import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo'; import { relativeTimeMs } from 'utils/time'; -import { Domain, DomainStatus } from 'gql-client'; import { AuctionCard } from 'components/projects/project/overview/Activity/AuctionCard'; +// NOTE: Commented code for verify domain functionality +// import { useGQLClient } from 'context/GQLClientContext'; +// import { Domain, DomainStatus } from 'gql-client'; +// import { +// CheckRoundFilledIcon, +// ClockIcon, +// GlobeIcon, +// } from 'components/shared/CustomIcon'; +// import { +// Button, +// Tag, +// } from 'components/shared'; +// import { useNavigate } from 'react-router-dom'; + const COMMITS_PER_PAGE = 4; const PROJECT_UPDATE_WAIT_MS = 5000; const OverviewTabPanel = () => { const { octokit } = useOctokit(); - const navigate = useNavigate(); + // const navigate = useNavigate(); const [activities, setActivities] = useState([]); const [fetchingActivities, setFetchingActivities] = useState(true); - const [liveDomain, setLiveDomain] = useState(); + // const [liveDomain, setLiveDomain] = useState(); - const client = useGQLClient(); + // const client = useGQLClient(); const { project, onUpdate } = useOutletContext(); useEffect(() => { @@ -107,22 +122,22 @@ const OverviewTabPanel = () => { return () => clearInterval(timerId); }, [onUpdate]); - useEffect(() => { - const fetchLiveProdDomain = async () => { - const { domains } = await client.getDomains(project.id, { - branch: project.prodBranch, - status: DomainStatus.Live, - }); + // useEffect(() => { + // const fetchLiveProdDomain = async () => { + // const { domains } = await client.getDomains(project.id, { + // branch: project.prodBranch, + // status: DomainStatus.Live, + // }); - if (domains.length === 0) { - return; - } + // if (domains.length === 0) { + // return; + // } - setLiveDomain(domains[0]); - }; + // setLiveDomain(domains[0]); + // }; - fetchLiveProdDomain(); - }, [project]); + // fetchLiveProdDomain(); + // }, [project]); return (
@@ -141,19 +156,16 @@ const OverviewTabPanel = () => { {project.deployments && project.deployments.length > 0 && project.deployments.map((deployment, index) => ( -

- - {deployment.deployer.baseDomain} - +

+ {deployment.deployer.baseDomain}

))}
- }> + {/* }> {liveDomain ? ( }> Connected @@ -174,7 +186,7 @@ const OverviewTabPanel = () => { )} - + */} {project.deployments.length !== 0 ? ( <> {/* SOURCE */} @@ -193,11 +205,9 @@ const OverviewTabPanel = () => { project.deployments.length > 0 && project.deployments.map((deployment) => (
- + - {`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`} + {deployment.applicationDeploymentRecordData.url} diff --git a/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx b/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx index 0de97ac0..01e98c6c 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx @@ -60,31 +60,39 @@ const Domains = () => { return ( } - size="md" - > - Add domain - - } - > - {domains.map((domain) => { - return ( - - ); + {...(!project.auctionId && { + button: ( + + ), })} + > + {project.auctionId ? ( +

+ Custom domains not supported for auction driven deployments. +

+ ) : ( + domains.map((domain) => { + return ( + + ); + }) + )}
); }; diff --git a/packages/frontend/src/pages/org-slug/projects/id/settings/domains/add/Config.tsx b/packages/frontend/src/pages/org-slug/projects/id/settings/domains/add/Config.tsx index d3419c06..b56fae5b 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/settings/domains/add/Config.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/settings/domains/add/Config.tsx @@ -7,6 +7,8 @@ import { InlineNotification } from 'components/shared/InlineNotification'; import { ArrowRightCircleIcon } from 'components/shared/CustomIcon'; import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer'; import { useToast } from 'components/shared/Toast'; +import { useEffect, useState } from 'react'; +import { DNSRecordAttributes } from 'gql-client'; const Config = () => { const { id, orgSlug } = useParams(); @@ -16,6 +18,8 @@ const Config = () => { const primaryDomainName = searchParams.get('name'); const { toast, dismiss } = useToast(); + const [dnsRecord, setDnsRecord] = useState(null); + const handleSubmitDomain = async () => { if (primaryDomainName === null) { toast({ @@ -59,51 +63,80 @@ const Config = () => { } }; + useEffect(() => { + const fetchDNSData = async () => { + if (id === undefined) { + toast({ + id: 'domain_cannot_find_project', + title: 'Cannot find project', + variant: 'error', + onDismiss: dismiss, + }); + return; + } + + const dnsRecordResponse = await client.getLatestDNSRecordByProjectId(id); + + setDnsRecord(dnsRecordResponse.latestDNSRecord); + }; + + fetchDNSData(); + }, [id, client]); + // TODO: Figure out DNS Provider if possible and update appropriatly return ( -

- Add the following records to your domain. -

+ {dnsRecord ? ( + <> +

+ Add the following records to your domain. +

- - - - Type - Host - Value - - +
+ + + Type + Host + Value + + - - - A - @ - IP.OF.THE.SP - + + + + {dnsRecord.resourceType} + + @ + +

+ {dnsRecord.value ?? 'Not available'} +

+
+
+
+
- - CNAME - subdomain - domain.of.the.sp - - - - - - + {dnsRecord?.value && ( + + )} + + + ) : ( +

DNS record data not available

+ )}
); }; diff --git a/packages/frontend/src/stories/MockStoriesData.ts b/packages/frontend/src/stories/MockStoriesData.ts index 50f11469..97f63a14 100644 --- a/packages/frontend/src/stories/MockStoriesData.ts +++ b/packages/frontend/src/stories/MockStoriesData.ts @@ -12,6 +12,7 @@ import { Domain, Environment, Permission, + AppDeploymentRecordAttributes, } from 'gql-client'; export const user: User = { @@ -99,7 +100,6 @@ export const deployment0: Deployment = { environment: Environment.Development, isCurrent: true, commitHash: 'Commit Hash', - domain: domain0, commitMessage: 'Commit Message', createdBy: user, deployer: { @@ -111,6 +111,7 @@ export const deployment0: Deployment = { }, applicationDeploymentRequestId: 'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize', + applicationDeploymentRecordData: {} as AppDeploymentRecordAttributes, }; export const project: Project = { diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index c915dde9..49ddbb60 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -453,4 +453,15 @@ export class GQLClient { return data.verifyTx; } + + async getLatestDNSRecordByProjectId(projectId: string): Promise { + const { data } = await this.client.query({ + query: queries.getLatestDNSRecordByProjectId, + variables: { + projectId, + }, + }); + + return data; + } } diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index 48760dbd..2af5a1e3 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -57,17 +57,12 @@ query ($projectId: String!) { commitHash createdAt environment + applicationDeploymentRecordData { + url + } deployer { baseDomain } - domain { - status - branch - createdAt - updatedAt - id - name - } createdBy { id name @@ -112,13 +107,8 @@ query ($organizationSlug: String!) { commitMessage createdAt environment - domain { - status - branch - createdAt - updatedAt - id - name + applicationDeploymentRecordData { + url } } } @@ -141,14 +131,6 @@ export const getDeployments = gql` query ($projectId: String!) { deployments(projectId: $projectId) { id - domain{ - branch - createdAt - id - name - status - updatedAt - } branch commitHash commitMessage @@ -343,3 +325,15 @@ query ($txHash: String!, $amount: String!, $senderAddress: String!) { verifyTx(txHash: $txHash, amount: $amount, senderAddress: $senderAddress) } `; + +export const getLatestDNSRecordByProjectId = gql` +query($projectId: String!) { + latestDNSRecord(projectId: $projectId) { + name + value + request + resourceType + version + } +} +`; diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 3c690fc6..2370b414 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -99,7 +99,6 @@ export type User = { export type Deployment = { id: string; - domain: Domain; branch: string; commitHash: string; commitMessage: string; @@ -108,6 +107,7 @@ export type Deployment = { environment: Environment; isCurrent: boolean; baseDomain?: string; + applicationDeploymentRecordData: AppDeploymentRecordAttributes; status: DeploymentStatus; createdBy: User; createdAt: string; @@ -376,3 +376,28 @@ export type AuctionParams = { maxPrice: string; numProviders: number; }; + +export type DNSRecordAttributes = { + name: string; + value: string; + request: string; + resourceType: string; + version: string; +} + +export type GetLatestDNSDataResponse = { + latestDNSRecord: DNSRecordAttributes | null +} + +export interface AppDeploymentRecordAttributes { + application: string; + auction: string; + deployer: string; + dns: string; + meta: string; + name: string; + request: string; + type: string; + url: string; + version: string; +}