From 5160a3f692964731cf70124e3ad98071bf56dfd3 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Thu, 15 Feb 2024 13:55:48 -0600 Subject: [PATCH] Add log API --- __tests__/main.test.ts | 6 ++--- src/config.ts | 27 +++++++++++-------- src/deployments.ts | 51 +++++++++++++++++++---------------- src/main.ts | 61 +++++++++++++++++++++++++++++++++--------- 4 files changed, 95 insertions(+), 50 deletions(-) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index de5d0ec..2035ad7 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -15,10 +15,8 @@ describe('greeter function', () => { }); // Assert if setTimeout was called properly - it('delays the greeting by 2 seconds', () => { - }); + it('delays the greeting by 2 seconds', () => {}); // Assert greeter result - it('greets a user with `Hello, {name}` message', () => { - }); + it('greets a user with `Hello, {name}` message', () => {}); }); diff --git a/src/config.ts b/src/config.ts index 4218556..22125fd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,11 @@ -import yaml from 'js-yaml' -import fs from 'fs' -import path from 'path' -import {Registry} from "@cerc-io/laconic-sdk"; +import yaml from 'js-yaml'; +import fs from 'fs'; +import path from 'path'; +import {Registry} from '@cerc-io/laconic-sdk'; const loadConfigFile = (configFilePath: string): any => { 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); }; @@ -13,14 +13,19 @@ export const Config = { LISTEN_PORT: parseInt(process.env.LISTEN_PORT || '9555'), LISTEN_ADDR: process.env.LISTEN_ADDR || '0.0.0.0', 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 => { const laconicConfig = loadConfigFile(Config.LACONIC_CONFIG); //TODO(telackey): Use a pool. - return new Registry(laconicConfig.services?.cns?.gqlEndpoint, - laconicConfig.services?.cns?.restEndpoint, - laconicConfig.services?.cns?.chainId); -} + return new Registry( + laconicConfig.services?.cns?.gqlEndpoint, + laconicConfig.services?.cns?.restEndpoint, + laconicConfig.services?.cns?.chainId, + ); +}; diff --git a/src/deployments.ts b/src/deployments.ts index 799f025..dd0b3f2 100644 --- a/src/deployments.ts +++ b/src/deployments.ts @@ -1,24 +1,23 @@ -import {getRegistry} from "./config.js"; -import {Registry} from "@cerc-io/laconic-sdk"; +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(); +function generateHostnameForApp(app) { + const lastPart = app.attributes.name.split('/').pop(); const hasher = createHash('sha256'); hasher.update(app.attributes.name); - hasher.update("|") + 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)}`; + 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 { public app?: any; @@ -26,8 +25,9 @@ export class RequestStatus { public url?: string; public deployment?: 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; } } @@ -82,7 +82,7 @@ export class RegHelper { if (!idOrName) { return null; } - if (idOrName.startsWith("crn:")) { + if (idOrName.startsWith('crn:')) { return this.resolveName(idOrName); } return this.getRecordById(idOrName); @@ -105,11 +105,17 @@ export class RegHelper { } async deploymentRequestStatus() { - const requests = await this.queryRecords({type: "ApplicationDeploymentRequest"}); - const deployments = await this.queryRecords({type: "ApplicationDeploymentRecord"}); - const removalRequests = await this.queryRecords({type: "ApplicationDeploymentRemovalRequest"}); + const requests = await this.queryRecords({ + type: 'ApplicationDeploymentRequest', + }); + 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(); const deploymentsByRequest = new Map(); @@ -123,7 +129,6 @@ export class RegHelper { } } - const latestByHostname = new Map(); const ret = []; @@ -137,33 +142,33 @@ export class RegHelper { const deployment = deploymentsByRequest.get(r.id); status.url = deployment.attributes.url; 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)) { latestByHostname.set(shortHost, status); } status.deployment = deployment.names ? deployment.names[0] : null; if (status.deployment) { - status.lastState = "DEPLOYED"; + status.lastState = 'DEPLOYED'; } else { - status.lastState = "REMOVED"; + status.lastState = 'REMOVED'; } continue; } if (removalsByRequest.has(r.id)) { - status.lastState = "CANCELLED"; + status.lastState = 'CANCELLED'; continue; } const app = await this.getRecord(r.attributes.application); if (!app) { - status.lastState = "ERROR"; + status.lastState = 'ERROR'; continue; } const shortHost = r.attributes.dns ?? generateHostnameForApp(app); if (latestByHostname.has(shortHost)) { - status.lastState = "CANCELLED"; + status.lastState = 'CANCELLED'; continue; } @@ -174,6 +179,6 @@ export class RegHelper { } async deployments() { - return this.queryRecords({type: "ApplicationDeploymentRecord"}, false); + return this.queryRecords({type: 'ApplicationDeploymentRecord'}, false); } } diff --git a/src/main.ts b/src/main.ts index e74043a..844c015 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,28 +1,55 @@ 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(); app.use(express.json()); -app.use(function(_req, res, next) { - res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); +app.use(function (_req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept',); 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) => { const reg = new RegHelper(); - const regStatus= await reg.deploymentRequestStatus(); + const regStatus = await reg.deploymentRequestStatus(); + let deployerState = {}; if (Config.DEPLOYER_STATE && existsSync(Config.DEPLOYER_STATE)) { - const 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") { - console.log(deployerState[r.id]) - r.lastState = deployerState[r.id].status; + deployerState = JSON.parse(readFileSync(Config.DEPLOYER_STATE).toString()); + } + for (const r of regStatus) { + r.logAvailable = null != logForRequest(r.id); + // 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); }); +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, () => { console.log(`listening on ${Config.LISTEN_ADDR}:${Config.LISTEN_PORT}`); });