Skip to content
Closed

Dev #23

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
82fac91
refactor: rename onclose in RunHandler
ZorTik Mar 15, 2026
dfc696a
TODO.txt
ZorTik Mar 15, 2026
f49022c
TODO.txt
ZorTik Mar 15, 2026
28b8837
TODO.txt
ZorTik Mar 15, 2026
e95bf19
refactor: remove import
ZorTik Mar 15, 2026
042c081
TODO.txt
ZorTik Mar 16, 2026
8b0f535
refactor: await state message calls
ZorTik Mar 16, 2026
41bc109
refactor: comments
ZorTik Mar 16, 2026
fcb7a3c
refactor: remove async from functions where is not needed
ZorTik Mar 16, 2026
f3d83fb
refactor: remove comment
ZorTik Mar 16, 2026
a7f7eb1
refactor: TODO.txt
ZorTik Mar 16, 2026
86aa27c
refactor
ZorTik Mar 16, 2026
b8f6e1f
jsdoc
ZorTik Mar 16, 2026
d4f475f
jsdoc
ZorTik Mar 16, 2026
89ef6e8
refactor
ZorTik Mar 16, 2026
3279188
refactor: remove no-template mode
ZorTik Mar 16, 2026
d35b8b5
fix: bad reference
ZorTik Mar 16, 2026
d1ee07e
feat: remove session info from db
ZorTik Mar 16, 2026
39477f2
feat: move some of the label responsibility to service manager from e…
ZorTik Mar 18, 2026
bbb889f
fix: implement label listing for container
ZorTik Mar 18, 2026
fefdb03
fix: tests
ZorTik Mar 18, 2026
703dee4
feat: stack trace
ZorTik Mar 18, 2026
4d64584
feat: fixes
ZorTik Mar 18, 2026
b0a3f43
feat: refactor engine name display
ZorTik Mar 18, 2026
099f714
feat: big update
ZorTik Mar 20, 2026
98481ae
feat: small compilation fixes
ZorTik Mar 20, 2026
6989fa8
feat: fixes and refactorings, remove template lookup from service loo…
ZorTik Mar 20, 2026
c297991
feat: improve log record timestamps & todos
ZorTik Mar 20, 2026
8060b27
feat: service sessions ep
ZorTik Mar 20, 2026
80fe5e0
feat: service state
ZorTik Mar 20, 2026
2db42cc
refactor
ZorTik Mar 20, 2026
b1bde65
todo
ZorTik Mar 20, 2026
1d9ccfd
feat: session logs ep
ZorTik Mar 20, 2026
26613a5
todo
ZorTik Mar 20, 2026
131cd60
todo
ZorTik Mar 20, 2026
fe649da
feat: change log level for base dir watcher
ZorTik Mar 20, 2026
67d6aac
refactor
ZorTik Mar 21, 2026
9acf098
refactor: move resources to data folder
ZorTik Mar 21, 2026
39e7d9e
todo
ZorTik Mar 21, 2026
6576a7e
feat: service logs ep
ZorTik Mar 21, 2026
b3ea56d
todo
ZorTik Mar 21, 2026
c165a27
fix: handle engine power errors at the top layer
ZorTik Mar 21, 2026
ad52fd8
todo
ZorTik Mar 21, 2026
a4af0bf
todo
ZorTik Mar 21, 2026
83f6e34
refactor
ZorTik Mar 21, 2026
e75b8e9
feat: reworked configuration handling
ZorTik Mar 21, 2026
3ec24b8
feat: configuration rework, resources_path config option
ZorTik Mar 21, 2026
71b09f2
fixes
ZorTik Mar 31, 2026
9233791
refactor: change node version in Dockerfile
ZorTik Mar 31, 2026
ff382b0
feat: defaultResourcesTargetPath
ZorTik Apr 17, 2026
a2c8588
Merge remote-tracking branch 'origin/refactor/v1.1.2' into refactor/v…
ZorTik Apr 17, 2026
6a44c30
Merge pull request #22 from ZorTik/refactor/v1.1.2
ZorTik Apr 17, 2026
943d77b
fix: tests
ZorTik Apr 18, 2026
0dc61f6
fix: port parsing when inserted through env
ZorTik Apr 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18
FROM node:22

WORKDIR /data

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ NSM is a robust service manager built on Docker Engine. Its primary purpose is t

- **Dynamic Service Generation**: Create and manage services on-the-fly using RESTful APIs.
- **Template-Based Configuration**: Use customizable templates to define service configurations.
- **No-template mode**: NSM supports integrating custom engine with no template mode to disable templates completely.
- **Docker Integration**: Leverage Docker Engine for reliable and scalable service management.
- **Resources usage management**: NSM provides ability to limit or extend resources limits and view current usage.

Expand Down
10 changes: 10 additions & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
fixnout stop flow pomocí stop cmd
přidat created at a last started at do service info
abstrahovat template management a udělat další sources jako například používání images z docker registry
předělat usages monitoring na nějaký standardizovaný formát/způsob
pořešit /resources/config.yml aby měl víc možností a možná byl uložený jinde v systému podle platformy
port retrieval strategy pro spouštění servis, aby nebyla jenom random ale i jiné strategies
udělat nějaký auto packaging do balíčků na githubu jako bundle aby se dal spustit jako systémový proces v systemctl například
restart policy
předělat addons systém
flagy servis, každý flag bude mít jiný účel a půjdou přidávat přes addony které je budou taky managovat
6 changes: 3 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ const tsconfig = require("./tsconfig.json")
const moduleNameMapper = require("tsconfig-paths-jest")(tsconfig)

module.exports = {
"modulePathIgnorePatterns": [
"<rootDir>/tests/"
],
moduleNameMapper,
transformIgnorePatterns: [
"/node_modules/(?!(env-paths)/)",
],
reporters: [
'default',
['jest-ctrf-json-reporter', {}],
Expand Down
29 changes: 21 additions & 8 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@ components:
type: "object"
description: "The session information about a running service, if running"
properties:
serviceId:
type: "string"
nodeId:
type: "string"
containerId:
id:
type: "string"
description: "The currently active session ID"
startedAt:
type: "number"
format: "int64"
description: "The timestamp when the session started, in milliseconds since epoch"
stats:
type: "object"
properties:
Expand Down Expand Up @@ -136,8 +137,16 @@ components:
id:
type: "string"
description: "The service UID in the system"
template:
$ref: "#/components/schemas/TemplateLookup"
templateId:
type: "string"
description: "The template ID used to create the service"
state:
type: string
description: "The current state of the service. One of: 'RUNNING', 'BUILDING', 'STOPPED'."
enum:
- "RUNNING"
- "BUILDING"
- "STOPPED"
port:
type: "integer"
format: "int32"
Expand Down Expand Up @@ -537,4 +546,8 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Result"
$ref: "#/components/schemas/Result"

# TODO: /v1/service/{serviceId}/sessions
# TODO: /v1/service/{serviceId}/logs
# TODO: /v1/session/{sessionId}/logs
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"build": "node installTempDeps.js && tsc && tscp",
"start": "cross-env TS_NODE_BASEURL=./dist node -r tsconfig-paths/register dist/index.js",
"test": "npm run build && cross-env TS_NODE_BASEURL=./dist jest"
"test": "jest"
},
"keywords": [],
"author": "ZorTik",
Expand Down Expand Up @@ -54,6 +54,7 @@
"crypto": "^1.0.1",
"dockerode": "^4.0.2",
"dotenv": "^16.4.5",
"env-paths": "^3.0.0",
"express": "^4.18.2",
"express-fileupload": "^1.5.0",
"folder-hash": "^4.1.1",
Expand Down
8 changes: 8 additions & 0 deletions prisma/migrations/20260316232423_remove_session/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.

*/
-- DropTable
DROP TABLE `Session`;
24 changes: 24 additions & 0 deletions prisma/migrations/20260319213658_service_session/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- CreateTable
CREATE TABLE `ServiceSession` (
`id` VARCHAR(191) NOT NULL,
`serviceId` VARCHAR(191) NOT NULL,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `ServiceLogRecord` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`sessionId` VARCHAR(191) NOT NULL,
`timestamp` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`logLevel` VARCHAR(191) NOT NULL,
`message` VARCHAR(191) NOT NULL,

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `ServiceSession` ADD CONSTRAINT `ServiceSession_serviceId_fkey` FOREIGN KEY (`serviceId`) REFERENCES `Service`(`serviceId`) ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `ServiceLogRecord` ADD CONSTRAINT `ServiceLogRecord_sessionId_fkey` FOREIGN KEY (`sessionId`) REFERENCES `ServiceSession`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- Added the required column `source` to the `ServiceLogRecord` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE `ServiceLogRecord` ADD COLUMN `source` ENUM('ENGINE', 'CONTAINER') NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `ServiceLogRecord` MODIFY `message` LONGTEXT NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `ServiceSession` ADD COLUMN `startedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);
38 changes: 27 additions & 11 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,27 @@ generator client {
}

datasource db {
provider = "mysql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
provider = "mysql"
url = env("DATABASE_URL")
}

model Session {
serviceId String @id @default(uuid())
createdAt DateTime @default(now())
nodeId String
containerId String
enum ServiceLogSource {
ENGINE
CONTAINER
}

model Service {
serviceId String @id @default(uuid())
serviceId String @id @default(uuid())
nodeId String
imageId String?
template String
port Int
options Json
meta Json @default("{}")
meta Json @default("{}")
env Json
network Json?
image Image? @relation(fields: [imageId], references: [id])
image Image? @relation(fields: [imageId], references: [id])
sessions ServiceSession[]
}

model ServiceMeta {
Expand All @@ -40,6 +38,24 @@ model ServiceMeta {
value Json
}

model ServiceSession {
id String @id @default(uuid())
serviceId String
service Service @relation(fields: [serviceId], references: [serviceId], onDelete: Cascade)
logs ServiceLogRecord[]
startedAt DateTime @default(now())
}

model ServiceLogRecord {
id BigInt @id @default(autoincrement())
sessionId String
timestamp DateTime @default(now())
source ServiceLogSource
logLevel String
message String @db.LongText
session ServiceSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
}

// Persistent key-value storage for system-wide attributes.
model Meta {
id Int @id @default(autoincrement())
Expand Down
4 changes: 3 additions & 1 deletion resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ port: 3000
# Supported types: 'none', 'auth_token'
auth: 'none'
docker_host: 'unix:///var/run/docker.sock'
service_logs: true
# Override resources path if needed.
# By default, an explicit system-specific data path is used.
# resources_path: '/srv/resources'
69 changes: 38 additions & 31 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
import dotenv from "dotenv";
import config from "@nsm/configuration/appConfig";
import {loadAppConfig} from "@nsm/config";
import {init as initFileStructure, getResourcesTargetPath, prepareFolders} from "@nsm/filestructure";

// Load .env
dotenv.config();
// Preload app config here to set needed env variables
// before some modules require them.
config();
const appConfig = loadAppConfig();
initFileStructure(appConfig);

import {Router} from 'express';
import {Database} from "@nsm/database";
import {ServiceManager} from "@nsm/engine";
import loadAddons from "./addon";
import loadAppRoutes from '@nsm/router';
import createDbManager from '@nsm/database';
import initServiceManager from '@nsm/engine';
import loadSecurity from "@nsm/security";
import * as r from "@nsm/configuration/resources";
import * as manager from "@nsm/engine";
import * as manager from "@nsm/engine/manager";
import * as sessionManager from "@nsm/engine/session";
import * as logging from "./logger";
import winston from "winston";
import {Application} from "express-ws";
import fs from "fs";
import isInsideContainer from "@nsm/lib/isInsideContainer";
import {middleLayer} from "@nsm/engine/middle";
import {SessionManager} from "@nsm/engine/session";
import {mkdirResource, saveResource} from "@nsm/resources";
import path from "path";
import {AppConfig} from "@nsm/config";

export type AppBootContext = AppContext & { steps: any };

// Passed context to the routes
export type AppContext = {
router: Router;
manager: ServiceManager;
sessionManager: SessionManager;
database: Database;
appConfig: any;
appConfig: AppConfig;
logger: winston.Logger;
debug: boolean;
workers: boolean;
Expand All @@ -42,20 +48,11 @@ export type AppBootOptions = {
disableWorkers?: boolean;
}

let currentContext: AppContext;

function prepareServiceLogs(appConfig: any, logger: winston.Logger) {
if (appConfig.service_logs === true) {
logger.info('Service logs are enabled');
const path = process.cwd() + '/service_logs';
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
}
}
export let currentContext: AppContext;

function initGlobalLogger() {
logging.createNewLatest();
logging.createLatestLogFile();

return logging.createLogger();
}

Expand All @@ -79,23 +76,28 @@ function managerForUnsafeUse() {
return new Proxy(manager, handler);
}

// App orchestration code
/**
* App orchestration code.
*
* @param router The app router.
* @param options The optional boot options.
*/
export const init = async (router: Application, options?: AppBootOptions): Promise<AppBootContext> => {
// Prepare logging
const logger = initGlobalLogger();

r.prepareTemplatesFolder();
prepareFolders();

// Prepare templates folder
mkdirResource("templates");
if (options?.test === true) {
r.prepareTestResources(); // Copy resources for test
prepareTestResources(); // Copy resources for test
}

// Load addon steps
const steps = await loadAddons(logger);

steps('BEFORE_CONFIG', { logger });
const appConfig = config();

prepareServiceLogs(appConfig, logger);

// Database connection layer
steps('BEFORE_DB', { logger, appConfig });
Expand All @@ -105,6 +107,7 @@ export const init = async (router: Application, options?: AppBootOptions): Promi
const ctx = currentContext = {
router,
manager: managerForUnsafeUse(),
sessionManager,
database,
appConfig,
logger,
Expand All @@ -114,7 +117,7 @@ export const init = async (router: Application, options?: AppBootOptions): Promi

// Service (virtualization) layer
steps('BEFORE_ENGINE', ctx);
await initServiceManager({ db: database, appConfig, logger });
await manager.init(database, appConfig, logger);

// Bring back original manager
ctx.manager = currentContext.manager = middleLayer(manager);
Expand All @@ -139,16 +142,20 @@ export const init = async (router: Application, options?: AppBootOptions): Promi
let srv = undefined;
if (options?.test == undefined || options.test == false) {
logger.info(`Starting server`);
srv = router.listen(appConfig.port, () => {
logger.info(`Server started on port ${appConfig.port}`);
srv = router.listen(appConfig.getPort(), () => {
logger.info(`Server started on port ${appConfig.getPort()}`);
});
}
steps('BOOT', ctx, srv);
return { ...ctx, steps };
}

export {
Database,
ServiceManager,
currentContext
const prepareTestResources = () => {
if (fs.existsSync(path.join(getResourcesTargetPath(), 'templates', 'test'))) {
return;
}

saveResource('template/test/test_settings.yml', 'templates/test/settings.yml')
saveResource('template/test/test_dockerfile', 'templates/test/Dockerfile')
saveResource('template/test/test_nsmignore', 'templates/test/.nsmignore')
}
Loading
Loading