2024-02-12 06:04:01 +00:00
|
|
|
import debug from 'debug';
|
2024-02-14 05:33:22 +00:00
|
|
|
import assert from 'assert';
|
2024-02-12 06:04:01 +00:00
|
|
|
import { inc as semverInc } from 'semver';
|
2024-02-14 05:33:22 +00:00
|
|
|
import { DateTime } from 'luxon';
|
2024-02-12 06:04:01 +00:00
|
|
|
|
|
|
|
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
|
|
|
|
|
|
|
import { RegistryConfig } from './config';
|
2024-02-22 05:45:17 +00:00
|
|
|
import {
|
|
|
|
ApplicationRecord,
|
|
|
|
Deployment,
|
2024-03-27 16:41:53 +00:00
|
|
|
ApplicationDeploymentRequest,
|
|
|
|
ApplicationDeploymentRemovalRequest
|
2024-02-22 05:45:17 +00:00
|
|
|
} from './entity/Deployment';
|
2024-04-15 14:06:04 +00:00
|
|
|
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
2024-02-29 13:03:34 +00:00
|
|
|
import { sleep } from './utils';
|
2024-02-12 06:04:01 +00:00
|
|
|
|
|
|
|
const log = debug('snowball:registry');
|
|
|
|
|
|
|
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
2024-02-19 08:13:29 +00:00
|
|
|
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
2024-03-27 16:41:53 +00:00
|
|
|
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
2024-02-19 08:13:29 +00:00
|
|
|
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
2024-04-15 14:06:04 +00:00
|
|
|
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
2024-02-29 13:03:34 +00:00
|
|
|
const SLEEP_DURATION = 1000;
|
2024-02-12 06:04:01 +00:00
|
|
|
|
2024-02-12 09:48:00 +00:00
|
|
|
// TODO: Move registry code to laconic-sdk/watcher-ts
|
2024-02-12 06:04:01 +00:00
|
|
|
export class Registry {
|
|
|
|
private registry: LaconicRegistry;
|
|
|
|
private registryConfig: RegistryConfig;
|
|
|
|
|
2024-02-22 05:45:17 +00:00
|
|
|
constructor (registryConfig: RegistryConfig) {
|
2024-02-12 06:04:01 +00:00
|
|
|
this.registryConfig = registryConfig;
|
2024-02-22 05:45:17 +00:00
|
|
|
this.registry = new LaconicRegistry(
|
|
|
|
registryConfig.gqlEndpoint,
|
|
|
|
registryConfig.restEndpoint,
|
|
|
|
registryConfig.chainId
|
|
|
|
);
|
2024-02-12 06:04:01 +00:00
|
|
|
}
|
|
|
|
|
2024-02-14 05:33:22 +00:00
|
|
|
async createApplicationRecord ({
|
2024-02-22 04:34:33 +00:00
|
|
|
appName,
|
2024-02-14 05:33:22 +00:00
|
|
|
packageJSON,
|
|
|
|
commitHash,
|
2024-02-15 11:54:57 +00:00
|
|
|
appType,
|
|
|
|
repoUrl
|
2024-02-14 05:33:22 +00:00
|
|
|
}: {
|
2024-02-22 05:45:17 +00:00
|
|
|
appName: string;
|
|
|
|
packageJSON: PackageJSON;
|
|
|
|
commitHash: string;
|
|
|
|
appType: string;
|
|
|
|
repoUrl: string;
|
|
|
|
}): Promise<{
|
|
|
|
applicationRecordId: string;
|
|
|
|
applicationRecordData: ApplicationRecord;
|
|
|
|
}> {
|
2024-02-12 06:04:01 +00:00
|
|
|
// Use laconic-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
|
2024-02-22 05:45:17 +00:00
|
|
|
const records = await this.registry.queryRecords(
|
|
|
|
{
|
|
|
|
type: APP_RECORD_TYPE,
|
|
|
|
name: packageJSON.name
|
|
|
|
},
|
|
|
|
true
|
|
|
|
);
|
2024-02-12 06:04:01 +00:00
|
|
|
|
|
|
|
// Get next version of record
|
2024-02-22 05:45:17 +00:00
|
|
|
const bondRecords = records.filter(
|
|
|
|
(record: any) => record.bondId === this.registryConfig.bondId
|
|
|
|
);
|
|
|
|
const [latestBondRecord] = bondRecords.sort(
|
|
|
|
(a: any, b: any) =>
|
|
|
|
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
|
|
|
);
|
|
|
|
const nextVersion = semverInc(
|
|
|
|
latestBondRecord?.attributes.version ?? '0.0.0',
|
|
|
|
'patch'
|
|
|
|
);
|
2024-02-12 06:04:01 +00:00
|
|
|
|
2024-02-14 05:33:22 +00:00
|
|
|
assert(nextVersion, 'Application record version not valid');
|
|
|
|
|
2024-02-12 06:04:01 +00:00
|
|
|
// Create record of type ApplicationRecord and publish
|
|
|
|
const applicationRecord = {
|
|
|
|
type: APP_RECORD_TYPE,
|
2024-02-14 05:33:22 +00:00
|
|
|
version: nextVersion,
|
|
|
|
repository_ref: commitHash,
|
2024-02-15 11:54:57 +00:00
|
|
|
repository: [repoUrl],
|
2024-02-14 05:33:22 +00:00
|
|
|
app_type: appType,
|
2024-02-22 04:34:33 +00:00
|
|
|
name: appName,
|
2024-02-14 05:33:22 +00:00
|
|
|
...(packageJSON.description && { description: packageJSON.description }),
|
|
|
|
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
|
|
|
...(packageJSON.license && { license: packageJSON.license }),
|
2024-02-22 05:45:17 +00:00
|
|
|
...(packageJSON.author && {
|
|
|
|
author:
|
|
|
|
typeof packageJSON.author === 'object'
|
|
|
|
? JSON.stringify(packageJSON.author)
|
|
|
|
: packageJSON.author
|
|
|
|
}),
|
2024-02-14 05:33:22 +00:00
|
|
|
...(packageJSON.version && { app_version: packageJSON.version })
|
2024-02-12 06:04:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const result = await this.registry.setRecord(
|
|
|
|
{
|
|
|
|
privateKey: this.registryConfig.privateKey,
|
|
|
|
record: applicationRecord,
|
|
|
|
bondId: this.registryConfig.bondId
|
|
|
|
},
|
|
|
|
'',
|
|
|
|
this.registryConfig.fee
|
|
|
|
);
|
|
|
|
|
|
|
|
log('Application record data:', applicationRecord);
|
|
|
|
|
2024-02-14 05:33:22 +00:00
|
|
|
// TODO: Discuss computation of CRN
|
2024-02-23 09:17:29 +00:00
|
|
|
const crn = this.getCrn(appName);
|
2024-02-15 11:54:57 +00:00
|
|
|
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
2024-02-12 06:04:01 +00:00
|
|
|
|
2024-02-29 13:03:34 +00:00
|
|
|
await sleep(SLEEP_DURATION);
|
2024-02-22 05:45:17 +00:00
|
|
|
await this.registry.setName(
|
|
|
|
{ cid: result.data.id, crn },
|
|
|
|
this.registryConfig.privateKey,
|
|
|
|
this.registryConfig.fee
|
|
|
|
);
|
2024-02-29 13:03:34 +00:00
|
|
|
|
|
|
|
await sleep(SLEEP_DURATION);
|
2024-02-22 05:45:17 +00:00
|
|
|
await this.registry.setName(
|
|
|
|
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
|
|
|
this.registryConfig.privateKey,
|
|
|
|
this.registryConfig.fee
|
|
|
|
);
|
2024-02-29 13:03:34 +00:00
|
|
|
|
|
|
|
await sleep(SLEEP_DURATION);
|
2024-02-22 05:45:17 +00:00
|
|
|
await this.registry.setName(
|
|
|
|
{
|
|
|
|
cid: result.data.id,
|
|
|
|
crn: `${crn}@${applicationRecord.repository_ref}`
|
|
|
|
},
|
|
|
|
this.registryConfig.privateKey,
|
|
|
|
this.registryConfig.fee
|
|
|
|
);
|
2024-02-12 06:04:01 +00:00
|
|
|
|
2024-02-22 05:45:17 +00:00
|
|
|
return {
|
|
|
|
applicationRecordId: result.data.id,
|
|
|
|
applicationRecordData: applicationRecord
|
|
|
|
};
|
2024-02-12 06:04:01 +00:00
|
|
|
}
|
|
|
|
|
2024-02-14 05:33:22 +00:00
|
|
|
async createApplicationDeploymentRequest (data: {
|
2024-02-22 07:20:35 +00:00
|
|
|
deployment: Deployment,
|
2024-02-14 05:33:22 +00:00
|
|
|
appName: string,
|
2024-02-15 12:32:37 +00:00
|
|
|
repository: string,
|
2024-02-29 13:03:34 +00:00
|
|
|
environmentVariables: { [key: string]: string },
|
|
|
|
dns: string,
|
2024-02-14 05:33:22 +00:00
|
|
|
}): Promise<{
|
2024-02-22 05:45:17 +00:00
|
|
|
applicationDeploymentRequestId: string;
|
|
|
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
2024-02-14 05:33:22 +00:00
|
|
|
}> {
|
2024-02-23 09:17:29 +00:00
|
|
|
const crn = this.getCrn(data.appName);
|
2024-02-12 06:04:01 +00:00
|
|
|
const records = await this.registry.resolveNames([crn]);
|
|
|
|
const applicationRecord = records[0];
|
|
|
|
|
|
|
|
if (!applicationRecord) {
|
|
|
|
throw new Error(`No record found for ${crn}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create record of type ApplicationDeploymentRequest and publish
|
|
|
|
const applicationDeploymentRequest = {
|
2024-02-19 08:13:29 +00:00
|
|
|
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
2024-02-12 06:04:01 +00:00
|
|
|
version: '1.0.0',
|
|
|
|
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
|
|
|
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
2024-02-29 13:03:34 +00:00
|
|
|
dns: data.dns,
|
2024-02-12 06:04:01 +00:00
|
|
|
|
|
|
|
// TODO: Not set in test-progressive-web-app CI
|
|
|
|
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
|
|
|
|
2024-02-14 05:33:22 +00:00
|
|
|
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
|
|
|
config: JSON.stringify({
|
2024-02-15 12:32:37 +00:00
|
|
|
env: data.environmentVariables
|
2024-02-14 05:33:22 +00:00
|
|
|
}),
|
|
|
|
meta: JSON.stringify({
|
2024-02-22 05:45:17 +00:00
|
|
|
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
|
|
|
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
|
|
|
)}`,
|
2024-02-14 05:33:22 +00:00
|
|
|
repository: data.repository,
|
2024-02-22 07:20:35 +00:00
|
|
|
repository_ref: data.deployment.commitHash
|
2024-02-14 05:33:22 +00:00
|
|
|
})
|
2024-02-12 06:04:01 +00:00
|
|
|
};
|
|
|
|
|
2024-02-29 13:03:34 +00:00
|
|
|
await sleep(SLEEP_DURATION);
|
2024-02-12 06:04:01 +00:00
|
|
|
const result = await this.registry.setRecord(
|
|
|
|
{
|
|
|
|
privateKey: this.registryConfig.privateKey,
|
|
|
|
record: applicationDeploymentRequest,
|
|
|
|
bondId: this.registryConfig.bondId
|
|
|
|
},
|
|
|
|
'',
|
|
|
|
this.registryConfig.fee
|
|
|
|
);
|
|
|
|
log(`Application deployment request record published: ${result.data.id}`);
|
|
|
|
log('Application deployment request data:', applicationDeploymentRequest);
|
|
|
|
|
2024-02-22 05:45:17 +00:00
|
|
|
return {
|
|
|
|
applicationDeploymentRequestId: result.data.id,
|
|
|
|
applicationDeploymentRequestData: applicationDeploymentRequest
|
|
|
|
};
|
2024-02-19 08:13:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch ApplicationDeploymentRecords for deployments
|
|
|
|
*/
|
2024-02-22 05:45:17 +00:00
|
|
|
async getDeploymentRecords (
|
|
|
|
deployments: Deployment[]
|
|
|
|
): Promise<AppDeploymentRecord[]> {
|
2024-02-19 08:13:29 +00:00
|
|
|
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
|
|
|
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
2024-02-22 05:45:17 +00:00
|
|
|
const records = await this.registry.queryRecords(
|
|
|
|
{
|
|
|
|
type: APP_DEPLOYMENT_RECORD_TYPE
|
|
|
|
},
|
|
|
|
true
|
|
|
|
);
|
2024-02-19 08:13:29 +00:00
|
|
|
|
2024-02-29 13:03:34 +00:00
|
|
|
// Filter records with ApplicationRecord ID and Deployment specific URL
|
2024-02-22 05:45:17 +00:00
|
|
|
return records.filter((record: AppDeploymentRecord) =>
|
|
|
|
deployments.some(
|
|
|
|
(deployment) =>
|
2024-02-29 13:03:34 +00:00
|
|
|
deployment.applicationRecordId === record.attributes.application &&
|
|
|
|
record.attributes.url.includes(deployment.id)
|
2024-02-22 05:45:17 +00:00
|
|
|
)
|
|
|
|
);
|
2024-02-12 06:04:01 +00:00
|
|
|
}
|
|
|
|
|
2024-04-15 14:06:04 +00:00
|
|
|
/**
|
|
|
|
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
|
|
|
*/
|
|
|
|
async getDeploymentRemovalRecords (
|
|
|
|
deployments: Deployment[]
|
|
|
|
): Promise<AppDeploymentRemovalRecord[]> {
|
|
|
|
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
|
|
|
const records = await this.registry.queryRecords(
|
|
|
|
{
|
|
|
|
type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE
|
|
|
|
},
|
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
// Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs
|
|
|
|
return records.filter((record: AppDeploymentRemovalRecord) =>
|
|
|
|
deployments.some(
|
|
|
|
(deployment) =>
|
|
|
|
deployment.applicationDeploymentRemovalRequestId === record.attributes.request &&
|
|
|
|
deployment.applicationDeploymentRecordId === record.attributes.deployment
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-03-27 16:41:53 +00:00
|
|
|
async createApplicationDeploymentRemovalRequest (data: {
|
2024-03-27 16:54:34 +00:00
|
|
|
deploymentId: string;
|
2024-03-27 16:41:53 +00:00
|
|
|
}): Promise<{
|
|
|
|
applicationDeploymentRemovalRequestId: string;
|
|
|
|
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
|
|
|
}> {
|
|
|
|
const applicationDeploymentRemovalRequest = {
|
|
|
|
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
|
|
|
version: '1.0.0',
|
2024-03-27 16:54:34 +00:00
|
|
|
deployment: data.deploymentId
|
2024-03-27 16:41:53 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const result = await this.registry.setRecord(
|
|
|
|
{
|
|
|
|
privateKey: this.registryConfig.privateKey,
|
|
|
|
record: applicationDeploymentRemovalRequest,
|
|
|
|
bondId: this.registryConfig.bondId
|
|
|
|
},
|
|
|
|
'',
|
|
|
|
this.registryConfig.fee
|
|
|
|
);
|
|
|
|
|
|
|
|
log(`Application deployment removal request record published: ${result.data.id}`);
|
|
|
|
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
|
|
|
|
|
|
|
return {
|
|
|
|
applicationDeploymentRemovalRequestId: result.data.id,
|
|
|
|
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-02-23 09:17:29 +00:00
|
|
|
getCrn (appName: string): string {
|
|
|
|
assert(this.registryConfig.authority, "Authority doesn't exist");
|
|
|
|
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
2024-02-12 06:04:01 +00:00
|
|
|
}
|
|
|
|
}
|