Add log API
This commit is contained in:
parent
1168a00d33
commit
5160a3f692
@ -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', () => {});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -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?.gqlEndpoint,
|
||||||
laconicConfig.services?.cns?.restEndpoint,
|
laconicConfig.services?.cns?.restEndpoint,
|
||||||
laconicConfig.services?.cns?.chainId);
|
laconicConfig.services?.cns?.chainId,
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
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 {
|
||||||
@ -18,7 +17,7 @@ function generateHostnameForApp(app){
|
|||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
src/main.ts
53
src/main.ts
@ -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) {
|
for (const r of regStatus) {
|
||||||
if (deployerState[r.id] && deployerState[r.id].status && deployerState[r.id].status != "SEEN") {
|
r.logAvailable = null != logForRequest(r.id);
|
||||||
console.log(deployerState[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;
|
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}`);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user