feat: add Docker container management for MCP servers
McpOrchestrator interface with DockerContainerManager implementation, instance service for lifecycle management, instance API routes, and docker-compose with mcpd service. 127 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
125
src/mcpd/tests/container-manager.test.ts
Normal file
125
src/mcpd/tests/container-manager.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { DockerContainerManager } from '../src/services/docker/container-manager.js';
|
||||
import type { ContainerSpec } from '../src/services/orchestrator.js';
|
||||
|
||||
// Mock dockerode
|
||||
vi.mock('dockerode', () => {
|
||||
const mockContainer = {
|
||||
id: 'ctr-abc123',
|
||||
start: vi.fn(async () => {}),
|
||||
stop: vi.fn(async () => {}),
|
||||
remove: vi.fn(async () => {}),
|
||||
inspect: vi.fn(async () => ({
|
||||
Id: 'ctr-abc123',
|
||||
Name: '/mcpctl-test',
|
||||
State: { Status: 'running' },
|
||||
Created: '2024-01-01T00:00:00Z',
|
||||
NetworkSettings: {
|
||||
Ports: {
|
||||
'3000/tcp': [{ HostIp: '0.0.0.0', HostPort: '32768' }],
|
||||
},
|
||||
},
|
||||
})),
|
||||
logs: vi.fn(async () => Buffer.from('test log output')),
|
||||
};
|
||||
|
||||
const mockModem = {
|
||||
followProgress: vi.fn((_stream: unknown, onFinished: (err: Error | null) => void) => {
|
||||
onFinished(null);
|
||||
}),
|
||||
};
|
||||
|
||||
class MockDocker {
|
||||
modem = mockModem;
|
||||
ping = vi.fn(async () => 'OK');
|
||||
pull = vi.fn(async () => ({}));
|
||||
createContainer = vi.fn(async () => mockContainer);
|
||||
getContainer = vi.fn(() => mockContainer);
|
||||
}
|
||||
|
||||
return { default: MockDocker };
|
||||
});
|
||||
|
||||
describe('DockerContainerManager', () => {
|
||||
let manager: DockerContainerManager;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
manager = new DockerContainerManager();
|
||||
});
|
||||
|
||||
describe('ping', () => {
|
||||
it('returns true when Docker is available', async () => {
|
||||
expect(await manager.ping()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pullImage', () => {
|
||||
it('pulls an image', async () => {
|
||||
await manager.pullImage('node:20-alpine');
|
||||
// No error = success
|
||||
});
|
||||
});
|
||||
|
||||
describe('createContainer', () => {
|
||||
it('creates and starts a container', async () => {
|
||||
const spec: ContainerSpec = {
|
||||
image: 'ghcr.io/slack-mcp:latest',
|
||||
name: 'mcpctl-slack-inst1',
|
||||
env: { SLACK_TOKEN: 'xoxb-test' },
|
||||
containerPort: 3000,
|
||||
hostPort: null,
|
||||
labels: { 'mcpctl.server-id': 'srv-1' },
|
||||
};
|
||||
|
||||
const result = await manager.createContainer(spec);
|
||||
|
||||
expect(result.containerId).toBe('ctr-abc123');
|
||||
expect(result.name).toBe('mcpctl-test');
|
||||
expect(result.state).toBe('running');
|
||||
expect(result.port).toBe(32768);
|
||||
});
|
||||
|
||||
it('applies resource limits', async () => {
|
||||
const spec: ContainerSpec = {
|
||||
image: 'test:latest',
|
||||
name: 'test-container',
|
||||
memoryLimit: 256 * 1024 * 1024,
|
||||
nanoCpus: 250_000_000,
|
||||
};
|
||||
|
||||
await manager.createContainer(spec);
|
||||
// The mock captures the call - we verify it doesn't throw
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopContainer', () => {
|
||||
it('stops a container', async () => {
|
||||
await manager.stopContainer('ctr-abc123');
|
||||
// No error = success
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeContainer', () => {
|
||||
it('removes a container', async () => {
|
||||
await manager.removeContainer('ctr-abc123', true);
|
||||
// No error = success
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspectContainer', () => {
|
||||
it('returns container info with mapped state', async () => {
|
||||
const info = await manager.inspectContainer('ctr-abc123');
|
||||
expect(info.containerId).toBe('ctr-abc123');
|
||||
expect(info.state).toBe('running');
|
||||
expect(info.port).toBe(32768);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContainerLogs', () => {
|
||||
it('returns container logs', async () => {
|
||||
const logs = await manager.getContainerLogs('ctr-abc123', { tail: 50 });
|
||||
expect(logs.stdout).toBe('test log output');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user