Display deployment build logs (#8)
All checks were successful
Lint / lint (20.x) (push) Successful in 4m53s

Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Co-authored-by: Neeraj <neeraj.rtly@gmail.com>
Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: #8
This commit is contained in:
nabarun 2024-10-22 09:12:59 +00:00
parent ef26f9b39e
commit 5152952a45
14 changed files with 316 additions and 115 deletions

View File

@ -23,6 +23,7 @@ import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { getEntities, loadAndSaveData } from './utils'; import { getEntities, loadAndSaveData } from './utils';
import { UserOrganization } from './entity/UserOrganization'; import { UserOrganization } from './entity/UserOrganization';
import { Deployer } from './entity/Deployer';
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'; const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
@ -215,7 +216,8 @@ export class Database {
relations: { relations: {
project: true, project: true,
domain: true, domain: true,
createdBy: true createdBy: true,
deployer: true,
}, },
where: { where: {
project: { project: {
@ -488,6 +490,12 @@ export class Database {
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
async saveProject (project: Project): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project);
return projectRepository.save(project);
}
async updateProjectById ( async updateProjectById (
projectId: string, projectId: string,
data: DeepPartial<Project> data: DeepPartial<Project>
@ -573,4 +581,19 @@ export class Database {
return domains; return domains;
} }
async addDeployer (data: DeepPartial<Deployer>): Promise<Deployer> {
const deployerRepository = this.dataSource.getRepository(Deployer);
const newDomain = await deployerRepository.save(data);
return newDomain;
}
async getDeployerById (deployerId: string): Promise<Deployer | null> {
const deployerRepository = this.dataSource.getRepository(Deployer);
const deployer = await deployerRepository.findOne({ where: { deployerId } });
return deployer;
}
} }

View File

@ -0,0 +1,20 @@
import { Entity, PrimaryColumn, Column, ManyToMany } from 'typeorm';
import { Project } from './Project';
@Entity()
export class Deployer {
@PrimaryColumn('varchar')
deployerId!: string;
@Column('varchar')
deployerLrn!: string;
@Column('varchar')
deployerApiUrl!: string;
@Column('varchar')
baseDomain!: string;
@ManyToMany(() => Project, (project) => project.deployers)
projects!: Project[];
}

View File

@ -13,6 +13,7 @@ import {
import { Project } from './Project'; import { Project } from './Project';
import { Domain } from './Domain'; import { Domain } from './Domain';
import { User } from './User'; import { User } from './User';
import { Deployer } from './Deployer';
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types'; import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
export enum Environment { export enum Environment {
@ -127,8 +128,9 @@ export class Deployment {
@Column('simple-json', { nullable: true }) @Column('simple-json', { nullable: true })
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null; applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
@Column('varchar') @ManyToOne(() => Deployer)
deployerLrn!: string; @JoinColumn({ name: 'deployerId' })
deployer!: Deployer;
@Column({ @Column({
enum: Environment enum: Environment
@ -138,9 +140,6 @@ export class Deployment {
@Column('boolean', { default: false }) @Column('boolean', { default: false })
isCurrent!: boolean; isCurrent!: boolean;
@Column('varchar', { nullable: true })
baseDomain!: string | null;
@Column({ @Column({
enum: DeploymentStatus enum: DeploymentStatus
}) })

View File

@ -7,13 +7,16 @@ import {
ManyToOne, ManyToOne,
JoinColumn, JoinColumn,
OneToMany, OneToMany,
DeleteDateColumn DeleteDateColumn,
JoinTable,
ManyToMany
} from 'typeorm'; } from 'typeorm';
import { User } from './User'; import { User } from './User';
import { Organization } from './Organization'; import { Organization } from './Organization';
import { ProjectMember } from './ProjectMember'; import { ProjectMember } from './ProjectMember';
import { Deployment } from './Deployment'; import { Deployment } from './Deployment';
import { Deployer } from './Deployer';
@Entity() @Entity()
export class Project { export class Project {
@ -49,8 +52,9 @@ export class Project {
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
auctionId!: string | null; auctionId!: string | null;
@Column({ type: 'simple-array', nullable: true }) @ManyToMany(() => Deployer, (deployer) => (deployer.projects))
deployerLrns!: string[] | null; @JoinTable()
deployers!: Deployer[]
@Column('boolean', { default: false, nullable: true }) @Column('boolean', { default: false, nullable: true })
fundsReleased!: boolean; fundsReleased!: boolean;
@ -70,9 +74,6 @@ export class Project {
@Column('varchar') @Column('varchar')
icon!: string; icon!: string;
@Column({ type: 'simple-array', nullable: true })
baseDomains!: string[] | null;
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;

View File

@ -14,7 +14,7 @@ import {
ApplicationDeploymentRequest, ApplicationDeploymentRequest,
ApplicationDeploymentRemovalRequest ApplicationDeploymentRemovalRequest
} from './entity/Deployment'; } from './entity/Deployment';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams } from './types'; import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
import { getConfig, getRepoDetails, sleep } from './utils'; import { getConfig, getRepoDetails, sleep } from './utils';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
@ -25,6 +25,7 @@ const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest'; const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'; const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
const SLEEP_DURATION = 1000; const SLEEP_DURATION = 1000;
// TODO: Move registry code to registry-sdk/watcher-ts // TODO: Move registry code to registry-sdk/watcher-ts
@ -291,32 +292,29 @@ export class Registry {
}; };
} }
async getAuctionWinningDeployers( async getAuctionWinningDeployerRecords(
auctionId: string auctionId: string
): Promise<string[]> { ): Promise<DeployerRecord[]> {
const records = await this.registry.getAuctionsByIds([auctionId]); const records = await this.registry.getAuctionsByIds([auctionId]);
const auctionResult = records[0]; const auctionResult = records[0];
let deployerLrns = []; let deployerRecords = [];
const { winnerAddresses } = auctionResult; const { winnerAddresses } = auctionResult;
for (const auctionWinner of winnerAddresses) { for (const auctionWinner of winnerAddresses) {
const deployerRecords = await this.registry.queryRecords( const records = await this.getDeployerRecordsByFilter({
{
paymentAddress: auctionWinner, paymentAddress: auctionWinner,
}, });
true
);
for (const record of deployerRecords) { for (const record of records) {
if (record.names && record.names.length > 0) { if (record.id) {
deployerLrns.push(record.names[0]); deployerRecords.push(record);
break; break;
} }
} }
} }
return deployerLrns; return deployerRecords;
} }
async releaseDeployerFunds( async releaseDeployerFunds(
@ -359,6 +357,19 @@ export class Registry {
); );
} }
/**
* Fetch WebappDeployer Records by filter
*/
async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise<DeployerRecord[]> {
return this.registry.queryRecords(
{
type: WEBAPP_DEPLOYER_RECORD_TYPE,
...filter
},
true
);
}
/** /**
* Fetch ApplicationDeploymentRecords by filter * Fetch ApplicationDeploymentRecords by filter
*/ */

View File

@ -104,7 +104,8 @@ type Deployment {
commitMessage: String! commitMessage: String!
url: String url: String
environment: Environment! environment: Environment!
deployerLrn: String deployer: Deployer
applicationDeploymentRequestId: String
isCurrent: Boolean! isCurrent: Boolean!
baseDomain: String baseDomain: String
status: DeploymentStatus! status: DeploymentStatus!
@ -132,6 +133,14 @@ type EnvironmentVariable {
updatedAt: String! updatedAt: String!
} }
type Deployer {
deployerId: String!
deployerLrn: String!
deployerApiUrl: String!
createdAt: String!
updatedAt: String!
}
type AuthResult { type AuthResult {
token: String! token: String!
} }

View File

@ -14,6 +14,7 @@ import { Project } from './entity/Project';
import { Permission, ProjectMember } from './entity/ProjectMember'; import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User'; import { User } from './entity/User';
import { Registry } from './registry'; import { Registry } from './registry';
import { Deployer } from './entity/Deployer';
import { GitHubConfig, RegistryConfig } from './config'; import { GitHubConfig, RegistryConfig } from './config';
import { import {
AddProjectFromTemplateInput, AddProjectFromTemplateInput,
@ -174,6 +175,7 @@ export class Service {
applicationDeploymentRequestId: record.attributes.request, applicationDeploymentRequestId: record.attributes.request,
})), })),
relations: { relations: {
deployer: true,
project: true, project: true,
}, },
order: { order: {
@ -193,30 +195,23 @@ export class Service {
const deploymentUpdatePromises = records.map(async (record) => { const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.request]; const deployment = recordToDeploymentsMap[record.attributes.request];
const parts = record.attributes.url.replace('https://', '').split('.'); if (!deployment.project) {
const baseDomain = parts.slice(1).join('.'); log(`Project ${deployment.projectId} not found`);
return;
} else {
deployment.applicationDeploymentRecordId = record.id; deployment.applicationDeploymentRecordId = record.id;
deployment.applicationDeploymentRecordData = record.attributes; deployment.applicationDeploymentRecordData = record.attributes;
deployment.url = record.attributes.url; deployment.url = record.attributes.url;
deployment.baseDomain = baseDomain;
deployment.status = DeploymentStatus.Ready; deployment.status = DeploymentStatus.Ready;
deployment.isCurrent = deployment.environment === Environment.Production; deployment.isCurrent = deployment.environment === Environment.Production;
await this.db.updateDeploymentById(deployment.id, deployment); await this.db.updateDeploymentById(deployment.id, deployment);
const baseDomains = deployment.project.baseDomains || [];
if (!baseDomains.includes(baseDomain)) {
baseDomains.push(baseDomain);
}
// Release deployer funds on successful deployment // Release deployer funds on successful deployment
if (!deployment.project.fundsReleased) { if (!deployment.project.fundsReleased) {
const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId); const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId);
await this.db.updateProjectById(deployment.projectId, { await this.db.updateProjectById(deployment.projectId, {
baseDomains,
fundsReleased, fundsReleased,
}); });
} }
@ -224,6 +219,7 @@ export class Service {
log( log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`, `Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
); );
}
}); });
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
@ -235,7 +231,7 @@ export class Service {
for (const deployment of prodDeployments) { for (const deployment of prodDeployments) {
const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId); const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId);
const oldDeployments = projectDeployments const oldDeployments = projectDeployments
.filter(projectDeployment => projectDeployment.deployerLrn === deployment.deployerLrn && projectDeployment.id !== deployment.id); .filter(projectDeployment => projectDeployment.deployer.deployerLrn === deployment.deployer.deployerLrn && projectDeployment.id !== deployment.id);
for (const oldDeployment of oldDeployments) { for (const oldDeployment of oldDeployments) {
await this.db.updateDeployment( await this.db.updateDeployment(
{ id: oldDeployment.id }, { id: oldDeployment.id },
@ -308,17 +304,37 @@ export class Service {
); );
for (const project of projectsToBedeployed) { for (const project of projectsToBedeployed) {
const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project.auctionId!); const deployerRecords = await this.laconicRegistry.getAuctionWinningDeployerRecords(project!.auctionId!);
if (!deployerLrns) { if (!deployerRecords) {
log(`No winning deployer for auction ${project!.auctionId}`); log(`No winning deployer for auction ${project!.auctionId}`);
} else { } else {
// Update project with deployer LRNs const deployerIds = [];
await this.db.updateProjectById(project.id!, {
deployerLrns
});
for (const deployer of deployerLrns) { for (const record of deployerRecords) {
const deployerId = record.id;
const deployerLrn = record.names[0];
deployerIds.push(deployerId);
const deployerApiUrl = record.attributes.apiUrl;
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
const deployerData = {
deployerId,
deployerLrn,
deployerApiUrl,
baseDomain
};
// Store the deployer in the DB
const deployer = await this.db.addDeployer(deployerData);
// Update project with deployer
await this.updateProjectWithDeployer(project.id, deployer);
}
for (const deployer of deployerIds) {
log(`Creating deployment for deployer LRN ${deployer}`); log(`Creating deployment for deployer LRN ${deployer}`);
await this.createDeploymentFromAuction(project, deployer); await this.createDeploymentFromAuction(project, deployer);
} }
@ -587,7 +603,7 @@ export class Service {
domain: prodBranchDomains[0], domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash, commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage, commitMessage: oldDeployment.commitMessage,
deployerLrn: oldDeployment.deployerLrn deployer: oldDeployment.deployer
}); });
return newDeployment; return newDeployment;
@ -596,7 +612,8 @@ export class Service {
async createDeployment( async createDeployment(
userId: string, userId: string,
octokit: Octokit, octokit: Octokit,
data: DeepPartial<Deployment> data: DeepPartial<Deployment>,
deployerLrn?: string
): Promise<Deployment> { ): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found'); assert(data.project?.repository, 'Project repository not found');
log( log(
@ -625,7 +642,14 @@ export class Service {
); );
} }
const newDeployment = await this.createDeploymentFromData(userId, data, data.deployerLrn!, applicationRecordId, applicationRecordData); let deployer;
if (deployerLrn) {
deployer = await this.createDeployerFromLRN(deployerLrn);
} else {
deployer = data.deployer;
}
const newDeployment = await this.createDeploymentFromData(userId, data, deployer!.deployerId!, applicationRecordId, applicationRecordData);
const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash); const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash);
const environmentVariablesObj = await this.getEnvVariables(data.project!.id!); const environmentVariablesObj = await this.getEnvVariables(data.project!.id!);
@ -639,7 +663,7 @@ export class Service {
repository: repoUrl, repository: repoUrl,
environmentVariables: environmentVariablesObj, environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`, dns: `${newDeployment.project.name}`,
lrn: data.deployerLrn! lrn: deployer!.deployerLrn!
}); });
} }
@ -648,7 +672,7 @@ export class Service {
deployment: newDeployment, deployment: newDeployment,
appName: repo, appName: repo,
repository: repoUrl, repository: repoUrl,
lrn: data.deployerLrn!, lrn: deployer!.deployerLrn!,
environmentVariables: environmentVariablesObj, environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`, dns: `${newDeployment.project.name}-${newDeployment.id}`,
}); });
@ -663,7 +687,7 @@ export class Service {
async createDeploymentFromAuction( async createDeploymentFromAuction(
project: DeepPartial<Project>, project: DeepPartial<Project>,
deployerLrn: string deployerId: string
): Promise<Deployment> { ): Promise<Deployment> {
const octokit = await this.getOctokit(project.ownerId!); const octokit = await this.getOctokit(project.ownerId!);
const [owner, repo] = project.repository!.split('/'); const [owner, repo] = project.repository!.split('/');
@ -689,6 +713,9 @@ export class Service {
const applicationRecordId = record.id; const applicationRecordId = record.id;
const applicationRecordData = record.attributes; const applicationRecordData = record.attributes;
const deployer = await this.db.getDeployerById(deployerId);
const deployerLrn = deployer!.deployerLrn
// Create deployment with prod branch and latest commit // Create deployment with prod branch and latest commit
const deploymentData = { const deploymentData = {
project, project,
@ -699,7 +726,7 @@ export class Service {
commitMessage: latestCommit.commit.message, commitMessage: latestCommit.commit.message,
}; };
const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData); const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerId, applicationRecordId, applicationRecordData);
const environmentVariablesObj = await this.getEnvVariables(project!.id!); const environmentVariablesObj = await this.getEnvVariables(project!.id!);
// To set project DNS // To set project DNS
@ -740,7 +767,7 @@ export class Service {
async createDeploymentFromData( async createDeploymentFromData(
userId: string, userId: string,
data: DeepPartial<Deployment>, data: DeepPartial<Deployment>,
deployerLrn: string, deployerId: string,
applicationRecordId: string, applicationRecordId: string,
applicationRecordData: ApplicationRecord, applicationRecordData: ApplicationRecord,
): Promise<Deployment> { ): Promise<Deployment> {
@ -757,7 +784,9 @@ export class Service {
createdBy: Object.assign(new User(), { createdBy: Object.assign(new User(), {
id: userId, id: userId,
}), }),
deployerLrn, deployer: Object.assign(new Deployer(), {
deployerId,
}),
}); });
log(`Created deployment ${newDeployment.id}`); log(`Created deployment ${newDeployment.id}`);
@ -765,6 +794,50 @@ export class Service {
return newDeployment; return newDeployment;
} }
async createDeployerFromLRN(deployerLrn: string): Promise<Deployer | null> {
const records = await this.laconicRegistry.getRecordsByName(deployerLrn);
if (records.length === 0) {
log('No records found for deployer LRN:', deployerLrn);
return null;
}
const deployerId = records[0].id;
const deployerApiUrl = records[0].attributes.apiUrl;
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
const deployerData = {
deployerId,
deployerLrn,
deployerApiUrl,
baseDomain
};
const deployer = await this.db.addDeployer(deployerData);
return deployer;
}
async updateProjectWithDeployer(
projectId: string,
deployer: Deployer
): Promise<Deployer> {
const deploymentProject = await this.db.getProjects({
where: { id: projectId },
relations: ['deployers']
});
if (!deploymentProject[0].deployers) {
deploymentProject[0].deployers = [];
}
deploymentProject[0].deployers.push(deployer);
await this.db.saveProject(deploymentProject[0]);
return deployer;
}
async addProjectFromTemplate( async addProjectFromTemplate(
user: User, user: User,
organizationSlug: string, organizationSlug: string,
@ -828,6 +901,7 @@ export class Service {
slug: organizationSlug, slug: organizationSlug,
}, },
}); });
if (!organization) { if (!organization) {
throw new Error('Organization does not exist'); throw new Error('Organization does not exist');
} }
@ -858,15 +932,15 @@ export class Service {
domain: null, domain: null,
commitHash: latestCommit.sha, commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message, commitMessage: latestCommit.commit.message,
deployerLrn: lrn
}; };
if (auctionParams) { if (auctionParams) {
const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData); const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData);
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId }) await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId });
} else { } else {
await this.createDeployment(user.id, octokit, deploymentData); const newDeployment = await this.createDeployment(user.id, octokit, deploymentData, lrn);
await this.updateProject(project.id, { deployerLrns: [lrn!] }) // Update project with deployer
await this.updateProjectWithDeployer(newDeployment.projectId, newDeployment.deployer);
} }
await this.createRepoHook(octokit, project); await this.createRepoHook(octokit, project);
@ -920,6 +994,9 @@ export class Service {
); );
const projects = await this.db.getProjects({ const projects = await this.db.getProjects({
where: { repository: repository.full_name }, where: { repository: repository.full_name },
relations: {
deployers: true,
}
}); });
if (!projects.length) { if (!projects.length) {
@ -936,7 +1013,7 @@ export class Service {
branch, branch,
}); });
const deployers = project.deployerLrns; const deployers = project.deployers;
if (!deployers) { if (!deployers) {
log(`No deployer present for project ${project.id}`) log(`No deployer present for project ${project.id}`)
return; return;
@ -955,7 +1032,7 @@ export class Service {
domain, domain,
commitHash: headCommit.id, commitHash: headCommit.id,
commitMessage: headCommit.message, commitMessage: headCommit.message,
deployerLrn: deployer deployer: deployer
}, },
); );
} }
@ -995,6 +1072,7 @@ export class Service {
relations: { relations: {
project: true, project: true,
domain: true, domain: true,
deployer: true,
createdBy: true, createdBy: true,
}, },
where: { where: {
@ -1011,8 +1089,7 @@ export class Service {
let newDeployment: Deployment; let newDeployment: Deployment;
if (oldDeployment.project.auctionId) { if (oldDeployment.project.auctionId) {
// TODO: Discuss creating applicationRecord for redeployments newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployer.deployerId);
newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployerLrn);
} else { } else {
newDeployment = await this.createDeployment(user.id, octokit, newDeployment = await this.createDeployment(user.id, octokit,
{ {
@ -1023,7 +1100,7 @@ export class Service {
domain: oldDeployment.domain, domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash, commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage, commitMessage: oldDeployment.commitMessage,
deployerLrn: oldDeployment.deployerLrn deployer: oldDeployment.deployer
} }
); );
} }
@ -1072,13 +1149,14 @@ export class Service {
}, },
relations: { relations: {
project: true, project: true,
deployer: true,
}, },
}); });
if (deployment && deployment.applicationDeploymentRecordId) { if (deployment && deployment.applicationDeploymentRecordId) {
// If deployment is current, remove deployment for project subdomain as well // If deployment is current, remove deployment for project subdomain as well
if (deployment.isCurrent) { if (deployment.isCurrent) {
const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.baseDomain}`; const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.deployer.baseDomain}`;
// TODO: Store the latest DNS deployment record // TODO: Store the latest DNS deployment record
const deploymentRecords = const deploymentRecords =
@ -1101,14 +1179,14 @@ export class Service {
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
deploymentId: latestRecord.id, deploymentId: latestRecord.id,
deployerLrn: deployment.deployerLrn deployerLrn: deployment.deployer.deployerLrn
}); });
} }
const result = const result =
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
deploymentId: deployment.applicationDeploymentRecordId, deploymentId: deployment.applicationDeploymentRecordId,
deployerLrn: deployment.deployerLrn deployerLrn: deployment.deployer.deployerLrn
}); });
await this.db.updateDeploymentById(deployment.id, { await this.db.updateDeploymentById(deployment.id, {

View File

@ -82,3 +82,20 @@ export interface EnvironmentVariables {
key: string, key: string,
value: string, value: string,
} }
export interface DeployerRecord {
id: string;
names: string[];
owners: string[];
bondId: string;
createTime: string;
expiryTime: string;
attributes: {
apiUrl: string;
name: string;
paymentAddress: string;
publicKey: string;
type: string;
version: string;
};
}

View File

@ -139,30 +139,8 @@ const Configure = () => {
const projectId = await createProject(createFormData, environmentVariables); const projectId = await createProject(createFormData, environmentVariables);
const { environmentVariables: isEnvironmentVariablesAdded } =
await client.getEnvironmentVariables(projectId); await client.getEnvironmentVariables(projectId);
if (isEnvironmentVariablesAdded.length > 0) {
toast({
id:
createFormData.variables.length > 1
? 'env_variable_added'
: 'env_variables_added',
title:
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
variant: 'success',
onDismiss: dismiss,
});
} else {
toast({
id: 'env_variables_not_added',
title: 'Environment variables not added',
variant: 'error',
onDismiss: dismiss,
});
}
if (templateId) { if (templateId) {
createFormData.option === 'Auction' createFormData.option === 'Auction'
? navigate( ? navigate(

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { import {
Deployment, Deployment,
DeploymentStatus, DeploymentStatus,
@ -6,6 +6,14 @@ import {
Environment, Environment,
Project, Project,
} from 'gql-client'; } from 'gql-client';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import { Avatar } from 'components/shared/Avatar'; import { Avatar } from 'components/shared/Avatar';
import { import {
BranchStrokeIcon, BranchStrokeIcon,
@ -18,12 +26,23 @@ import {
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
import { OverflownText } from 'components/shared/OverflownText'; import { OverflownText } from 'components/shared/OverflownText';
import { Tag, TagTheme } from 'components/shared/Tag'; import { Tag, TagTheme } from 'components/shared/Tag';
import { Button } from 'components/shared/Button';
import { getInitials } from 'utils/geInitials'; import { getInitials } from 'utils/geInitials';
import { relativeTimeMs } from 'utils/time'; import { relativeTimeMs } from 'utils/time';
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants'; import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
import { formatAddress } from '../../../../utils/format'; import { formatAddress } from '../../../../utils/format';
import { DeploymentMenu } from './DeploymentMenu'; import { DeploymentMenu } from './DeploymentMenu';
const DEPLOYMENT_LOGS_STYLE = {
backgroundColor: "rgba(0,0,0, .9)",
padding: "2em",
borderRadius: "0.5em",
marginLeft: "0.5em",
marginRight: "0.5em",
color: "gray",
fontSize: "small",
};
interface DeployDetailsCardProps { interface DeployDetailsCardProps {
deployment: Deployment; deployment: Deployment;
currentDeployment: Deployment; currentDeployment: Deployment;
@ -48,6 +67,12 @@ const DeploymentDetailsCard = ({
project, project,
prodBranchDomains, prodBranchDomains,
}: DeployDetailsCardProps) => { }: DeployDetailsCardProps) => {
const [openDialog, setOpenDialog] = useState<boolean>(false);
const [deploymentLogs, setDeploymentLogs] = useState<string>();
const handleOpenDialog = () => setOpenDialog(true);
const handleCloseDialog = () => setOpenDialog(false);
const getIconByDeploymentStatus = (status: DeploymentStatus) => { const getIconByDeploymentStatus = (status: DeploymentStatus) => {
if ( if (
status === DeploymentStatus.Building || status === DeploymentStatus.Building ||
@ -64,6 +89,14 @@ const DeploymentDetailsCard = ({
} }
}; };
const fetchDeploymentLogs = useCallback(async () => {
let url = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
const res = await fetch(url, { cache: 'no-store' });
const logs = await res.text();
setDeploymentLogs(logs);
handleOpenDialog();
}, [deployment.deployer.deployerApiUrl, deployment.applicationDeploymentRequestId, handleOpenDialog]);
const renderDeploymentStatus = useCallback( const renderDeploymentStatus = useCallback(
(className?: string) => { (className?: string) => {
return ( return (
@ -72,6 +105,7 @@ const DeploymentDetailsCard = ({
leftIcon={getIconByDeploymentStatus(deployment.status)} leftIcon={getIconByDeploymentStatus(deployment.status)}
size="xs" size="xs"
type={STATUS_COLORS[deployment.status] ?? 'neutral'} type={STATUS_COLORS[deployment.status] ?? 'neutral'}
onClick={fetchDeploymentLogs}
> >
{deployment.status} {deployment.status}
</Tag> </Tag>
@ -96,9 +130,9 @@ const DeploymentDetailsCard = ({
</OverflownText> </OverflownText>
</Heading> </Heading>
)} )}
{deployment.deployerLrn && ( {deployment.deployer.deployerLrn && (
<span className="text-sm text-elements-low-em tracking-tight block mt-2"> <span className="text-sm text-elements-low-em tracking-tight block mt-2">
Deployer LRN: {deployment.deployerLrn} Deployer LRN: {deployment.deployer.deployerLrn}
</span> </span>
)} )}
<span className="text-sm text-elements-low-em tracking-tight block"> <span className="text-sm text-elements-low-em tracking-tight block">
@ -167,6 +201,15 @@ const DeploymentDetailsCard = ({
prodBranchDomains={prodBranchDomains} prodBranchDomains={prodBranchDomains}
/> />
</div> </div>
<Dialog open={openDialog} onClose={handleCloseDialog} fullWidth maxWidth="md">
<DialogTitle>Deployment logs</DialogTitle>
<DialogContent style={DEPLOYMENT_LOGS_STYLE} >
{deploymentLogs && <pre >{deploymentLogs}</pre>}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Close</Button>
</DialogActions>
</Dialog>
</div> </div>
); );
}; };

View File

@ -36,7 +36,11 @@ const deployment: Deployment = {
url: 'https://deploy1.example.com', url: 'https://deploy1.example.com',
environment: Environment.Production, environment: Environment.Production,
isCurrent: true, isCurrent: true,
deployerLrn: 'lrn://example/deployers/webapp-deployer-api.test.com', deployer: {
deployerApiUrl: 'https://webapp-deployer-api.example.com',
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
deployerLrn:'lrn://example/deployers/webapp-deployer-api.example.com'
},
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
createdBy: { createdBy: {
id: 'user1', id: 'user1',
@ -49,6 +53,7 @@ const deployment: Deployment = {
}, },
createdAt: '1677676800', // 2023-03-01T12:00:00Z createdAt: '1677676800', // 2023-03-01T12:00:00Z
updatedAt: '1677680400', // 2023-03-01T13:00:00Z updatedAt: '1677680400', // 2023-03-01T13:00:00Z
applicationDeploymentRequestId: 'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
}; };
const domains: Domain[] = [ const domains: Domain[] = [

View File

@ -102,7 +102,12 @@ export const deployment0: Deployment = {
domain: domain0, domain: domain0,
commitMessage: 'Commit Message', commitMessage: 'Commit Message',
createdBy: user, createdBy: user,
deployerLrn: 'lrn://deployer.apps.snowballtools.com ', deployer: {
deployerApiUrl: 'https://webapp-deployer-api.example.com',
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
deployerLrn:'lrn://deployer.apps.snowballtools.com '
},
applicationDeploymentRequestId: 'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
}; };
export const project: Project = { export const project: Project = {

View File

@ -136,7 +136,11 @@ query ($projectId: String!) {
commitHash commitHash
commitMessage commitMessage
url url
deployerLrn deployer {
deployerId
deployerLrn,
deployerApiUrl,
}
environment environment
isCurrent isCurrent
baseDomain baseDomain
@ -148,6 +152,7 @@ query ($projectId: String!) {
name name
email email
} }
applicationDeploymentRequestId
} }
} }
`; `;

View File

@ -105,7 +105,7 @@ export type Deployment = {
commitHash: string; commitHash: string;
commitMessage: string; commitMessage: string;
url?: string; url?: string;
deployerLrn: string; deployer: Deployer;
environment: Environment; environment: Environment;
isCurrent: boolean; isCurrent: boolean;
baseDomain?: string; baseDomain?: string;
@ -113,8 +113,15 @@ export type Deployment = {
createdBy: User; createdBy: User;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
applicationDeploymentRequestId: string;
}; };
export type Deployer = {
deployerApiUrl: string;
deployerId: string;
deployerLrn: string;
}
export type OrganizationMember = { export type OrganizationMember = {
id: string; id: string;
member: User; member: User;