feat: Kubernetes operator for MCP server management #47

Merged
michal merged 7 commits from feat/k8s-operator into main 2026-04-09 22:46:22 +00:00
2 changed files with 11 additions and 3 deletions
Showing only changes of commit 016f8abe68 - Show all commits

View File

@@ -49,6 +49,7 @@ export class InstanceService {
if ((inst.status === 'RUNNING' || inst.status === 'STARTING') && inst.containerId) { if ((inst.status === 'RUNNING' || inst.status === 'STARTING') && inst.containerId) {
try { try {
const info = await this.orchestrator.inspectContainer(inst.containerId); const info = await this.orchestrator.inspectContainer(inst.containerId);
if (info.state === 'stopped' || info.state === 'error') { if (info.state === 'stopped' || info.state === 'error') {
// Container died — get last logs for error context // Container died — get last logs for error context
let errorMsg = `Container ${info.state}`; let errorMsg = `Container ${info.state}`;
@@ -60,6 +61,12 @@ export class InstanceService {
await this.instanceRepo.updateStatus(inst.id, 'ERROR', { await this.instanceRepo.updateStatus(inst.id, 'ERROR', {
metadata: { error: errorMsg }, metadata: { error: errorMsg },
}); });
} else if (info.state === 'starting' && inst.status === 'RUNNING') {
// Pod went back to starting (e.g. CrashLoopBackOff restart)
await this.instanceRepo.updateStatus(inst.id, 'STARTING', {});
} else if (info.state === 'running' && inst.status === 'STARTING') {
// Pod became ready — promote to RUNNING
await this.instanceRepo.updateStatus(inst.id, 'RUNNING', {});
} }
} catch { } catch {
// Container gone entirely // Container gone entirely
@@ -305,7 +312,8 @@ export class InstanceService {
updateFields.port = containerInfo.port; updateFields.port = containerInfo.port;
} }
instance = await this.instanceRepo.updateStatus(instance.id, 'RUNNING', updateFields); // Set STARTING — syncStatus will promote to RUNNING once the container is actually ready
instance = await this.instanceRepo.updateStatus(instance.id, 'STARTING', updateFields);
} catch (err) { } catch (err) {
instance = await this.instanceRepo.updateStatus(instance.id, 'ERROR', { instance = await this.instanceRepo.updateStatus(instance.id, 'ERROR', {
metadata: { error: err instanceof Error ? err.message : String(err) }, metadata: { error: err instanceof Error ? err.message : String(err) },

View File

@@ -484,7 +484,7 @@ describe('MCP server full flow', () => {
expect(instancesRes.statusCode).toBe(200); expect(instancesRes.statusCode).toBe(200);
const instances = instancesRes.json<Array<{ id: string; status: string; containerId: string }>>(); const instances = instancesRes.json<Array<{ id: string; status: string; containerId: string }>>();
expect(instances).toHaveLength(1); expect(instances).toHaveLength(1);
expect(instances[0]!.status).toBe('RUNNING'); expect(instances[0]!.status).toBe('STARTING');
expect(instances[0]!.containerId).toBeTruthy(); expect(instances[0]!.containerId).toBeTruthy();
// 3. Verify orchestrator was called with correct spec // 3. Verify orchestrator was called with correct spec
@@ -564,7 +564,7 @@ describe('MCP server full flow', () => {
expect(listRes.statusCode).toBe(200); expect(listRes.statusCode).toBe(200);
const instances = listRes.json<Array<{ id: string; status: string }>>(); const instances = listRes.json<Array<{ id: string; status: string }>>();
expect(instances).toHaveLength(1); expect(instances).toHaveLength(1);
expect(instances[0]!.status).toBe('RUNNING'); expect(instances[0]!.status).toBe('STARTING');
const instanceId = instances[0]!.id; const instanceId = instances[0]!.id;
// Delete instance → triggers reconcile → new instance auto-created // Delete instance → triggers reconcile → new instance auto-created