forked from cerc-io/snowballtools-base
Integrate SP auctions for app deployment (#2)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75) - Add support for configuring web-app deployers by - - Configuring deployer LRN (for targeted deployments) - Configuring SP auction params for deployment auction (max price and number of providers) Co-authored-by: IshaVenikar <ishavenikar7@gmail.com> Reviewed-on: cerc-io/snowballtools-base#2
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
gqlEndpoint = "http://localhost:9473/api"
|
||||
chainId = "laconic_9000-1"
|
||||
@@ -36,9 +37,17 @@
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
gas = "200000"
|
||||
fees = "200000alnt"
|
||||
gasPrice = ""
|
||||
gas = ""
|
||||
fees = ""
|
||||
gasPrice = "1alnt"
|
||||
|
||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||
[auction]
|
||||
commitFee = "100000"
|
||||
commitsDuration = "120s"
|
||||
revealFee = "100000"
|
||||
revealsDuration = "120s"
|
||||
denom = "alnt"
|
||||
|
||||
[misc]
|
||||
projectDomain = "apps.snowballtools.com"
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface RegistryConfig {
|
||||
privateKey: string;
|
||||
bondId: string;
|
||||
fetchDeploymentRecordDelay: number;
|
||||
checkAuctionStatusDelay: number;
|
||||
authority: string;
|
||||
fee: {
|
||||
gas: string;
|
||||
@@ -42,6 +43,14 @@ export interface RegistryConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuctionConfig {
|
||||
commitFee: string;
|
||||
commitsDuration: string;
|
||||
revealFee: string;
|
||||
revealsDuration: string;
|
||||
denom: string;
|
||||
}
|
||||
|
||||
export interface MiscConfig {
|
||||
projectDomain: string;
|
||||
}
|
||||
@@ -51,6 +60,7 @@ export interface Config {
|
||||
database: DatabaseConfig;
|
||||
gitHub: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
auction: AuctionConfig;
|
||||
misc: MiscConfig;
|
||||
turnkey: {
|
||||
apiBaseUrl: string;
|
||||
|
||||
@@ -465,8 +465,6 @@ export class Database {
|
||||
id: organizationId
|
||||
});
|
||||
|
||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
||||
|
||||
return projectRepository.save(newProject);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface ApplicationDeploymentRequest {
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
lrn?: string;
|
||||
auction?: string;
|
||||
config: string;
|
||||
meta: string;
|
||||
}
|
||||
@@ -112,19 +114,22 @@ export class Deployment {
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
||||
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRequestId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
|
||||
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRecordId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||
|
||||
@Column('varchar')
|
||||
deployerLrn!: string;
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
@@ -133,6 +138,9 @@ export class Deployment {
|
||||
@Column('boolean', { default: false })
|
||||
isCurrent!: boolean;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
baseDomain!: string | null;
|
||||
|
||||
@Column({
|
||||
enum: DeploymentStatus
|
||||
})
|
||||
@@ -147,7 +155,7 @@ export class Deployment {
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@ export class Project {
|
||||
@Column('text', { default: '' })
|
||||
description!: string;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
auctionId!: string | null;
|
||||
|
||||
@Column({ type: 'simple-array', nullable: true })
|
||||
deployerLrns!: string[] | null;
|
||||
|
||||
// TODO: Compute template & framework in import repository
|
||||
@Column('varchar', { nullable: true })
|
||||
template!: string | null;
|
||||
@@ -61,8 +67,8 @@ export class Project {
|
||||
@Column('varchar')
|
||||
icon!: string;
|
||||
|
||||
@Column('varchar')
|
||||
subDomain!: string;
|
||||
@Column({ type: 'simple-array', nullable: true })
|
||||
baseDomains!: string[] | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import debug from 'debug';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Octokit } from 'octokit';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
|
||||
import { Registry as LaconicRegistry, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||
import { Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import {
|
||||
@@ -12,12 +14,13 @@ import {
|
||||
ApplicationDeploymentRequest,
|
||||
ApplicationDeploymentRemovalRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
||||
import { sleep } from './utils';
|
||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams } from './types';
|
||||
import { getConfig, getRepoDetails, sleep } from './utils';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||
@@ -29,31 +32,33 @@ export class Registry {
|
||||
private registry: LaconicRegistry;
|
||||
private registryConfig: RegistryConfig;
|
||||
|
||||
constructor (registryConfig: RegistryConfig) {
|
||||
constructor(registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig;
|
||||
|
||||
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
|
||||
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
{ chainId: registryConfig.chainId, gasPrice }
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationRecord ({
|
||||
appName,
|
||||
packageJSON,
|
||||
async createApplicationRecord({
|
||||
octokit,
|
||||
repository,
|
||||
commitHash,
|
||||
appType,
|
||||
repoUrl
|
||||
}: {
|
||||
appName: string;
|
||||
packageJSON: PackageJSON;
|
||||
octokit: Octokit
|
||||
repository: string;
|
||||
commitHash: string;
|
||||
appType: string;
|
||||
repoUrl: string;
|
||||
}): Promise<{
|
||||
applicationRecordId: string;
|
||||
applicationRecordData: ApplicationRecord;
|
||||
}> {
|
||||
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
|
||||
// Use registry-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
@@ -87,7 +92,7 @@ export class Registry {
|
||||
repository_ref: commitHash,
|
||||
repository: [repoUrl],
|
||||
app_type: appType,
|
||||
name: appName,
|
||||
name: repo,
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
@@ -112,22 +117,29 @@ export class Registry {
|
||||
fee
|
||||
);
|
||||
|
||||
log(`Published application record ${result.id}`);
|
||||
log('Application record data:', applicationRecord);
|
||||
|
||||
// TODO: Discuss computation of LRN
|
||||
const lrn = this.getLrn(appName);
|
||||
const lrn = this.getLrn(repo);
|
||||
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.id, lrn },
|
||||
{
|
||||
cid: result.id,
|
||||
lrn
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.id, lrn: `${lrn}@${applicationRecord.app_version}` },
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
);
|
||||
@@ -148,10 +160,78 @@ export class Registry {
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
async createApplicationDeploymentAuction(
|
||||
appName: string,
|
||||
octokit: Octokit,
|
||||
auctionParams: AuctionParams,
|
||||
data: DeepPartial<Deployment>,
|
||||
): Promise<{
|
||||
applicationDeploymentAuctionId: string;
|
||||
}> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
|
||||
await this.createApplicationRecord({
|
||||
octokit,
|
||||
repository: data.project.repository,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
});
|
||||
|
||||
const lrn = this.getLrn(appName);
|
||||
const config = await getConfig();
|
||||
const auctionConfig = config.auction;
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const auctionResult = await this.registry.createProviderAuction(
|
||||
{
|
||||
commitFee: auctionConfig.commitFee,
|
||||
commitsDuration: auctionConfig.commitsDuration,
|
||||
revealFee: auctionConfig.revealFee,
|
||||
revealsDuration: auctionConfig.revealsDuration,
|
||||
denom: auctionConfig.denom,
|
||||
maxPrice: auctionParams.maxPrice,
|
||||
numProviders: auctionParams.numProviders,
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
|
||||
if (!auctionResult.auction) {
|
||||
throw new Error('Error creating auction');
|
||||
}
|
||||
|
||||
// Create record of type applicationDeploymentAuction and publish
|
||||
const applicationDeploymentAuction = {
|
||||
application: lrn,
|
||||
auction: auctionResult.auction.id,
|
||||
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
||||
};
|
||||
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentAuction,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
);
|
||||
|
||||
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
||||
log(`Application deployment auction record published: ${result.id}`);
|
||||
log('Application deployment auction data:', applicationDeploymentAuction);
|
||||
|
||||
return {
|
||||
applicationDeploymentAuctionId: auctionResult.auction.id,
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest(data: {
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
repository: string,
|
||||
auctionId?: string,
|
||||
lrn: string,
|
||||
environmentVariables: { [key: string]: string },
|
||||
dns: string,
|
||||
}): Promise<{
|
||||
@@ -174,9 +254,6 @@ export class Registry {
|
||||
application: `${lrn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: data.dns,
|
||||
|
||||
// TODO: Not set in test-progressive-web-app CI
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_LRN',
|
||||
|
||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||
config: JSON.stringify({
|
||||
env: data.environmentVariables
|
||||
@@ -187,7 +264,9 @@ export class Registry {
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.deployment.commitHash
|
||||
})
|
||||
}),
|
||||
deployer: data.lrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
@@ -212,10 +291,38 @@ export class Registry {
|
||||
};
|
||||
}
|
||||
|
||||
async getAuctionWinningDeployers(
|
||||
auctionId: string
|
||||
): Promise<string[]> {
|
||||
const records = await this.registry.getAuctionsByIds([auctionId]);
|
||||
const auctionResult = records[0];
|
||||
|
||||
let deployerLrns = [];
|
||||
const { winnerAddresses } = auctionResult;
|
||||
|
||||
for (const auctionWinner of winnerAddresses) {
|
||||
const deployerRecords = await this.registry.queryRecords(
|
||||
{
|
||||
paymentAddress: auctionWinner,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
for (const record of deployerRecords) {
|
||||
if (record.names && record.names.length > 0) {
|
||||
deployerLrns.push(record.names[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deployerLrns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords for deployments
|
||||
*/
|
||||
async getDeploymentRecords (
|
||||
async getDeploymentRecords(
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRecord[]> {
|
||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||
@@ -227,11 +334,11 @@ export class Registry {
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationRecordId === record.attributes.application &&
|
||||
deployment.applicationDeploymentRequestId === record.attributes.request &&
|
||||
record.attributes.url.includes(deployment.id)
|
||||
)
|
||||
);
|
||||
@@ -240,7 +347,7 @@ export class Registry {
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords by filter
|
||||
*/
|
||||
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||
@@ -253,7 +360,7 @@ export class Registry {
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||
*/
|
||||
async getDeploymentRemovalRecords (
|
||||
async getDeploymentRemovalRecords(
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRemovalRecord[]> {
|
||||
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||
@@ -274,8 +381,9 @@ export class Registry {
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRemovalRequest (data: {
|
||||
async createApplicationDeploymentRemovalRequest(data: {
|
||||
deploymentId: string;
|
||||
deployerLrn: string;
|
||||
}): Promise<{
|
||||
applicationDeploymentRemovalRequestId: string;
|
||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||
@@ -283,7 +391,8 @@ export class Registry {
|
||||
const applicationDeploymentRemovalRequest = {
|
||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
deployment: data.deploymentId
|
||||
deployment: data.deploymentId,
|
||||
deployer: data.deployerLrn
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
@@ -307,6 +416,30 @@ export class Registry {
|
||||
};
|
||||
}
|
||||
|
||||
async getCompletedAuctionIds(auctionIds: (string | null | undefined)[]): Promise<string[] | null> {
|
||||
const validAuctionIds = auctionIds.filter((id): id is string => id !== null && id !== undefined);
|
||||
|
||||
if (!validAuctionIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const auctions = await this.registry.getAuctionsByIds(validAuctionIds);
|
||||
|
||||
const completedAuctions = auctions
|
||||
.filter((auction: { id: string, status: string }) => auction.status === 'completed')
|
||||
.map((auction: { id: string, status: string }) => auction.id);
|
||||
|
||||
return completedAuctions;
|
||||
}
|
||||
|
||||
async getRecordsByName(name: string): Promise<any> {
|
||||
return this.registry.resolveNames([name]);
|
||||
}
|
||||
|
||||
async getAuctionData(auctionId: string): Promise<any> {
|
||||
return this.registry.getAuctionsByIds([auctionId]);
|
||||
}
|
||||
|
||||
getLrn(appName: string): string {
|
||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Permission } from './entity/ProjectMember';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { Project } from './entity/Project';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { AddProjectFromTemplateInput } from './types';
|
||||
import { AddProjectFromTemplateInput, AuctionParams } from './types';
|
||||
|
||||
const log = debug('snowball:resolver');
|
||||
|
||||
@@ -69,6 +69,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
) => {
|
||||
return service.getDomainsByProjectId(projectId, filter);
|
||||
},
|
||||
|
||||
getAuctionData: async (
|
||||
_: any,
|
||||
{ auctionId }: { auctionId: string },
|
||||
) => {
|
||||
return service.getAuctionData(auctionId);
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
@@ -203,7 +210,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
}: { organizationSlug: string; data: AddProjectFromTemplateInput },
|
||||
lrn,
|
||||
auctionParams
|
||||
}: { organizationSlug: string; data: AddProjectFromTemplateInput; lrn: string; auctionParams: AuctionParams },
|
||||
context: any,
|
||||
) => {
|
||||
try {
|
||||
@@ -211,6 +220,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
@@ -223,11 +234,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||
lrn,
|
||||
auctionParams
|
||||
}: { organizationSlug: string; data: DeepPartial<Project>; lrn: string; auctionParams: AuctionParams },
|
||||
context: any,
|
||||
) => {
|
||||
try {
|
||||
return await service.addProject(context.user, organizationSlug, data);
|
||||
return await service.addProject(context.user, organizationSlug, data, lrn, auctionParams);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
throw err;
|
||||
|
||||
@@ -22,6 +22,13 @@ enum DeploymentStatus {
|
||||
Deleting
|
||||
}
|
||||
|
||||
enum AuctionStatus {
|
||||
completed
|
||||
reveal
|
||||
commit
|
||||
expired
|
||||
}
|
||||
|
||||
enum DomainStatus {
|
||||
Live
|
||||
Pending
|
||||
@@ -65,6 +72,8 @@ type Project {
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
description: String
|
||||
deployerLrns: [String]
|
||||
auctionId: String
|
||||
template: String
|
||||
framework: String
|
||||
webhooks: [String!]
|
||||
@@ -74,7 +83,7 @@ type Project {
|
||||
updatedAt: String!
|
||||
organization: Organization!
|
||||
icon: String
|
||||
subDomain: String
|
||||
baseDomains: [String!]
|
||||
}
|
||||
|
||||
type ProjectMember {
|
||||
@@ -94,7 +103,9 @@ type Deployment {
|
||||
commitMessage: String!
|
||||
url: String
|
||||
environment: Environment!
|
||||
deployerLrn: String
|
||||
isCurrent: Boolean!
|
||||
baseDomain: String
|
||||
status: DeploymentStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
@@ -182,6 +193,48 @@ input FilterDomainsInput {
|
||||
status: DomainStatus
|
||||
}
|
||||
|
||||
type Fee {
|
||||
type: String!
|
||||
quantity: String!
|
||||
}
|
||||
|
||||
type Bid {
|
||||
auctionId: String!
|
||||
bidderAddress: String!
|
||||
status: String!
|
||||
commitHash: String!
|
||||
commitTime: String
|
||||
commitFee: Fee
|
||||
revealTime: String
|
||||
revealFee: Fee
|
||||
bidAmount: Fee
|
||||
}
|
||||
|
||||
type Auction {
|
||||
id: String!
|
||||
kind: String!
|
||||
status: String!
|
||||
ownerAddress: String!
|
||||
createTime: String!
|
||||
commitsEndTime: String!
|
||||
revealsEndTime: String!
|
||||
commitFee: Fee!
|
||||
revealFee: Fee!
|
||||
minimumBid: Fee
|
||||
winnerAddresses: [String!]!
|
||||
winnerBids: [Fee!]
|
||||
winnerPrice: Fee
|
||||
maxPrice: Fee
|
||||
numProviders: Int!
|
||||
fundsReleased: Boolean!
|
||||
bids: [Bid!]!
|
||||
}
|
||||
|
||||
input AuctionParams {
|
||||
maxPrice: String,
|
||||
numProviders: Int,
|
||||
}
|
||||
|
||||
type Query {
|
||||
user: User!
|
||||
organizations: [Organization!]
|
||||
@@ -192,6 +245,7 @@ type Query {
|
||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||
projectMembers(projectId: String!): [ProjectMember!]
|
||||
searchProjects(searchText: String!): [Project!]
|
||||
getAuctionData(auctionId: String!): Auction!
|
||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||
}
|
||||
|
||||
@@ -215,8 +269,15 @@ type Mutation {
|
||||
addProjectFromTemplate(
|
||||
organizationSlug: String!
|
||||
data: AddProjectFromTemplateInput
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
): Project!
|
||||
addProject(
|
||||
organizationSlug: String!
|
||||
data: AddProjectInput!
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
): Project!
|
||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||
redeployToProd(deploymentId: String!): Boolean!
|
||||
deleteProject(projectId: String!): Boolean!
|
||||
|
||||
+341
-140
@@ -1,12 +1,12 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||
import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm';
|
||||
import { Octokit, RequestError } from 'octokit';
|
||||
|
||||
import { OAuthApp } from '@octokit/oauth-app';
|
||||
|
||||
import { Database } from './database';
|
||||
import { Deployment, DeploymentStatus, Environment } from './entity/Deployment';
|
||||
import { ApplicationRecord, Deployment, DeploymentStatus, Environment } from './entity/Deployment';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Organization } from './entity/Organization';
|
||||
@@ -19,10 +19,11 @@ import {
|
||||
AddProjectFromTemplateInput,
|
||||
AppDeploymentRecord,
|
||||
AppDeploymentRemovalRecord,
|
||||
AuctionParams,
|
||||
GitPushEventPayload,
|
||||
PackageJSON,
|
||||
} from './types';
|
||||
import { Role } from './entity/UserOrganization';
|
||||
import { getRepoDetails } from './utils';
|
||||
|
||||
const log = debug('snowball:service');
|
||||
|
||||
@@ -39,15 +40,16 @@ interface Config {
|
||||
export class Service {
|
||||
private db: Database;
|
||||
private oauthApp: OAuthApp;
|
||||
private registry: Registry;
|
||||
private laconicRegistry: Registry;
|
||||
private config: Config;
|
||||
|
||||
private deployRecordCheckTimeout?: NodeJS.Timeout;
|
||||
private auctionStatusCheckTimeout?: NodeJS.Timeout;
|
||||
|
||||
constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) {
|
||||
this.db = db;
|
||||
this.oauthApp = app;
|
||||
this.registry = registry;
|
||||
this.laconicRegistry = registry;
|
||||
this.config = config;
|
||||
this.init();
|
||||
}
|
||||
@@ -60,6 +62,8 @@ export class Service {
|
||||
this.checkDeployRecordsAndUpdate();
|
||||
// Start check for ApplicationDeploymentRemovalRecords asynchronously
|
||||
this.checkDeploymentRemovalRecordsAndUpdate();
|
||||
// Start check for Deployment Auctions asynchronously
|
||||
this.checkAuctionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +71,7 @@ export class Service {
|
||||
*/
|
||||
destroy(): void {
|
||||
clearTimeout(this.deployRecordCheckTimeout);
|
||||
clearTimeout(this.auctionStatusCheckTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,7 +113,7 @@ export class Service {
|
||||
}
|
||||
|
||||
// Fetch ApplicationDeploymentRecord for deployments
|
||||
const records = await this.registry.getDeploymentRecords(deployments);
|
||||
const records = await this.laconicRegistry.getDeploymentRecords(deployments);
|
||||
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
||||
|
||||
// Update deployments for which ApplicationDeploymentRecords were returned
|
||||
@@ -141,7 +146,7 @@ export class Service {
|
||||
|
||||
// Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||
const records =
|
||||
await this.registry.getDeploymentRemovalRecords(deployments);
|
||||
await this.laconicRegistry.getDeploymentRemovalRecords(deployments);
|
||||
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
|
||||
|
||||
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
|
||||
@@ -157,41 +162,24 @@ export class Service {
|
||||
|
||||
/**
|
||||
* Update deployments with ApplicationDeploymentRecord data
|
||||
* Deployments that are completed but not updated in DB
|
||||
*/
|
||||
async updateDeploymentsWithRecordData(
|
||||
records: AppDeploymentRecord[],
|
||||
): Promise<void> {
|
||||
// Get deployments for ApplicationDeploymentRecords
|
||||
// get and update deployments to be updated using request id
|
||||
const deployments = await this.db.getDeployments({
|
||||
where: records.map((record) => ({
|
||||
applicationRecordId: record.attributes.application,
|
||||
applicationDeploymentRequestId: record.attributes.request,
|
||||
})),
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
// Get project IDs of deployments that are in production environment
|
||||
const productionDeploymentProjectIds = deployments.reduce(
|
||||
(acc, deployment): Set<string> => {
|
||||
if (deployment.environment === Environment.Production) {
|
||||
acc.add(deployment.projectId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
new Set<string>(),
|
||||
);
|
||||
|
||||
// Set old deployments isCurrent to false
|
||||
await this.db.updateDeploymentsByProjectIds(
|
||||
Array.from(productionDeploymentProjectIds),
|
||||
{ isCurrent: false },
|
||||
);
|
||||
|
||||
const recordToDeploymentsMap = deployments.reduce(
|
||||
(acc: { [key: string]: Deployment }, deployment) => {
|
||||
acc[deployment.applicationRecordId] = deployment;
|
||||
acc[deployment.applicationDeploymentRequestId!] = deployment;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
@@ -199,21 +187,64 @@ export class Service {
|
||||
|
||||
// Update deployment data for ApplicationDeploymentRecords
|
||||
const deploymentUpdatePromises = records.map(async (record) => {
|
||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||
const deployment = recordToDeploymentsMap[record.attributes.request];
|
||||
const project = await this.getProjectById(deployment.projectId)
|
||||
assert(project)
|
||||
|
||||
await this.db.updateDeploymentById(deployment.id, {
|
||||
applicationDeploymentRecordId: record.id,
|
||||
applicationDeploymentRecordData: record.attributes,
|
||||
url: record.attributes.url,
|
||||
status: DeploymentStatus.Ready,
|
||||
isCurrent: deployment.environment === Environment.Production,
|
||||
});
|
||||
const parts = record.attributes.url.replace('https://', '').split('.');
|
||||
const baseDomain = parts.slice(1).join('.');
|
||||
|
||||
deployment.applicationDeploymentRecordId = record.id;
|
||||
deployment.applicationDeploymentRecordData = record.attributes;
|
||||
deployment.url = record.attributes.url;
|
||||
deployment.baseDomain = baseDomain;
|
||||
deployment.status = DeploymentStatus.Ready;
|
||||
deployment.isCurrent = deployment.environment === Environment.Production;
|
||||
|
||||
await this.db.updateDeploymentById(deployment.id, deployment);
|
||||
|
||||
const baseDomains = project.baseDomains || [];
|
||||
|
||||
if (!baseDomains.includes(baseDomain)) {
|
||||
baseDomains.push(baseDomain);
|
||||
}
|
||||
|
||||
await this.db.updateProjectById(project.id, {
|
||||
baseDomains
|
||||
})
|
||||
|
||||
log(
|
||||
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(deploymentUpdatePromises);
|
||||
|
||||
// if iscurrent is true for this deployment then update the old ones
|
||||
const prodDeployments = Object.values(recordToDeploymentsMap).filter(deployment => deployment.isCurrent);
|
||||
|
||||
// Get deployment IDs of deployments that are in production environment
|
||||
for (const deployment of prodDeployments) {
|
||||
const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId);
|
||||
const oldDeployments = projectDeployments
|
||||
.filter(projectDeployment => projectDeployment.deployerLrn === deployment.deployerLrn && projectDeployment.id !== deployment.id);
|
||||
for (const oldDeployment of oldDeployments) {
|
||||
await this.db.updateDeployment(
|
||||
{ id: oldDeployment.id },
|
||||
{ isCurrent: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get old deployments for ApplicationDeploymentRecords
|
||||
// flter out deps with is current false
|
||||
|
||||
// loop over these deps
|
||||
// get the project
|
||||
// get all the deployemnts in that proj with the same deployer lrn (query filter not above updated dep)
|
||||
// set is current to false
|
||||
|
||||
|
||||
await Promise.all(deploymentUpdatePromises);
|
||||
}
|
||||
|
||||
@@ -262,6 +293,58 @@ export class Service {
|
||||
await Promise.all(deploymentUpdatePromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status for all ongoing auctions
|
||||
* Calls the createDeploymentFromAuction method for deployments with completed auctions
|
||||
*/
|
||||
async checkAuctionStatus(): Promise<void> {
|
||||
const allProjects = await this.db.getProjects({
|
||||
where: {
|
||||
auctionId: Not(IsNull()),
|
||||
},
|
||||
relations: ['deployments'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
// Should only check on the first deployment
|
||||
const projects = allProjects.filter(project => {
|
||||
if (project.deletedAt !== null) return false;
|
||||
|
||||
return project.deployments.length === 0;
|
||||
});
|
||||
|
||||
const auctionIds = projects.map((project) => project.auctionId);
|
||||
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(auctionIds);
|
||||
|
||||
if (completedAuctionIds) {
|
||||
const projectsToBedeployed = projects.filter((project) =>
|
||||
completedAuctionIds.includes(project.auctionId!)
|
||||
);
|
||||
|
||||
for (const project of projectsToBedeployed) {
|
||||
const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!);
|
||||
|
||||
if (!deployerLrns) {
|
||||
log(`No winning deployer for auction ${project!.auctionId}`);
|
||||
} else {
|
||||
// Update project with deployer LRNs
|
||||
await this.db.updateProjectById(project.id!, {
|
||||
deployerLrns
|
||||
});
|
||||
|
||||
for (const deployer of deployerLrns) {
|
||||
log(`Creating deployment for deployer LRN ${deployer}`);
|
||||
await this.createDeploymentFromAuction(project, deployer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.auctionStatusCheckTimeout = setTimeout(() => {
|
||||
this.checkAuctionStatus();
|
||||
}, this.config.registryConfig.checkAuctionStatusDelay);
|
||||
}
|
||||
|
||||
async getUser(userId: string): Promise<User | null> {
|
||||
return this.db.getUser({
|
||||
where: {
|
||||
@@ -519,6 +602,7 @@ export class Service {
|
||||
domain: prodBranchDomains[0],
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage,
|
||||
deployerLrn: oldDeployment.deployerLrn
|
||||
});
|
||||
|
||||
return newDeployment;
|
||||
@@ -527,45 +611,20 @@ export class Service {
|
||||
async createDeployment(
|
||||
userId: string,
|
||||
octokit: Octokit,
|
||||
data: DeepPartial<Deployment>,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<Deployment> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
log(
|
||||
`Creating deployment in project ${data.project.name} from branch ${data.branch}`,
|
||||
);
|
||||
const [owner, repo] = data.project.repository.split('/');
|
||||
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'package.json',
|
||||
ref: data.commitHash,
|
||||
});
|
||||
|
||||
if (!packageJSONData) {
|
||||
throw new Error('Package.json file not found');
|
||||
}
|
||||
|
||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
const repoUrl = (
|
||||
await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
).data.html_url;
|
||||
|
||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||
const { applicationRecordId, applicationRecordData } =
|
||||
await this.registry.createApplicationRecord({
|
||||
appName: repo,
|
||||
packageJSON,
|
||||
await this.laconicRegistry.createApplicationRecord({
|
||||
octokit,
|
||||
repository: data.project.repository,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl,
|
||||
});
|
||||
|
||||
// Update previous deployment with prod branch domain
|
||||
@@ -581,6 +640,125 @@ export class Service {
|
||||
);
|
||||
}
|
||||
|
||||
const newDeployment = await this.createDeploymentFromData(userId, data, data.deployerLrn!, applicationRecordId, applicationRecordData);
|
||||
|
||||
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: data.deployerLrn!
|
||||
});
|
||||
}
|
||||
|
||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
||||
await this.laconicRegistry.createApplicationDeploymentRequest({
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
repository: repoUrl,
|
||||
lrn: data.deployerLrn!,
|
||||
environmentVariables: environmentVariablesObj,
|
||||
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
||||
});
|
||||
|
||||
await this.db.updateDeploymentById(newDeployment.id, {
|
||||
applicationDeploymentRequestId,
|
||||
applicationDeploymentRequestData,
|
||||
});
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
|
||||
async createDeploymentFromAuction(
|
||||
project: DeepPartial<Project>,
|
||||
deployerLrn: string
|
||||
): Promise<Deployment> {
|
||||
const octokit = await this.getOctokit(project.ownerId!);
|
||||
const [owner, repo] = project.repository!.split('/');
|
||||
|
||||
const repoUrl = (
|
||||
await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
).data.html_url;
|
||||
|
||||
const {
|
||||
data: [latestCommit],
|
||||
} = await octokit.rest.repos.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
sha: project.prodBranch,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const lrn = this.laconicRegistry.getLrn(repo);
|
||||
const [record] = await this.laconicRegistry.getRecordsByName(lrn);
|
||||
const applicationRecordId = record.id;
|
||||
const applicationRecordData = record.attributes;
|
||||
|
||||
// Create deployment with prod branch and latest commit
|
||||
const deploymentData = {
|
||||
project,
|
||||
branch: project.prodBranch,
|
||||
environment: Environment.Production,
|
||||
domain: null,
|
||||
commitHash: latestCommit.sha,
|
||||
commitMessage: latestCommit.commit.message,
|
||||
};
|
||||
|
||||
const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
||||
// Create requests for all the deployers
|
||||
await this.laconicRegistry.createApplicationDeploymentRequest({
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
repository: repoUrl,
|
||||
auctionId: project.auctionId!,
|
||||
lrn: deployerLrn,
|
||||
environmentVariables: environmentVariablesObj,
|
||||
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
||||
});
|
||||
|
||||
await this.db.updateDeploymentById(newDeployment.id, {
|
||||
applicationDeploymentRequestId,
|
||||
applicationDeploymentRequestData,
|
||||
});
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
|
||||
async createDeploymentFromData(
|
||||
userId: string,
|
||||
data: DeepPartial<Deployment>,
|
||||
deployerLrn: string,
|
||||
applicationRecordId: string,
|
||||
applicationRecordData: ApplicationRecord,
|
||||
): Promise<Deployment> {
|
||||
const newDeployment = await this.db.addDeployment({
|
||||
project: data.project,
|
||||
branch: data.branch,
|
||||
@@ -594,52 +772,10 @@ export class Service {
|
||||
createdBy: Object.assign(new User(), {
|
||||
id: userId,
|
||||
}),
|
||||
deployerLrn,
|
||||
});
|
||||
|
||||
log(
|
||||
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`,
|
||||
);
|
||||
|
||||
const environmentVariables =
|
||||
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
|
||||
environment: Environment.Production,
|
||||
});
|
||||
|
||||
const environmentVariablesObj = environmentVariables.reduce(
|
||||
(acc, env) => {
|
||||
acc[env.key] = env.value;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
|
||||
// To set project DNS
|
||||
if (data.environment === Environment.Production) {
|
||||
// On deleting deployment later, project DNS deployment is also deleted
|
||||
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
|
||||
await this.registry.createApplicationDeploymentRequest({
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
repository: repoUrl,
|
||||
environmentVariables: environmentVariablesObj,
|
||||
dns: `${newDeployment.project.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
||||
await this.registry.createApplicationDeploymentRequest({
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
repository: repoUrl,
|
||||
environmentVariables: environmentVariablesObj,
|
||||
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
||||
});
|
||||
|
||||
await this.db.updateDeploymentById(newDeployment.id, {
|
||||
applicationDeploymentRequestId,
|
||||
applicationDeploymentRequestData,
|
||||
});
|
||||
log(`Created deployment ${newDeployment.id}`);
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
@@ -648,6 +784,8 @@ export class Service {
|
||||
user: User,
|
||||
organizationSlug: string,
|
||||
data: AddProjectFromTemplateInput,
|
||||
lrn?: string,
|
||||
auctionParams?: AuctionParams
|
||||
): Promise<Project | undefined> {
|
||||
try {
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
@@ -678,7 +816,7 @@ export class Service {
|
||||
repository: gitRepo.data.full_name,
|
||||
// TODO: Set selected template
|
||||
template: 'webapp',
|
||||
});
|
||||
}, lrn, auctionParams);
|
||||
|
||||
if (!project || !project.id) {
|
||||
throw new Error('Failed to create project from template');
|
||||
@@ -695,6 +833,8 @@ export class Service {
|
||||
user: User,
|
||||
organizationSlug: string,
|
||||
data: DeepPartial<Project>,
|
||||
lrn?: string,
|
||||
auctionParams?: AuctionParams
|
||||
): Promise<Project | undefined> {
|
||||
const organization = await this.db.getOrganization({
|
||||
where: {
|
||||
@@ -706,6 +846,7 @@ export class Service {
|
||||
}
|
||||
|
||||
const project = await this.db.addProject(user, organization.id, data);
|
||||
log(`Project created ${project.id}`);
|
||||
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
const [owner, repo] = project.repository.split('/');
|
||||
@@ -720,19 +861,26 @@ export class Service {
|
||||
});
|
||||
|
||||
// Create deployment with prod branch and latest commit
|
||||
const deployment = await this.createDeployment(user.id, octokit, {
|
||||
const deploymentData = {
|
||||
project,
|
||||
branch: project.prodBranch,
|
||||
environment: Environment.Production,
|
||||
domain: null,
|
||||
commitHash: latestCommit.sha,
|
||||
commitMessage: latestCommit.commit.message,
|
||||
});
|
||||
deployerLrn: lrn
|
||||
};
|
||||
|
||||
if (auctionParams) {
|
||||
const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData);
|
||||
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId })
|
||||
} else {
|
||||
await this.createDeployment(user.id, octokit, deploymentData);
|
||||
await this.updateProject(project.id, { deployerLrns: [lrn!] })
|
||||
}
|
||||
|
||||
await this.createRepoHook(octokit, project);
|
||||
|
||||
console.log('projectid is', project.id);
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
@@ -798,18 +946,29 @@ export class Service {
|
||||
branch,
|
||||
});
|
||||
|
||||
// Create deployment with branch and latest commit in GitHub data
|
||||
await this.createDeployment(project.ownerId, octokit, {
|
||||
project,
|
||||
branch,
|
||||
environment:
|
||||
project.prodBranch === branch
|
||||
? Environment.Production
|
||||
: Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message,
|
||||
});
|
||||
const deployers = project.deployerLrns;
|
||||
if (!deployers) {
|
||||
log(`No deployer present for project ${project.id}`)
|
||||
return;
|
||||
}
|
||||
|
||||
for (const deployer of deployers) {
|
||||
// Create deployment with branch and latest commit in GitHub data
|
||||
await this.createDeployment(project.ownerId, octokit,
|
||||
{
|
||||
project,
|
||||
branch,
|
||||
environment:
|
||||
project.prodBranch === branch
|
||||
? Environment.Production
|
||||
: Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message,
|
||||
deployerLrn: deployer
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,15 +1018,25 @@ export class Service {
|
||||
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
|
||||
const newDeployment = await this.createDeployment(user.id, octokit, {
|
||||
project: oldDeployment.project,
|
||||
// TODO: Put isCurrent field in project
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: oldDeployment.domain,
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage,
|
||||
});
|
||||
let newDeployment: Deployment;
|
||||
|
||||
if (oldDeployment.project.auctionId) {
|
||||
// TODO: Discuss creating applicationRecord for redeployments
|
||||
newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployerLrn);
|
||||
} else {
|
||||
newDeployment = await this.createDeployment(user.id, octokit,
|
||||
{
|
||||
project: oldDeployment.project,
|
||||
// TODO: Put isCurrent field in project
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: oldDeployment.domain,
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage,
|
||||
deployerLrn: oldDeployment.deployerLrn
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
@@ -919,10 +1088,11 @@ 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.subDomain}`;
|
||||
const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.baseDomain}`;
|
||||
|
||||
// TODO: Store the latest DNS deployment record
|
||||
const deploymentRecords =
|
||||
await this.registry.getDeploymentRecordsByFilter({
|
||||
await this.laconicRegistry.getDeploymentRecordsByFilter({
|
||||
application: deployment.applicationRecordId,
|
||||
url: currentDeploymentURL,
|
||||
});
|
||||
@@ -935,14 +1105,20 @@ export class Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.registry.createApplicationDeploymentRemovalRequest({
|
||||
deploymentId: deploymentRecords[0].id,
|
||||
// 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.deployerLrn
|
||||
});
|
||||
}
|
||||
|
||||
const result =
|
||||
await this.registry.createApplicationDeploymentRemovalRequest({
|
||||
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
||||
deploymentId: deployment.applicationDeploymentRecordId,
|
||||
deployerLrn: deployment.deployerLrn
|
||||
});
|
||||
|
||||
await this.db.updateDeploymentById(deployment.id, {
|
||||
@@ -1077,4 +1253,29 @@ export class Service {
|
||||
): Promise<boolean> {
|
||||
return this.db.updateUser(user, data);
|
||||
}
|
||||
|
||||
async getEnvVariables(
|
||||
projectId: string,
|
||||
): Promise<{ [key: string]: string }> {
|
||||
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId, {
|
||||
environment: Environment.Production,
|
||||
});
|
||||
|
||||
const environmentVariablesObj = environmentVariables.reduce(
|
||||
(acc, env) => {
|
||||
acc[env.key] = env.value;
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
|
||||
return environmentVariablesObj;
|
||||
}
|
||||
|
||||
async getAuctionData(
|
||||
auctionId: string
|
||||
): Promise<any> {
|
||||
const auctions = await this.laconicRegistry.getAuctionData(auctionId);
|
||||
return auctions[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface GitPushEventPayload {
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
application: string;
|
||||
auction: string;
|
||||
deployer: string;
|
||||
dns: string;
|
||||
meta: string;
|
||||
name: string;
|
||||
@@ -69,3 +71,8 @@ export interface AddProjectFromTemplateInput {
|
||||
name: string;
|
||||
isPrivate: boolean;
|
||||
}
|
||||
|
||||
export interface AuctionParams {
|
||||
maxPrice: string,
|
||||
numProviders: number,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import fs from 'fs-extra';
|
||||
import { Octokit } from 'octokit';
|
||||
import path from 'path';
|
||||
import toml from 'toml';
|
||||
import debug from 'debug';
|
||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { Config } from './config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||
import { PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:utils');
|
||||
|
||||
@@ -78,3 +80,43 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const getRepoDetails = async (
|
||||
octokit: Octokit,
|
||||
repository: string,
|
||||
commitHash: string | undefined,
|
||||
): Promise<{
|
||||
repo: string;
|
||||
packageJSON: PackageJSON;
|
||||
repoUrl: string;
|
||||
}> => {
|
||||
const [owner, repo] = repository.split('/');
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'package.json',
|
||||
ref: commitHash,
|
||||
});
|
||||
|
||||
if (!packageJSONData) {
|
||||
throw new Error('Package.json file not found');
|
||||
}
|
||||
|
||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
const repoUrl = (
|
||||
await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
).data.html_url;
|
||||
|
||||
return {
|
||||
repo,
|
||||
packageJSON,
|
||||
repoUrl
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ async function main() {
|
||||
|
||||
// Remove deployment for project subdomain if deployment is for production environment
|
||||
if (deployment.environment === Environment.Production) {
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.baseDomain}`;
|
||||
|
||||
await registry.setRecord(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user