Files
mcpctl/.taskmaster/tasks/task_017.md
2026-02-21 03:10:39 +00:00

15 KiB

Task ID: 17

Title: Implement Kubernetes Support Architecture

Status: pending

Dependencies: 6, 16

Priority: low

Description: Design and implement the abstraction layer for Kubernetes deployment support, preparing for future pod scheduling of MCP instances.

Details:

Create orchestrator abstraction:

// services/orchestrator.ts
export interface McpOrchestrator {
  startServer(server: McpServer, config: any): Promise<string>;
  stopServer(instanceId: string): Promise<void>;
  getStatus(instanceId: string): Promise<InstanceStatus>;
  getLogs(instanceId: string, options: LogOptions): Promise<string>;
  listInstances(filters?: InstanceFilters): Promise<Instance[]>;
}

// Docker implementation (current)
export class DockerOrchestrator implements McpOrchestrator {
  private docker: Docker;
  // ... existing Docker implementation
}

// Kubernetes implementation (future-ready)
export class KubernetesOrchestrator implements McpOrchestrator {
  private k8sClient: KubernetesClient;

  constructor(config: K8sConfig) {
    this.k8sClient = new KubernetesClient(config);
  }

  async startServer(server: McpServer, config: any): Promise<string> {
    const pod = {
      apiVersion: 'v1',
      kind: 'Pod',
      metadata: {
        name: `mcp-${server.name}-${Date.now()}`,
        labels: {
          'mcpctl.io/server': server.name,
          'mcpctl.io/managed': 'true'
        }
      },
      spec: {
        containers: [{
          name: 'mcp-server',
          image: server.image || 'node:20-alpine',
          command: this.buildCommand(server),
          env: this.buildEnvVars(config),
          resources: {
            requests: { memory: '128Mi', cpu: '100m' },
            limits: { memory: '512Mi', cpu: '500m' }
          }
        }],
        restartPolicy: 'Always'
      }
    };

    const created = await this.k8sClient.createPod(pod);
    return created.metadata.name;
  }

  // ... other K8s implementations
}

// Factory based on configuration
export function createOrchestrator(config: OrchestratorConfig): McpOrchestrator {
  switch (config.type) {
    case 'docker': return new DockerOrchestrator(config);
    case 'kubernetes': return new KubernetesOrchestrator(config);
    default: throw new Error(`Unknown orchestrator: ${config.type}`);
  }
}

Configuration:

orchestrator:
  type: docker  # or 'kubernetes'
  docker:
    socketPath: /var/run/docker.sock
  kubernetes:
    namespace: mcpctl
    kubeconfig: /path/to/kubeconfig

Test Strategy:

Unit test orchestrator interface compliance for both implementations. Integration test Docker implementation. Mock Kubernetes API for K8s implementation tests.

Subtasks

17.1. Define K8s-specific interfaces and write TDD tests for KubernetesOrchestrator

Status: pending
Dependencies: None

Extend the McpOrchestrator interface (from Task 6) with Kubernetes-specific types and write comprehensive Vitest unit tests for all KubernetesOrchestrator methods BEFORE implementation using mocked @kubernetes/client-node.

Details:

Create src/shared/src/types/kubernetes.ts with K8s-specific types:

import { McpOrchestrator, McpServer, InstanceStatus, LogOptions, InstanceFilters } from './orchestrator';

export interface K8sConfig {
  namespace: string;
  kubeconfig?: string;  // Path to kubeconfig file
  inCluster?: boolean;  // Use in-cluster config
  context?: string;     // Specific kubeconfig context
}

export interface K8sPodMetadata {
  name: string;
  namespace: string;
  labels: Record<string, string>;
  annotations: Record<string, string>;
  uid: string;
}

export interface K8sResourceRequirements {
  requests: { memory: string; cpu: string };
  limits: { memory: string; cpu: string };
}

export interface K8sSecurityContext {
  runAsNonRoot: boolean;
  runAsUser: number;
  readOnlyRootFilesystem: boolean;
  allowPrivilegeEscalation: boolean;
  capabilities: { drop: string[] };
}

Create src/mcpd/tests/unit/services/kubernetes-orchestrator.test.ts with comprehensive TDD tests:

  1. Constructor tests: verify kubeconfig loading (file path vs in-cluster), namespace validation, error handling for missing config
  2. startServer() tests: verify Pod spec generation includes security context, resource limits, labels, command building, env vars
  3. stopServer() tests: verify graceful pod termination, wait for completion, error handling for non-existent pods
  4. getStatus() tests: verify status mapping from K8s pod phases (Pending, Running, Succeeded, Failed, Unknown) to InstanceStatus
  5. getLogs() tests: verify log options (tail, follow, since, timestamps) are mapped correctly to K8s log API
  6. listInstances() tests: verify label selector filtering works, pagination handling for large deployments

Mock @kubernetes/client-node CoreV1Api using vitest.mock() with proper type definitions. All tests should fail initially (TDD red phase).

17.2. Implement KubernetesOrchestrator class with Pod security contexts and resource management

Status: pending
Dependencies: 17.1

Implement the KubernetesOrchestrator class using @kubernetes/client-node, with all methods passing TDD tests from subtask 1, including SRE-approved pod security contexts, resource requests/limits, and proper label conventions.

Details:

Install @kubernetes/client-node in src/mcpd. Create src/mcpd/src/services/kubernetes-orchestrator.ts:

import * as k8s from '@kubernetes/client-node';
import { McpOrchestrator, McpServer, InstanceStatus, LogOptions, InstanceFilters, Instance } from '@mcpctl/shared';
import { K8sConfig, K8sSecurityContext, K8sResourceRequirements } from '@mcpctl/shared';

export class KubernetesOrchestrator implements McpOrchestrator {
  private coreApi: k8s.CoreV1Api;
  private namespace: string;

  constructor(config: K8sConfig) {
    const kc = new k8s.KubeConfig();
    if (config.inCluster) {
      kc.loadFromCluster();
    } else if (config.kubeconfig) {
      kc.loadFromFile(config.kubeconfig);
    } else {
      kc.loadFromDefault();
    }
    if (config.context) kc.setCurrentContext(config.context);
    this.coreApi = kc.makeApiClient(k8s.CoreV1Api);
    this.namespace = config.namespace;
  }

  async startServer(server: McpServer, config: any): Promise<string> {
    const podName = `mcp-${server.name}-${Date.now()}`;
    const pod: k8s.V1Pod = {
      apiVersion: 'v1',
      kind: 'Pod',
      metadata: {
        name: podName,
        namespace: this.namespace,
        labels: {
          'mcpctl.io/server': server.name,
          'mcpctl.io/managed': 'true',
          'app.kubernetes.io/name': `mcp-${server.name}`,
          'app.kubernetes.io/component': 'mcp-server',
          'app.kubernetes.io/managed-by': 'mcpctl'
        },
        annotations: {
          'mcpctl.io/created-at': new Date().toISOString()
        }
      },
      spec: {
        containers: [{
          name: 'mcp-server',
          image: server.image || 'node:20-alpine',
          command: this.buildCommand(server),
          env: this.buildEnvVars(config),
          resources: this.getResourceRequirements(config),
          securityContext: this.getSecurityContext()
        }],
        securityContext: { runAsNonRoot: true, runAsUser: 1000, fsGroup: 1000 },
        restartPolicy: 'Always',
        serviceAccountName: config.serviceAccount || 'default'
      }
    };
    const created = await this.coreApi.createNamespacedPod(this.namespace, pod);
    return created.body.metadata!.name!;
  }

  private getSecurityContext(): k8s.V1SecurityContext {
    return {
      runAsNonRoot: true,
      runAsUser: 1000,
      readOnlyRootFilesystem: true,
      allowPrivilegeEscalation: false,
      capabilities: { drop: ['ALL'] }
    };
  }

  private getResourceRequirements(config: any): k8s.V1ResourceRequirements {
    return {
      requests: { memory: config.memoryRequest || '128Mi', cpu: config.cpuRequest || '100m' },
      limits: { memory: config.memoryLimit || '512Mi', cpu: config.cpuLimit || '500m' }
    };
  }
  // ... implement stopServer, getStatus, getLogs, listInstances
}

Implement all remaining methods with proper error handling and K8s API error translation.

17.3. Implement createOrchestrator factory function and configuration schema

Status: pending
Dependencies: 17.2

Create the orchestrator factory function that instantiates DockerOrchestrator or KubernetesOrchestrator based on configuration, with Zod schema validation and configuration file support.

Details:

Create src/mcpd/src/services/orchestrator-factory.ts:

import { z } from 'zod';
import { McpOrchestrator } from '@mcpctl/shared';
import { DockerOrchestrator } from './container-manager'; // From Task 6
import { KubernetesOrchestrator } from './kubernetes-orchestrator';

const DockerConfigSchema = z.object({
  socketPath: z.string().default('/var/run/docker.sock'),
  host: z.string().optional(),
  port: z.number().optional(),
  network: z.string().default('mcpctl-network')
});

const KubernetesConfigSchema = z.object({
  namespace: z.string().default('mcpctl'),
  kubeconfig: z.string().optional(),
  inCluster: z.boolean().default(false),
  context: z.string().optional()
});

const OrchestratorConfigSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('docker'), docker: DockerConfigSchema }),
  z.object({ type: z.literal('kubernetes'), kubernetes: KubernetesConfigSchema })
]);

export type OrchestratorConfig = z.infer<typeof OrchestratorConfigSchema>;

export function createOrchestrator(config: OrchestratorConfig): McpOrchestrator {
  const validated = OrchestratorConfigSchema.parse(config);
  switch (validated.type) {
    case 'docker':
      return new DockerOrchestrator(validated.docker);
    case 'kubernetes':
      return new KubernetesOrchestrator(validated.kubernetes);
    default:
      throw new Error(`Unknown orchestrator type`);
  }
}

Create src/mcpd/src/config/orchestrator.ts for loading config from environment variables and config files (supporting both YAML and JSON). Write TDD tests in src/mcpd/tests/unit/services/orchestrator-factory.test.ts BEFORE implementation:

  1. Test factory creates DockerOrchestrator when type='docker'
  2. Test factory creates KubernetesOrchestrator when type='kubernetes'
  3. Test factory throws on invalid type
  4. Test Zod validation rejects invalid configs
  5. Test default values are applied correctly
  6. Test config loading from MCPCTL_ORCHESTRATOR_TYPE env var

17.4. Implement K8s NetworkPolicy and PersistentVolumeClaim builders for MCP server isolation

Status: pending
Dependencies: 17.2

Create resource builders for Kubernetes NetworkPolicy (network isolation between MCP servers) and PersistentVolumeClaim (for stateful data MCPs like caching or GPU providers) with proper annotations for observability.

Details:

Create src/mcpd/src/services/k8s-resources.ts with resource builder functions:

import * as k8s from '@kubernetes/client-node';

export interface NetworkPolicyConfig {
  serverName: string;
  namespace: string;
  allowEgress?: string[];  // CIDR blocks or service names to allow
  allowIngress?: string[]; // Pod labels allowed to connect
}

export function buildNetworkPolicy(config: NetworkPolicyConfig): k8s.V1NetworkPolicy {
  return {
    apiVersion: 'networking.k8s.io/v1',
    kind: 'NetworkPolicy',
    metadata: {
      name: `mcp-${config.serverName}-netpol`,
      namespace: config.namespace,
      labels: { 'mcpctl.io/server': config.serverName, 'mcpctl.io/managed': 'true' }
    },
    spec: {
      podSelector: { matchLabels: { 'mcpctl.io/server': config.serverName } },
      policyTypes: ['Ingress', 'Egress'],
      ingress: [{
        from: [{ podSelector: { matchLabels: { 'mcpctl.io/component': 'local-proxy' } } }]
      }],
      egress: config.allowEgress?.map(cidr => ({
        to: [{ ipBlock: { cidr } }]
      })) || [{ to: [{ ipBlock: { cidr: '0.0.0.0/0' } }] }]  // Default: allow all egress
    }
  };
}

export interface PVCConfig {
  serverName: string;
  namespace: string;
  storageSize: string;  // e.g., '1Gi'
  storageClass?: string;
  accessModes?: string[];
}

export function buildPVC(config: PVCConfig): k8s.V1PersistentVolumeClaim {
  return {
    apiVersion: 'v1',
    kind: 'PersistentVolumeClaim',
    metadata: {
      name: `mcp-${config.serverName}-data`,
      namespace: config.namespace,
      labels: { 'mcpctl.io/server': config.serverName, 'mcpctl.io/managed': 'true' },
      annotations: {
        'mcpctl.io/purpose': 'mcp-server-cache',
        'mcpctl.io/created-at': new Date().toISOString()
      }
    },
    spec: {
      accessModes: config.accessModes || ['ReadWriteOnce'],
      storageClassName: config.storageClass,
      resources: { requests: { storage: config.storageSize } }
    }
  };
}

export function buildGpuAffinityRules(gpuType: string): k8s.V1Affinity {
  return {
    nodeAffinity: {
      requiredDuringSchedulingIgnoredDuringExecution: {
        nodeSelectorTerms: [{
          matchExpressions: [{
            key: 'nvidia.com/gpu.product',
            operator: 'In',
            values: [gpuType]
          }]
        }]
      }
    }
  };
}

Write TDD tests in src/mcpd/tests/unit/services/k8s-resources.test.ts verifying all resource builders generate valid K8s manifests.

17.5. Create integration tests with kind/k3d and document K8s deployment architecture

Status: pending
Dependencies: 17.2, 17.3, 17.4

Build integration test suite using kind or k3d for local K8s cluster testing, create comprehensive SRE documentation covering deployment architecture, resource recommendations, and network requirements.

Details:

Create src/mcpd/tests/integration/kubernetes/ directory with integration tests:

  1. Create setup script src/mcpd/tests/integration/kubernetes/setup-kind.ts:
import { execSync } from 'child_process';

export async function setupKindCluster(): Promise<void> {
  execSync('kind create cluster --name mcpctl-test --config tests/integration/kubernetes/kind-config.yaml', { stdio: 'inherit' });
}

export async function teardownKindCluster(): Promise<void> {
  execSync('kind delete cluster --name mcpctl-test', { stdio: 'inherit' });
}
  1. Create kind-config.yaml with proper resource limits

  2. Create kubernetes-orchestrator.integration.test.ts testing:

    • Pod creation and deletion lifecycle
    • Status monitoring through pod phases
    • Log retrieval from running pods
    • NetworkPolicy enforcement (cannot reach blocked endpoints)
    • PVC mounting for stateful MCPs
  3. Create src/mcpd/docs/KUBERNETES_DEPLOYMENT.md documenting:

    • Architecture overview: mcpctl namespace, resource types, label conventions
    • Security: Pod security standards (restricted), NetworkPolicies, ServiceAccounts
    • SRE recommendations: HPA configurations, PDB templates, monitoring with Prometheus labels
    • Resource sizing guide: Small (128Mi/100m), Medium (512Mi/500m), Large (2Gi/1000m)
    • Network requirements: Required egress rules per MCP server type, ingress from local-proxy
    • Troubleshooting: Common issues, kubectl commands, log access
    • GPU support: Node affinity, NVIDIA device plugin requirements
  4. Create example manifests in src/mcpd/examples/k8s/:

    • namespace.yaml, rbac.yaml, networkpolicy.yaml, sample-mcp-pod.yaml

Integration tests should skip gracefully when kind is not available (CI compatibility).