import debug from 'debug'; import assert from 'assert'; import { inc as semverInc } from 'semver'; import { DateTime } from 'luxon'; import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk'; import { RegistryConfig } from './config'; import { ApplicationRecord, Deployment, ApplicationDeploymentRequest, ApplicationDeploymentRemovalRequest } from './entity/Deployment'; import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types'; import { sleep } from './utils'; const log = debug('snowball:registry'); const APP_RECORD_TYPE = 'ApplicationRecord'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'; const SLEEP_DURATION = 1000; // TODO: Move registry code to laconic-sdk/watcher-ts export class Registry { private registry: LaconicRegistry; private registryConfig: RegistryConfig; constructor (registryConfig: RegistryConfig) { this.registryConfig = registryConfig; this.registry = new LaconicRegistry( registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId ); } async createApplicationRecord ({ appName, packageJSON, commitHash, appType, repoUrl }: { appName: string; packageJSON: PackageJSON; commitHash: string; appType: string; repoUrl: string; }): Promise<{ applicationRecordId: string; applicationRecordData: ApplicationRecord; }> { // 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 const records = await this.registry.queryRecords( { type: APP_RECORD_TYPE, name: packageJSON.name }, true ); // Get next version of record 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' ); assert(nextVersion, 'Application record version not valid'); // Create record of type ApplicationRecord and publish const applicationRecord = { type: APP_RECORD_TYPE, version: nextVersion, repository_ref: commitHash, repository: [repoUrl], app_type: appType, name: appName, ...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.license && { license: packageJSON.license }), ...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }), ...(packageJSON.version && { app_version: packageJSON.version }) }; const result = await this.registry.setRecord( { privateKey: this.registryConfig.privateKey, record: applicationRecord, bondId: this.registryConfig.bondId }, '', this.registryConfig.fee ); log('Application record data:', applicationRecord); // TODO: Discuss computation of CRN const crn = this.getCrn(appName); log(`Setting name: ${crn} for record ID: ${result.data.id}`); await sleep(SLEEP_DURATION); await this.registry.setName( { cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee ); await sleep(SLEEP_DURATION); await this.registry.setName( { cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee ); await sleep(SLEEP_DURATION); await this.registry.setName( { cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee ); return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord }; } async createApplicationDeploymentRequest (data: { deployment: Deployment, appName: string, repository: string, environmentVariables: { [key: string]: string }, dns: string, }): Promise<{ applicationDeploymentRequestId: string; applicationDeploymentRequestData: ApplicationDeploymentRequest; }> { const crn = this.getCrn(data.appName); 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 = { type: APP_DEPLOYMENT_REQUEST_TYPE, version: '1.0.0', name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, application: `${crn}@${applicationRecord.attributes.app_version}`, dns: data.dns, // TODO: Not set in test-progressive-web-app CI // deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN', // https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b config: JSON.stringify({ env: data.environmentVariables }), meta: JSON.stringify({ note: `Added by Snowball @ ${DateTime.utc().toFormat( "EEE LLL dd HH:mm:ss 'UTC' yyyy" )}`, repository: data.repository, repository_ref: data.deployment.commitHash }) }; await sleep(SLEEP_DURATION); 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); return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest }; } /** * Fetch ApplicationDeploymentRecords for deployments */ async getDeploymentRecords ( deployments: Deployment[] ): Promise { // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments // TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute const records = await this.registry.queryRecords( { type: APP_DEPLOYMENT_RECORD_TYPE }, true ); // Filter records with ApplicationRecord ID and Deployment specific URL return records.filter((record: AppDeploymentRecord) => deployments.some( (deployment) => deployment.applicationRecordId === record.attributes.application && record.attributes.url.includes(deployment.id) ) ); } /** * Fetch ApplicationDeploymentRecords by filter */ async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise { return this.registry.queryRecords( { type: APP_DEPLOYMENT_RECORD_TYPE, ...filter }, true ); } /** * Fetch ApplicationDeploymentRemovalRecords for deployments */ async getDeploymentRemovalRecords ( deployments: Deployment[] ): Promise { // 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 ) ); } async createApplicationDeploymentRemovalRequest (data: { deploymentId: string; }): Promise<{ applicationDeploymentRemovalRequestId: string; applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest; }> { const applicationDeploymentRemovalRequest = { type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE, version: '1.0.0', deployment: data.deploymentId }; 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 }; } getCrn (appName: string): string { assert(this.registryConfig.authority, "Authority doesn't exist"); return `crn://${this.registryConfig.authority}/applications/${appName}`; } }