0.1.0-alpha
This commit is contained in:
commit
43e27eb713
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
yarn.lock
|
||||||
|
.env
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Bitstream (Docker Management Server)
|
||||||
|
|
||||||
|
This project provides a Node.js backend server for managing Docker containers and images, with real-time stats via WebSocket. It is built using Express, Dockerode, and supports CORS for frontend integration.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- List all Docker containers (running and stopped)
|
||||||
|
- Start, stop, restart, and delete containers
|
||||||
|
- Create and run new containers with custom options
|
||||||
|
- View container stats (CPU, memory, network, block I/O)
|
||||||
|
- View container logs
|
||||||
|
- List available Docker images
|
||||||
|
- Pull new images from Docker Hub
|
||||||
|
- Get Docker system info
|
||||||
|
- Real-time container stats updates via WebSocket
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Containers
|
||||||
|
- `GET /api/containers` — List all containers
|
||||||
|
- `GET /api/containers/:id/stats` — Get stats for a container
|
||||||
|
- `POST /api/containers/:id/start` — Start a container
|
||||||
|
- `POST /api/containers/:id/stop` — Stop a container
|
||||||
|
- `POST /api/containers/:id/restart` — Restart a container
|
||||||
|
- `POST /api/containers/create` — Create and start a new container
|
||||||
|
- `DELETE /api/containers/:id` — Delete a container
|
||||||
|
- `GET /api/containers/:id/logs` — Get logs for a container
|
||||||
|
|
||||||
|
### Images
|
||||||
|
- `GET /api/images` — List Docker images
|
||||||
|
- `POST /api/images/pull` — Pull a Docker image (body: `{ image: "<image-name>" }`)
|
||||||
|
|
||||||
|
### System
|
||||||
|
- `GET /api/system/info` — Get Docker system info
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
- Connect to the server via WebSocket (same port) to receive real-time container stats updates.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- [Node.js](https://nodejs.org/) (v22 recommended)
|
||||||
|
- [Docker](https://www.docker.com/) running locally
|
||||||
|
- On Windows: Docker must be configured to expose the API on TCP (see below)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```sh
|
||||||
|
git clone https://git.netbyte.host/netbyte/bitstream.git
|
||||||
|
cd bitstream
|
||||||
|
```
|
||||||
|
2. Install dependencies:
|
||||||
|
```sh
|
||||||
|
yarn
|
||||||
|
# or
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- **Linux/Mac:** Uses Docker socket at `/var/run/docker.sock` by default.
|
||||||
|
- **Windows:**
|
||||||
|
- Edit `server.js` and set Docker connection to `{ host: '127.0.0.1', port: 2375 }`.
|
||||||
|
- Ensure Docker is configured to expose the API on TCP port 2375 (insecure).
|
||||||
|
|
||||||
|
### Running the Server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on port `3001` by default (or set `PORT` environment variable).
|
||||||
|
|
||||||
|
### API Authentication (Secret Key)
|
||||||
|
|
||||||
|
Bitstream requires a secret API key for all requests. This prevents unauthorized access (like Pterodactyl's Wings).
|
||||||
|
|
||||||
|
#### Generating a Key
|
||||||
|
|
||||||
|
Run the following command to generate a new API secret:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run keygen
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the printed key and store it in a `.env` file at the root of your project:
|
||||||
|
|
||||||
|
```
|
||||||
|
BITSTREAM_API_SECRET=your-generated-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use `.env.example` as a template.
|
||||||
|
|
||||||
|
**All API requests must include the header:**
|
||||||
|
|
||||||
|
```
|
||||||
|
x-api-key: your-generated-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
If the key is missing or incorrect, the server will return `401 Unauthorized`.
|
||||||
|
|
||||||
|
## Example: Creating a Container
|
||||||
|
|
||||||
|
POST `/api/containers/create`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-nginx",
|
||||||
|
"image": "nginx:latest",
|
||||||
|
"ports": { "80/tcp": [{ "HostPort": "8080" }] },
|
||||||
|
"environment": ["ENV_VAR=value"],
|
||||||
|
"volumes": ["/host/path:/container/path"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-Time Stats
|
||||||
|
|
||||||
|
Connect to the WebSocket endpoint (same port) to receive periodic stats updates for all running containers.
|
||||||
|
|
||||||
|
## Running as a systemd Service
|
||||||
|
|
||||||
|
A sample systemd unit file is provided in `systemd/bitstream.service`. To use:
|
||||||
|
|
||||||
|
1. Copy the file to `/etc/systemd/system/bitstream.service` (edit paths as needed).
|
||||||
|
2. Set `WorkingDirectory` and `EnvironmentFile` to your Bitstream install location.
|
||||||
|
3. Create a dedicated user (e.g., `bitstream`) for security. (optional)
|
||||||
|
4. Reload systemd and start the service:
|
||||||
|
```sh
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now bitstream
|
||||||
|
sudo systemctl status bitstream
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debug Mode
|
||||||
|
|
||||||
|
You can run Bitstream in debug mode to log all API requests and see more detailed output:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node server.js --debug
|
||||||
|
# or
|
||||||
|
yarn dev --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
When debug mode is enabled, all API interactions (method, path, status, duration) are logged to the console.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// containerService.js
|
||||||
|
const docker = require('./dockerClient');
|
||||||
|
const calculateCPUPercent = require('../utils/calculateCPU');
|
||||||
|
const chalk = require('chalk').default;
|
||||||
|
|
||||||
|
async function listContainers() {
|
||||||
|
const containers = await docker.listContainers({ all: true });
|
||||||
|
return Promise.all(containers.map(async (containerInfo) => {
|
||||||
|
const container = docker.getContainer(containerInfo.Id);
|
||||||
|
const inspect = await container.inspect();
|
||||||
|
return {
|
||||||
|
id: containerInfo.Id,
|
||||||
|
name: containerInfo.Names[0].replace('/', ''),
|
||||||
|
image: containerInfo.Image,
|
||||||
|
status: containerInfo.State,
|
||||||
|
state: containerInfo.Status,
|
||||||
|
ports: containerInfo.Ports,
|
||||||
|
created: containerInfo.Created,
|
||||||
|
labels: containerInfo.Labels || {},
|
||||||
|
inspect: {
|
||||||
|
config: inspect.Config,
|
||||||
|
hostConfig: inspect.HostConfig,
|
||||||
|
networkSettings: inspect.NetworkSettings
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getContainerStats(id) {
|
||||||
|
const container = docker.getContainer(id);
|
||||||
|
const stats = await container.stats({ stream: false });
|
||||||
|
const cpuPercent = calculateCPUPercent(stats);
|
||||||
|
const memoryUsage = stats.memory_stats.usage || 0;
|
||||||
|
const memoryLimit = stats.memory_stats.limit || 0;
|
||||||
|
const memoryPercent = memoryLimit > 0 ? (memoryUsage / memoryLimit) * 100 : 0;
|
||||||
|
const networks = stats.networks || {};
|
||||||
|
const networkIO = Object.keys(networks).reduce((acc, key) => ({
|
||||||
|
rx_bytes: acc.rx_bytes + (networks[key].rx_bytes || 0),
|
||||||
|
tx_bytes: acc.tx_bytes + (networks[key].tx_bytes || 0)
|
||||||
|
}), { rx_bytes: 0, tx_bytes: 0 });
|
||||||
|
return {
|
||||||
|
cpu_percent: cpuPercent,
|
||||||
|
memory: { usage: memoryUsage, limit: memoryLimit, percent: memoryPercent },
|
||||||
|
network: networkIO,
|
||||||
|
block_io: stats.blkio_stats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startContainer(id) {
|
||||||
|
const container = docker.getContainer(id);
|
||||||
|
await container.start();
|
||||||
|
console.log(chalk.yellow('Container started:'), chalk.cyan(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopContainer(id) {
|
||||||
|
const container = docker.getContainer(id);
|
||||||
|
await container.stop();
|
||||||
|
console.log(chalk.yellow('Container stopped:'), chalk.cyan(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartContainer(id) {
|
||||||
|
const container = docker.getContainer(id);
|
||||||
|
await container.restart();
|
||||||
|
console.log(chalk.yellow('Container restarted:'), chalk.cyan(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createContainer(options) {
|
||||||
|
const container = await docker.createContainer(options);
|
||||||
|
await container.start();
|
||||||
|
console.log(chalk.green('Container created and started:'), chalk.cyan(container.id));
|
||||||
|
return container.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeContainer(id) {
|
||||||
|
const container = docker.getContainer(id);
|
||||||
|
try { await container.stop(); } catch (e) {}
|
||||||
|
await container.remove();
|
||||||
|
console.log(chalk.red('Container deleted:'), chalk.cyan(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getContainerLogs(id) {
|
||||||
|
const container = docker.getContainer(id);
|
||||||
|
const logs = await container.logs({
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
tail: 100,
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
return logs.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listContainers,
|
||||||
|
getContainerStats,
|
||||||
|
startContainer,
|
||||||
|
stopContainer,
|
||||||
|
restartContainer,
|
||||||
|
createContainer,
|
||||||
|
removeContainer,
|
||||||
|
getContainerLogs
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// dockerClient.js
|
||||||
|
const Docker = require('dockerode');
|
||||||
|
|
||||||
|
// For Linux/Mac: socketPath, for Windows: host/port
|
||||||
|
const docker = new Docker({
|
||||||
|
socketPath: '/var/run/docker.sock'
|
||||||
|
// For Windows: { host: '127.0.0.1', port: 2375 }
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = docker;
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// imageService.js
|
||||||
|
|
||||||
|
const docker = require('./dockerClient');
|
||||||
|
const chalk = require('chalk').default;
|
||||||
|
|
||||||
|
async function listImages() {
|
||||||
|
return docker.listImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pullImage(image) {
|
||||||
|
const stream = await docker.pull(image);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
docker.modem.followProgress(stream, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(chalk.red('Error pulling image:'), chalk.cyan(image), err.message);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
console.log(chalk.green('Image pulled successfully:'), chalk.cyan(image));
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listImages,
|
||||||
|
pullImage
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
// middleware/auth.js
|
||||||
|
module.exports = function (req, res, next) {
|
||||||
|
const apiKey = req.headers['x-api-key'];
|
||||||
|
if (apiKey && apiKey === process.env.BITSTREAM_API_SECRET) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// middleware/logApi.js
|
||||||
|
const chalk = require('chalk').default;
|
||||||
|
|
||||||
|
module.exports = function (req, res, next) {
|
||||||
|
if (global.DEBUG_MODE) {
|
||||||
|
const start = Date.now();
|
||||||
|
res.on('finish', () => {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
console.log(
|
||||||
|
chalk.magenta('[API]'),
|
||||||
|
chalk.cyan(req.method),
|
||||||
|
chalk.white(req.originalUrl),
|
||||||
|
chalk.yellow(res.statusCode),
|
||||||
|
chalk.gray(`${duration}ms`),
|
||||||
|
req.headers['x-api-key'] ? chalk.green('[x-api-key]') : ''
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "bitstream",
|
||||||
|
"version": "0.1.0-alpha",
|
||||||
|
"description": "Docker API built for Navigator",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"debug": "node server.js --debug",
|
||||||
|
"dev": "node server.js",
|
||||||
|
"keygen": "node scripts/keygen.js"
|
||||||
|
},
|
||||||
|
"repository": "https://git.netbyte.host/netbyte/bitstream",
|
||||||
|
"author": "Andreas Claesson <andreas@netbyte.host>",
|
||||||
|
"license": "Apache2.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dockerode": "^4.0.7",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"dotenv": "^17.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// containers.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const containerService = require('../docker/containerService');
|
||||||
|
|
||||||
|
// List all containers
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const containers = await containerService.listContainers();
|
||||||
|
res.json(containers);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch containers' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get container stats
|
||||||
|
router.get('/:id/stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stats = await containerService.getContainerStats(req.params.id);
|
||||||
|
res.json(stats);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch container stats' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start container
|
||||||
|
router.post('/:id/start', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await containerService.startContainer(req.params.id);
|
||||||
|
res.json({ message: 'Container started successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to start container' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop container
|
||||||
|
router.post('/:id/stop', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await containerService.stopContainer(req.params.id);
|
||||||
|
res.json({ message: 'Container stopped successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to stop container' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart container
|
||||||
|
router.post('/:id/restart', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await containerService.restartContainer(req.params.id);
|
||||||
|
res.json({ message: 'Container restarted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to restart container' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and run new container
|
||||||
|
router.post('/create', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, image, ports = {}, environment = [], volumes = [], restart = 'unless-stopped' } = req.body;
|
||||||
|
const createOptions = {
|
||||||
|
Image: image,
|
||||||
|
name: name,
|
||||||
|
Env: environment,
|
||||||
|
HostConfig: {
|
||||||
|
PortBindings: ports,
|
||||||
|
Binds: volumes,
|
||||||
|
RestartPolicy: { Name: restart }
|
||||||
|
},
|
||||||
|
ExposedPorts: Object.keys(ports).reduce((acc, port) => {
|
||||||
|
acc[port] = {};
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
};
|
||||||
|
const id = await containerService.createContainer(createOptions);
|
||||||
|
res.json({ id, message: 'Container created and started successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to create container' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete container
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await containerService.removeContainer(req.params.id);
|
||||||
|
res.json({ message: 'Container deleted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to delete container' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get container logs
|
||||||
|
router.get('/:id/logs', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const logs = await containerService.getContainerLogs(req.params.id);
|
||||||
|
res.json({ logs });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch logs' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// images.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const imageService = require('../docker/imageService');
|
||||||
|
|
||||||
|
// List images
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const images = await imageService.listImages();
|
||||||
|
res.json(images);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch images' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pull image
|
||||||
|
router.post('/pull', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { image } = req.body;
|
||||||
|
await imageService.pullImage(image);
|
||||||
|
res.json({ message: 'Image pull started' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to pull image' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// system.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const docker = require('../docker/dockerClient');
|
||||||
|
|
||||||
|
// Get Docker system info
|
||||||
|
router.get('/info', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const info = await docker.info();
|
||||||
|
res.json(info);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to fetch system info' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// scripts/keygen.js
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const envPath = path.join(__dirname, '../.env');
|
||||||
|
const force = process.argv.includes('--force');
|
||||||
|
|
||||||
|
let envContent = '';
|
||||||
|
let exists = false;
|
||||||
|
let alreadySet = false;
|
||||||
|
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
exists = true;
|
||||||
|
alreadySet = /^BITSTREAM_API_SECRET=.+/m.test(envContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alreadySet && !force) {
|
||||||
|
console.warn('BITSTREAM_API_SECRET already exists in .env!');
|
||||||
|
console.warn('If you want to overwrite it, run: npm run keygen --force / yarn keygen --force');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
let newEnvContent = '';
|
||||||
|
if (exists) {
|
||||||
|
// Remove any existing BITSTREAM_API_SECRET line
|
||||||
|
newEnvContent = envContent.replace(/^BITSTREAM_API_SECRET=.*$/m, '').trim();
|
||||||
|
if (newEnvContent.length > 0 && !newEnvContent.endsWith('\n')) newEnvContent += '\n';
|
||||||
|
newEnvContent += `BITSTREAM_API_SECRET=${key}\n`;
|
||||||
|
} else {
|
||||||
|
newEnvContent = `# Bitstream API Secret\nBITSTREAM_API_SECRET=${key}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(envPath, newEnvContent, 'utf8');
|
||||||
|
console.log('Your new Bitstream API secret key:');
|
||||||
|
console.log(key);
|
||||||
|
console.log('\nThe key has been written to .env as BITSTREAM_API_SECRET.');
|
||||||
|
if (alreadySet && force) {
|
||||||
|
console.log('Previous BITSTREAM_API_SECRET was overwritten.');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
// server.js - Main backend server
|
||||||
|
require('dotenv').config({ quiet: true });
|
||||||
|
const express = require('express');
|
||||||
|
const chalk = require('chalk').default;
|
||||||
|
const auth = require('./middleware/auth');
|
||||||
|
const logApi = require('./middleware/logApi');
|
||||||
|
const cors = require('cors');
|
||||||
|
const http = require('http');
|
||||||
|
const containersRouter = require('./routes/containers');
|
||||||
|
const imagesRouter = require('./routes/images');
|
||||||
|
const systemRouter = require('./routes/system');
|
||||||
|
const setupWebSocket = require('./ws/statsSocket');
|
||||||
|
|
||||||
|
// Debug mode flag
|
||||||
|
const DEBUG_MODE = process.argv.includes('--debug');
|
||||||
|
global.DEBUG_MODE = DEBUG_MODE;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
|
||||||
|
// Setup WebSocket for real-time stats
|
||||||
|
setupWebSocket(server);
|
||||||
|
|
||||||
|
|
||||||
|
// Log all API requests if debug mode is enabled
|
||||||
|
app.use('/api', logApi);
|
||||||
|
// Protect all /api routes with API key auth
|
||||||
|
app.use('/api', auth);
|
||||||
|
app.use('/api/containers', containersRouter);
|
||||||
|
app.use('/api/images', imagesRouter);
|
||||||
|
app.use('/api/system', systemRouter);
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 6560;
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(chalk.green.bold('Bitstream server running on port'), chalk.cyan.bold(PORT));
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
console.log(chalk.yellow.bold('[DEBUG MODE ENABLED] All API requests will be logged.'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Bitstream Docker Management API
|
||||||
|
After=network.target docker.service
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/path/to/bitstream
|
||||||
|
ExecStart=/usr/bin/node server.js
|
||||||
|
Restart=on-failure
|
||||||
|
EnvironmentFile=/path/to/bitstream/.env
|
||||||
|
User=bitstream
|
||||||
|
Group=bitstream
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// calculateCPU.js
|
||||||
|
function calculateCPUPercent(stats) {
|
||||||
|
const cpuStats = stats.cpu_stats;
|
||||||
|
const preCpuStats = stats.precpu_stats;
|
||||||
|
const cpuDelta = cpuStats.cpu_usage.total_usage - preCpuStats.cpu_usage.total_usage;
|
||||||
|
const systemDelta = cpuStats.system_cpu_usage - preCpuStats.system_cpu_usage;
|
||||||
|
if (systemDelta > 0 && cpuDelta > 0) {
|
||||||
|
const cpuPercent = (cpuDelta / systemDelta) * cpuStats.online_cpus * 100;
|
||||||
|
return Math.round(cpuPercent * 100) / 100;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
module.exports = calculateCPUPercent;
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
// statsSocket.js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const docker = require('../docker/dockerClient');
|
||||||
|
const calculateCPUPercent = require('../utils/calculateCPU');
|
||||||
|
|
||||||
|
let clients = new Set();
|
||||||
|
|
||||||
|
function setupWebSocket(server) {
|
||||||
|
const wss = new WebSocket.Server({ server });
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
clients.add(ws);
|
||||||
|
ws.on('close', () => clients.delete(ws));
|
||||||
|
});
|
||||||
|
startStatsMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastStats(data) {
|
||||||
|
clients.forEach(client => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startStatsMonitoring() {
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const containers = await docker.listContainers();
|
||||||
|
const statsPromises = containers.map(async (containerInfo) => {
|
||||||
|
try {
|
||||||
|
const container = docker.getContainer(containerInfo.Id);
|
||||||
|
const stats = await container.stats({ stream: false });
|
||||||
|
return {
|
||||||
|
id: containerInfo.Id,
|
||||||
|
name: containerInfo.Names[0].replace('/', ''),
|
||||||
|
stats: {
|
||||||
|
cpu_percent: calculateCPUPercent(stats),
|
||||||
|
memory: {
|
||||||
|
usage: stats.memory_stats.usage || 0,
|
||||||
|
limit: stats.memory_stats.limit || 0,
|
||||||
|
percent: stats.memory_stats.limit > 0 ? (stats.memory_stats.usage / stats.memory_stats.limit) * 100 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const allStats = await Promise.all(statsPromises);
|
||||||
|
const validStats = allStats.filter(stat => stat !== null);
|
||||||
|
broadcastStats({ type: 'stats_update', data: validStats });
|
||||||
|
} catch (error) {}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = setupWebSocket;
|
||||||
Loading…
Reference in New Issue