# 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: ```typescript // services/orchestrator.ts export interface McpOrchestrator { startServer(server: McpServer, config: any): Promise; stopServer(instanceId: string): Promise; getStatus(instanceId: string): Promise; getLogs(instanceId: string, options: LogOptions): Promise; listInstances(filters?: InstanceFilters): Promise; } // 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 { 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: ```yaml 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: ```typescript 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; annotations: Record; 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: ```typescript 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 { 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: ```typescript 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; 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: ```typescript 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: ```typescript import { execSync } from 'child_process'; export async function setupKindCluster(): Promise { execSync('kind create cluster --name mcpctl-test --config tests/integration/kubernetes/kind-config.yaml', { stdio: 'inherit' }); } export async function teardownKindCluster(): Promise { execSync('kind delete cluster --name mcpctl-test', { stdio: 'inherit' }); } ``` 2. Create kind-config.yaml with proper resource limits 3. 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 4. 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 5. 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).