203 lines
5.4 KiB
TypeScript
203 lines
5.4 KiB
TypeScript
import {getRegistry} from './config.js';
|
|
import {Registry} from '@cerc-io/laconic-sdk';
|
|
|
|
import stringify from 'json-stable-stringify';
|
|
import {createHash} from 'crypto';
|
|
|
|
function generateHostnameForApp(app) {
|
|
const lastPart = app.attributes.name.split('/').pop();
|
|
const hasher = createHash('sha256');
|
|
hasher.update(app.attributes.name);
|
|
hasher.update('|');
|
|
if (Array.isArray(app.attributes.repository)) {
|
|
hasher.update(app.attributes.repository[0]);
|
|
} else {
|
|
hasher.update(app.attributes.repository);
|
|
}
|
|
return `${lastPart}-${hasher.digest('hex').slice(0, 10)}`;
|
|
}
|
|
|
|
export type RequestState = | 'SUBMITTED' | 'DEPLOYING' | 'DEPLOYED' | 'REMOVED' | 'CANCELLED' | 'ERROR';
|
|
|
|
export class RequestStatus {
|
|
public app?: any;
|
|
public lastState?: RequestState;
|
|
public url?: string;
|
|
public deployment?: string;
|
|
public lastUpdate?: string;
|
|
|
|
constructor(public id: string, public createTime: string,) {
|
|
this.lastState = 'SUBMITTED';
|
|
this.lastUpdate = this.createTime;
|
|
}
|
|
}
|
|
|
|
export class RegHelper {
|
|
private registry: Registry;
|
|
private cache = new Map<string, any>();
|
|
|
|
constructor() {
|
|
this.registry = getRegistry();
|
|
}
|
|
|
|
private cacheKey(keyObj: any): any | null {
|
|
return stringify(keyObj);
|
|
}
|
|
|
|
async resolveName(name: string) {
|
|
if (this.cache.has(name)) {
|
|
return this.cache.get(name);
|
|
} else {
|
|
const records = await this.registry.resolveNames([name]);
|
|
for (const r of records) {
|
|
// r can be null
|
|
if (r) {
|
|
this.cache.set(r.id, r);
|
|
if (Array.isArray(r.names)) {
|
|
for (const n of r.names) {
|
|
this.cache.set(n, r);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return records[0];
|
|
}
|
|
}
|
|
|
|
async getRecordById(id: string) {
|
|
if (this.cache.has(id)) {
|
|
return this.cache.get(id);
|
|
} else {
|
|
const records = await this.registry.getRecordsByIds([id]);
|
|
for (const r of records) {
|
|
this.cache.set(r.id, r);
|
|
if (Array.isArray(r.names)) {
|
|
for (const n of r.names) {
|
|
this.cache.set(n, r);
|
|
}
|
|
}
|
|
}
|
|
return records[0];
|
|
}
|
|
}
|
|
|
|
async getRecord(idOrName: string) {
|
|
if (!idOrName) {
|
|
return null;
|
|
}
|
|
if (idOrName.startsWith('lrn:')) {
|
|
return this.resolveName(idOrName);
|
|
}
|
|
return this.getRecordById(idOrName);
|
|
}
|
|
|
|
async queryRecords(query, all = true) {
|
|
const cacheKey = this.cacheKey({query, all});
|
|
let records;
|
|
if (this.cache.has(cacheKey)) {
|
|
records = this.cache.get(cacheKey);
|
|
} else {
|
|
records = await this.registry.queryRecords(query, all);
|
|
this.cache.set(cacheKey, records);
|
|
for (const r of records) {
|
|
this.cache.set(r.id, r);
|
|
}
|
|
}
|
|
|
|
return [...records];
|
|
}
|
|
|
|
async deploymentRequestStatus(requestId?: string) {
|
|
const requests: any[] = [];
|
|
const deployments: any[] = [];
|
|
const removalRequests = await this.queryRecords({
|
|
type: 'ApplicationDeploymentRemovalRequest',
|
|
});
|
|
|
|
if (requestId) {
|
|
const request = await this.getRecordById(requestId);
|
|
if (request) {
|
|
requests.push(request);
|
|
}
|
|
deployments.push(...await this.queryRecords({
|
|
type: 'ApplicationDeploymentRecord', request: requestId
|
|
}));
|
|
} else {
|
|
requests.push(...await this.queryRecords({
|
|
type: 'ApplicationDeploymentRequest',
|
|
}));
|
|
deployments.push(...await this.queryRecords({
|
|
type: 'ApplicationDeploymentRecord',
|
|
}));
|
|
}
|
|
|
|
requests.sort((a, b) => a.createTime === b.createTime ? 0 : a.createTime > b.createTime ? 1 : -1,);
|
|
requests.reverse();
|
|
|
|
const deploymentsByRequest = new Map<string, any>();
|
|
for (const d of deployments) {
|
|
deploymentsByRequest.set(d.attributes.request, d);
|
|
}
|
|
const removalsByRequest = new Map<string, any>();
|
|
for (const rr of removalRequests) {
|
|
if (rr.attributes.request) {
|
|
removalsByRequest.set(rr.attributes.request, rr);
|
|
}
|
|
}
|
|
|
|
const latestByHostname = new Map<string, any>();
|
|
|
|
const ret = [];
|
|
for (const r of requests) {
|
|
const status = new RequestStatus(r.id, r.createTime);
|
|
ret.push(status);
|
|
|
|
const app = await this.getRecord(r.attributes.application);
|
|
if (!app) {
|
|
status.lastState = 'ERROR';
|
|
continue;
|
|
}
|
|
|
|
status.app = r.attributes.application;
|
|
|
|
const dnsList: string[] = r.attributes.dns ? r.attributes.dns.split(",") : [];
|
|
const hostname = dnsList.length ? dnsList[dnsList.length - 1] : generateHostnameForApp(app);
|
|
|
|
if (deploymentsByRequest.has(r.id)) {
|
|
const deployment = deploymentsByRequest.get(r.id);
|
|
status.url = deployment.attributes.url;
|
|
status.lastUpdate = deployment.createTime;
|
|
|
|
if (!latestByHostname.has(hostname)) {
|
|
latestByHostname.set(hostname, status);
|
|
}
|
|
status.deployment = deployment.names ? deployment.names[0] : null;
|
|
if (status.deployment) {
|
|
status.lastState = 'DEPLOYED';
|
|
} else {
|
|
status.lastState = 'REMOVED';
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (removalsByRequest.has(r.id)) {
|
|
status.lastState = 'CANCELLED';
|
|
continue;
|
|
}
|
|
|
|
if (latestByHostname.has(hostname)) {
|
|
status.lastState = 'CANCELLED';
|
|
continue;
|
|
}
|
|
|
|
latestByHostname.set(hostname, status);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
async deployments() {
|
|
return this.queryRecords({type: 'ApplicationDeploymentRecord'}, false);
|
|
}
|
|
}
|