webapp-deployment-status-api/src/deployments.ts

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);
}
}