mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 09:24:09 +00:00
2a37e114df
- NodeRegistry: wire TLS ca/cert/key into Dockerode when present on node config - index.ts: /api/system/stats now branches on node type — remote nodes use docker.info() for CPU/RAM, local keeps systeminformation; disk gracefully returns null for remote - index.ts: POST /api/nodes now persists tls_ca, tls_cert, tls_key fields - FileSystemService: throw clean error on missing/empty compose_dir instead of crashing path.join - FileSystemService: guard getStacks() against falsy item.name entries - SSHFileAdapter: filter undefined/non-string names from SFTP readdir before returning - NodeManager: add SSH Authentication Type toggle (Password vs Private Key) - NodeManager: add Enable TLS toggle with conditional CA/cert/key textarea fields - NodeManager: auto-test connection immediately after node creation - NodeManager: replace "Strategy B" copy with Docker TCP setup instructions - NodeManager: add pr-8 to header and DialogHeader to prevent overlap with parent dialog X button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
import Docker from 'dockerode';
|
|
import { DatabaseService, Node } from './DatabaseService';
|
|
|
|
/**
|
|
* NodeRegistry: Manages Docker daemon connections for multiple nodes.
|
|
* Replaces the old singleton DockerController pattern. Each node
|
|
* (local or remote) gets its own dedicated Docker client instance.
|
|
*/
|
|
export class NodeRegistry {
|
|
private static instance: NodeRegistry;
|
|
private connections: Map<number, Docker> = new Map();
|
|
|
|
private constructor() {}
|
|
|
|
public static getInstance(): NodeRegistry {
|
|
if (!NodeRegistry.instance) {
|
|
NodeRegistry.instance = new NodeRegistry();
|
|
}
|
|
return NodeRegistry.instance;
|
|
}
|
|
|
|
/**
|
|
* Get a Docker client for a specific node.
|
|
* Creates the connection lazily on first request and caches it.
|
|
*/
|
|
public getDocker(nodeId: number): Docker {
|
|
// Return cached connection if available
|
|
if (this.connections.has(nodeId)) {
|
|
return this.connections.get(nodeId)!;
|
|
}
|
|
|
|
const db = DatabaseService.getInstance();
|
|
const node = db.getNode(nodeId);
|
|
|
|
if (!node) {
|
|
throw new Error(`Node with id ${nodeId} not found`);
|
|
}
|
|
|
|
const docker = this.createDockerClient(node);
|
|
this.connections.set(nodeId, docker);
|
|
return docker;
|
|
}
|
|
|
|
/**
|
|
* Get the Docker client for the default node.
|
|
* This is the backward-compatible path for all existing code.
|
|
*/
|
|
public getDefaultDocker(): Docker {
|
|
const db = DatabaseService.getInstance();
|
|
const defaultNode = db.getDefaultNode();
|
|
|
|
if (!defaultNode || !defaultNode.id) {
|
|
// Absolute fallback: local socket (preserves legacy behavior)
|
|
return new Docker();
|
|
}
|
|
|
|
return this.getDocker(defaultNode.id);
|
|
}
|
|
|
|
/**
|
|
* Get the default node ID. Returns the ID of the node marked as default.
|
|
*/
|
|
public getDefaultNodeId(): number {
|
|
const db = DatabaseService.getInstance();
|
|
const defaultNode = db.getDefaultNode();
|
|
return defaultNode?.id || 1;
|
|
}
|
|
|
|
/**
|
|
* Get a node configuration by its ID.
|
|
*/
|
|
public getNode(nodeId: number): Node | undefined {
|
|
const db = DatabaseService.getInstance();
|
|
return db.getNode(nodeId);
|
|
}
|
|
|
|
/**
|
|
* Create a Docker client based on node configuration.
|
|
* - Local nodes: use the default socket (Docker autodetects)
|
|
* - Remote nodes: connect via TCP to host:port
|
|
*/
|
|
private createDockerClient(node: Node): Docker {
|
|
if (node.type === 'local') {
|
|
// Local node: use the default Docker socket
|
|
return new Docker();
|
|
}
|
|
|
|
// Remote node: connect via Docker TCP API
|
|
if (!node.host) {
|
|
throw new Error(`Remote node "${node.name}" is missing a host address`);
|
|
}
|
|
|
|
const dockerOptions: Docker.DockerOptions = {
|
|
host: node.host,
|
|
port: node.port || 2375,
|
|
};
|
|
|
|
// Phase 55.4 — TLS: if all three certs are present, enable secure connection
|
|
if (node.tls_ca && node.tls_cert && node.tls_key) {
|
|
dockerOptions.ca = node.tls_ca;
|
|
dockerOptions.cert = node.tls_cert;
|
|
dockerOptions.key = node.tls_key;
|
|
}
|
|
|
|
return new Docker(dockerOptions);
|
|
}
|
|
|
|
/**
|
|
* Test connectivity to a specific node.
|
|
* Returns true if we can ping the Docker daemon.
|
|
*/
|
|
public async testConnection(nodeId: number): Promise<{ success: boolean; error?: string; info?: any }> {
|
|
const db = DatabaseService.getInstance();
|
|
const node = db.getNode(nodeId);
|
|
|
|
if (!node) {
|
|
return { success: false, error: 'Node not found' };
|
|
}
|
|
|
|
try {
|
|
const docker = this.createDockerClient(node);
|
|
const info = await docker.info();
|
|
|
|
// Validate the payload contains actual Docker daemon info, not arbitrary HTML
|
|
if (!info || !info.OperatingSystem || typeof info.Containers !== 'number') {
|
|
throw new Error("Invalid response from Docker API. Did you provide a web port instead of the Docker daemon port?");
|
|
}
|
|
|
|
db.updateNodeStatus(nodeId, 'online');
|
|
return {
|
|
success: true,
|
|
info: {
|
|
name: info.Name,
|
|
serverVersion: info.ServerVersion,
|
|
os: info.OperatingSystem,
|
|
architecture: info.Architecture,
|
|
containers: info.Containers,
|
|
containersRunning: info.ContainersRunning,
|
|
images: info.Images,
|
|
memTotal: info.MemTotal,
|
|
cpus: info.NCPU,
|
|
}
|
|
};
|
|
} catch (error: any) {
|
|
db.updateNodeStatus(nodeId, 'offline');
|
|
return { success: false, error: error.message || 'Connection failed' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Evict a cached connection (e.g., after node config changes).
|
|
*/
|
|
public evictConnection(nodeId: number): void {
|
|
this.connections.delete(nodeId);
|
|
}
|
|
|
|
/**
|
|
* Flush all cached connections.
|
|
*/
|
|
public flushAll(): void {
|
|
this.connections.clear();
|
|
}
|
|
|
|
/**
|
|
* Get the compose directory for a specific node.
|
|
*/
|
|
public getComposeDir(nodeId: number): string {
|
|
const db = DatabaseService.getInstance();
|
|
const node = db.getNode(nodeId);
|
|
return node?.compose_dir || process.env.COMPOSE_DIR || '/app/compose';
|
|
}
|
|
}
|