[grade=pending] QA sprint: commit 103 at-risk files from multi-agent sprint work
Committed by Hermes autonomous QA sprint 2026-08-13. These files were modified during the Aug 12 sprint but never committed.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* DC-100: Service Discovery — auto-detect running Docker containers
|
||||
* and suggest them as services to add to the dashboard.
|
||||
*
|
||||
* Scans all running containers, extracts port mappings, image info,
|
||||
* and labels to suggest service configurations.
|
||||
*/
|
||||
const express = require('express');
|
||||
const { ok, errorResponse } = require('../src/utils/responses');
|
||||
const { ErrorCodes } = require('../src/utilities/error-codes');
|
||||
|
||||
// Known image patterns → suggested service type and default config
|
||||
const IMAGE_PATTERNS = {
|
||||
'plexinc/pms': { type: 'plex', name: 'Plex', port: 32400, https: false },
|
||||
'linuxserver/jellyfin': { type: 'jellyfin', name: 'Jellyfin', port: 8096, https: false },
|
||||
'linuxserver/emby': { type: 'emby', name: 'Emby', port: 8096, https: false },
|
||||
'lscr.io/linuxserver/sonarr': { type: 'sonarr', name: 'Sonarr', port: 8989, https: false },
|
||||
'lscr.io/linuxserver/radarr': { type: 'radarr', name: 'Radarr', port: 7878, https: false },
|
||||
'lscr.io/linuxserver/prowlarr': { type: 'prowlarr', name: 'Prowlarr', port: 9696, https: false },
|
||||
'lscr.io/linuxserver/lidarr': { type: 'lidarr', name: 'Lidarr', port: 8686, https: false },
|
||||
'lscr.io/linuxserver/readarr': { type: 'readarr', name: 'Readarr', port: 8787, https: false },
|
||||
'lscr.io/linuxserver/qbittorrent': { type: 'qbittorrent', name: 'qBittorrent', port: 8080, https: false },
|
||||
'lscr.io/linuxserver/transmission': { type: 'transmission', name: 'Transmission', port: 9091, https: false },
|
||||
'haugene/transmission-openvpn': { type: 'transmission', name: 'Transmission+VPN', port: 9091, https: false },
|
||||
'gitea/gitea': { type: 'gitea', name: 'Gitea', port: 3000, https: false },
|
||||
'nextcloud': { type: 'nextcloud', name: 'Nextcloud', port: 80, https: false },
|
||||
'vaultwarden': { type: 'vaultwarden', name: 'Vaultwarden', port: 80, https: false },
|
||||
'nginx': { type: 'web', name: 'Nginx', port: 80, https: false },
|
||||
'caddy': { type: 'web', name: 'Caddy', port: 80, https: false },
|
||||
'redis': { type: 'redis', name: 'Redis', port: 6379, https: false },
|
||||
'postgres': { type: 'postgres', name: 'PostgreSQL', port: 5432, https: false },
|
||||
'mariadb': { type: 'mariadb', name: 'MariaDB', port: 3306, https: false },
|
||||
'mongo': { type: 'mongodb', name: 'MongoDB', port: 27017, https: false },
|
||||
};
|
||||
|
||||
module.exports = function({ docker, servicesStateManager, asyncHandler }) {
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/v1/discover — scan running containers for auto-detection
|
||||
*
|
||||
* Returns a list of discovered services with suggested configurations.
|
||||
* Services already in the dashboard are marked as `existing: true`.
|
||||
*/
|
||||
router.get('/discover', asyncHandler(async (req, res) => {
|
||||
if (!docker || !docker.client) {
|
||||
return errorResponse(res, 503, 'Docker daemon not available', {
|
||||
code: ErrorCodes.CONTAINER.DOCKER_UNREACHABLE,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all running containers
|
||||
const containers = await docker.client.listContainers({ all: false });
|
||||
|
||||
// Get existing service IDs to mark duplicates
|
||||
let existingIds = new Set();
|
||||
if (servicesStateManager) {
|
||||
try {
|
||||
const services = await servicesStateManager.read();
|
||||
const list = Array.isArray(services) ? services : (services.services || []);
|
||||
existingIds = new Set(list.map(s => s.id));
|
||||
} catch { /* ignore — treat as empty */ }
|
||||
}
|
||||
|
||||
const discovered = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const container of containers) {
|
||||
const name = (container.Names && container.Names[0] || '').replace(/^\//, '');
|
||||
if (!name || seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
|
||||
const image = container.Image || '';
|
||||
const imageBase = image.split(':')[0].toLowerCase();
|
||||
|
||||
// Match against known patterns
|
||||
let matched = null;
|
||||
for (const [pattern, config] of Object.entries(IMAGE_PATTERNS)) {
|
||||
if (imageBase.includes(pattern)) {
|
||||
matched = config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract port mappings
|
||||
const ports = (container.Ports || []).map(p => ({
|
||||
ip: p.IP || '0.0.0.0',
|
||||
privatePort: p.PrivatePort,
|
||||
publicPort: p.PublicPort,
|
||||
type: p.Type || 'tcp',
|
||||
})).filter(p => p.publicPort);
|
||||
|
||||
// Suggested config
|
||||
const suggestedPort = matched ? matched.port : (ports[0] && ports[0].publicPort) || null;
|
||||
const suggestedId = name.replace(/[^a-z0-9-]/gi, '-').toLowerCase();
|
||||
|
||||
discovered.push({
|
||||
containerId: container.Id.substring(0, 12),
|
||||
name,
|
||||
image,
|
||||
status: container.State,
|
||||
suggested: {
|
||||
id: suggestedId,
|
||||
name: matched ? matched.name : name.charAt(0).toUpperCase() + name.slice(1),
|
||||
type: matched ? matched.type : 'generic',
|
||||
port: suggestedPort,
|
||||
protocol: matched ? (matched.https ? 'https' : 'http') : 'http',
|
||||
},
|
||||
ports,
|
||||
labels: container.Labels || {},
|
||||
existing: existingIds.has(suggestedId),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort: unmatched first (more interesting to discover), then by name
|
||||
discovered.sort((a, b) => {
|
||||
if (a.existing !== b.existing) return a.existing ? 1 : -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
ok(res, {
|
||||
total: discovered.length,
|
||||
matched: discovered.filter(d => d.suggested.type !== 'generic').length,
|
||||
newServices: discovered.filter(d => !d.existing).length,
|
||||
discovered,
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(res, 500, `Discovery failed: ${err.message}`, {
|
||||
code: ErrorCodes.GENERAL.INTERNAL,
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
return router;
|
||||
};
|
||||
Reference in New Issue
Block a user