Add log API

This commit is contained in:
Thomas E Lackey 2024-02-15 13:55:48 -06:00
parent 1168a00d33
commit 5160a3f692
4 changed files with 95 additions and 50 deletions

View File

@ -15,10 +15,8 @@ describe('greeter function', () => {
}); });
// Assert if setTimeout was called properly // Assert if setTimeout was called properly
it('delays the greeting by 2 seconds', () => { it('delays the greeting by 2 seconds', () => {});
});
// Assert greeter result // Assert greeter result
it('greets a user with `Hello, {name}` message', () => { it('greets a user with `Hello, {name}` message', () => {});
});
}); });

View File

@ -1,11 +1,11 @@
import yaml from 'js-yaml' import yaml from 'js-yaml';
import fs from 'fs' import fs from 'fs';
import path from 'path' import path from 'path';
import {Registry} from "@cerc-io/laconic-sdk"; import {Registry} from '@cerc-io/laconic-sdk';
const loadConfigFile = (configFilePath: string): any => { const loadConfigFile = (configFilePath: string): any => {
const resolvedFilePath = path.resolve(process.cwd(), configFilePath); const resolvedFilePath = path.resolve(process.cwd(), configFilePath);
const configFile = fs.readFileSync(resolvedFilePath, 'utf-8') const configFile = fs.readFileSync(resolvedFilePath, 'utf-8');
return yaml.load(configFile); return yaml.load(configFile);
}; };
@ -13,14 +13,19 @@ export const Config = {
LISTEN_PORT: parseInt(process.env.LISTEN_PORT || '9555'), LISTEN_PORT: parseInt(process.env.LISTEN_PORT || '9555'),
LISTEN_ADDR: process.env.LISTEN_ADDR || '0.0.0.0', LISTEN_ADDR: process.env.LISTEN_ADDR || '0.0.0.0',
LACONIC_CONFIG: process.env.LACONIC_CONFIG || '/etc/config/laconic.yml', LACONIC_CONFIG: process.env.LACONIC_CONFIG || '/etc/config/laconic.yml',
DEPLOYER_STATE: process.env.DEPLOYER_STATE || '/srv/deployments/autodeploy.state' DEPLOYER_STATE:
process.env.DEPLOYER_STATE || '/srv/deployments/autodeploy.state',
UNDEPLOYER_STATE:
process.env.UNDEPLOYER_STATE || '/srv/deployments/autoundeploy.state',
BUILD_LOGS: process.env.BUILD_LOGS || '/srv/logs',
}; };
export const getRegistry = (): Registry => { export const getRegistry = (): Registry => {
const laconicConfig = loadConfigFile(Config.LACONIC_CONFIG); const laconicConfig = loadConfigFile(Config.LACONIC_CONFIG);
//TODO(telackey): Use a pool. //TODO(telackey): Use a pool.
return new Registry(laconicConfig.services?.cns?.gqlEndpoint, return new Registry(
laconicConfig.services?.cns?.restEndpoint, laconicConfig.services?.cns?.gqlEndpoint,
laconicConfig.services?.cns?.chainId); laconicConfig.services?.cns?.restEndpoint,
} laconicConfig.services?.cns?.chainId,
);
};

View File

@ -1,24 +1,23 @@
import {getRegistry} from "./config.js"; import {getRegistry} from './config.js';
import {Registry} from "@cerc-io/laconic-sdk"; import {Registry} from '@cerc-io/laconic-sdk';
import stringify from 'json-stable-stringify'; import stringify from 'json-stable-stringify';
import {createHash} from 'crypto'; import {createHash} from 'crypto';
function generateHostnameForApp(app) {
function generateHostnameForApp(app){ const lastPart = app.attributes.name.split('/').pop();
const lastPart = app.attributes.name.split("/").pop();
const hasher = createHash('sha256'); const hasher = createHash('sha256');
hasher.update(app.attributes.name); hasher.update(app.attributes.name);
hasher.update("|") hasher.update('|');
if (Array.isArray(app.attributes.repository)) { if (Array.isArray(app.attributes.repository)) {
hasher.update(app.attributes.repository[0]); hasher.update(app.attributes.repository[0]);
} else { } else {
hasher.update(app.attributes.repository); hasher.update(app.attributes.repository);
} }
return `${lastPart}-${hasher.digest('hex').slice(0,10)}`; return `${lastPart}-${hasher.digest('hex').slice(0, 10)}`;
} }
export type RequestState = "SUBMITTED" | "DEPLOYING" | "DEPLOYED" | "REMOVED" | "CANCELLED" | "ERROR"; export type RequestState = | 'SUBMITTED' | 'DEPLOYING' | 'DEPLOYED' | 'REMOVED' | 'CANCELLED' | 'ERROR';
export class RequestStatus { export class RequestStatus {
public app?: any; public app?: any;
@ -26,8 +25,9 @@ export class RequestStatus {
public url?: string; public url?: string;
public deployment?: string; public deployment?: string;
public lastUpdate?: string; public lastUpdate?: string;
constructor(public id: string, public createTime: string) {
this.lastState = "SUBMITTED"; constructor(public id: string, public createTime: string,) {
this.lastState = 'SUBMITTED';
this.lastUpdate = this.createTime; this.lastUpdate = this.createTime;
} }
} }
@ -82,7 +82,7 @@ export class RegHelper {
if (!idOrName) { if (!idOrName) {
return null; return null;
} }
if (idOrName.startsWith("crn:")) { if (idOrName.startsWith('crn:')) {
return this.resolveName(idOrName); return this.resolveName(idOrName);
} }
return this.getRecordById(idOrName); return this.getRecordById(idOrName);
@ -105,11 +105,17 @@ export class RegHelper {
} }
async deploymentRequestStatus() { async deploymentRequestStatus() {
const requests = await this.queryRecords({type: "ApplicationDeploymentRequest"}); const requests = await this.queryRecords({
const deployments = await this.queryRecords({type: "ApplicationDeploymentRecord"}); type: 'ApplicationDeploymentRequest',
const removalRequests = await this.queryRecords({type: "ApplicationDeploymentRemovalRequest"}); });
const deployments = await this.queryRecords({
type: 'ApplicationDeploymentRecord',
});
const removalRequests = await this.queryRecords({
type: 'ApplicationDeploymentRemovalRequest',
});
requests.sort((a, b) => a.createTime === b.createTime ? 0 : a.createTime > b.createTime ? 1 : -1); requests.sort((a, b) => a.createTime === b.createTime ? 0 : a.createTime > b.createTime ? 1 : -1,);
requests.reverse(); requests.reverse();
const deploymentsByRequest = new Map<string, any>(); const deploymentsByRequest = new Map<string, any>();
@ -123,7 +129,6 @@ export class RegHelper {
} }
} }
const latestByHostname = new Map<string, any>(); const latestByHostname = new Map<string, any>();
const ret = []; const ret = [];
@ -137,33 +142,33 @@ export class RegHelper {
const deployment = deploymentsByRequest.get(r.id); const deployment = deploymentsByRequest.get(r.id);
status.url = deployment.attributes.url; status.url = deployment.attributes.url;
status.lastUpdate = deployment.createTime; status.lastUpdate = deployment.createTime;
const shortHost = new URL(status.url).host.split(".").shift(); const shortHost = new URL(status.url).host.split('.').shift();
if (!latestByHostname.has(shortHost)) { if (!latestByHostname.has(shortHost)) {
latestByHostname.set(shortHost, status); latestByHostname.set(shortHost, status);
} }
status.deployment = deployment.names ? deployment.names[0] : null; status.deployment = deployment.names ? deployment.names[0] : null;
if (status.deployment) { if (status.deployment) {
status.lastState = "DEPLOYED"; status.lastState = 'DEPLOYED';
} else { } else {
status.lastState = "REMOVED"; status.lastState = 'REMOVED';
} }
continue; continue;
} }
if (removalsByRequest.has(r.id)) { if (removalsByRequest.has(r.id)) {
status.lastState = "CANCELLED"; status.lastState = 'CANCELLED';
continue; continue;
} }
const app = await this.getRecord(r.attributes.application); const app = await this.getRecord(r.attributes.application);
if (!app) { if (!app) {
status.lastState = "ERROR"; status.lastState = 'ERROR';
continue; continue;
} }
const shortHost = r.attributes.dns ?? generateHostnameForApp(app); const shortHost = r.attributes.dns ?? generateHostnameForApp(app);
if (latestByHostname.has(shortHost)) { if (latestByHostname.has(shortHost)) {
status.lastState = "CANCELLED"; status.lastState = 'CANCELLED';
continue; continue;
} }
@ -174,6 +179,6 @@ export class RegHelper {
} }
async deployments() { async deployments() {
return this.queryRecords({type: "ApplicationDeploymentRecord"}, false); return this.queryRecords({type: 'ApplicationDeploymentRecord'}, false);
} }
} }

View File

@ -1,28 +1,55 @@
import express from 'express'; import express from 'express';
import { readFileSync, existsSync } from "fs"; import {existsSync, readdirSync, readFileSync} from 'fs';
import {Config, getRegistry} from './config.js' import {Config, getRegistry} from './config.js';
import {RegHelper} from './deployments.js' import {RegHelper} from './deployments.js';
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
app.use(function(_req, res, next) { app.use(function (_req, res, next) {
res.header("Access-Control-Allow-Origin", "*"); res.header('Access-Control-Allow-Origin', '*');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept',);
next(); next();
}); });
const logForRequest = (req_id) => {
let ret = null;
const logDir = `${Config.BUILD_LOGS}/${req_id}`;
if (existsSync(logDir)) {
const logFiles = readdirSync(logDir).sort();
if (logFiles.length) {
ret = `${logDir}/${logFiles.pop()}`;
}
}
return ret;
};
app.get('/', async (_req, res) => { app.get('/', async (_req, res) => {
const reg = new RegHelper(); const reg = new RegHelper();
const regStatus= await reg.deploymentRequestStatus(); const regStatus = await reg.deploymentRequestStatus();
let deployerState = {};
if (Config.DEPLOYER_STATE && existsSync(Config.DEPLOYER_STATE)) { if (Config.DEPLOYER_STATE && existsSync(Config.DEPLOYER_STATE)) {
const deployerState = JSON.parse(readFileSync(Config.DEPLOYER_STATE).toString()); deployerState = JSON.parse(readFileSync(Config.DEPLOYER_STATE).toString());
for (const r of regStatus) { }
if (deployerState[r.id] && deployerState[r.id].status && deployerState[r.id].status != "SEEN") { for (const r of regStatus) {
console.log(deployerState[r.id]) r.logAvailable = null != logForRequest(r.id);
r.lastState = deployerState[r.id].status; // If we know something more specific in the local state, use that ...
if (deployerState[r.id]) {
switch (deployerState[r.id].state) {
case 'DEPLOYED': {
r.lastState = deployerState[r.id].status;
break;
}
case 'DEPLOYING': {
r.lastState = deployerState[r.id].status;
break;
}
case 'ERROR': {
r.lastState = deployerState[r.id].status;
break;
}
} }
} }
} }
@ -35,6 +62,16 @@ app.get('/:id', async (req, res) => {
res.send(records); res.send(records);
}); });
app.get('/log/:id', async (req, res) => {
const logFile = logForRequest(req.params.id);
if (!logFile) {
res.sendStatus(404);
return;
}
res.send(readFileSync(logFile));
});
app.listen(Config.LISTEN_PORT, Config.LISTEN_ADDR, () => { app.listen(Config.LISTEN_PORT, Config.LISTEN_ADDR, () => {
console.log(`listening on ${Config.LISTEN_ADDR}:${Config.LISTEN_PORT}`); console.log(`listening on ${Config.LISTEN_ADDR}:${Config.LISTEN_PORT}`);
}); });