Create K8s Service per agent for terminal access
All checks were successful
Build and Push Backend / build (push) Successful in 5s
All checks were successful
Build and Push Backend / build (push) Successful in 5s
- Create ClusterIP service for each agent pod
- Service exposes port 7681 (ttyd terminal)
- Service DNS: {podName}-terminal.agents.svc.cluster.local
- Backend proxy uses service DNS instead of pod IP
- Fixes WebSocket proxy issues
- Services are created/deleted with pods
This commit is contained in:
@@ -8,7 +8,7 @@ import { agents, tasks } from '../../db/schema'
|
|||||||
import { eq, and } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { authenticateRequest } from '../middleware/auth'
|
import { authenticateRequest } from '../middleware/auth'
|
||||||
import { createAgentPod, deleteAgentPod } from '../../lib/k8s'
|
import { createAgentPod, deleteAgentPod, createAgentService, deleteAgentService } from '../../lib/k8s'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle all agent routes
|
* Handle all agent routes
|
||||||
@@ -332,12 +332,13 @@ async function unregisterAgent(agentId: string, userId: string): Promise<Respons
|
|||||||
.where(eq(tasks.id, existing[0].currentTaskId))
|
.where(eq(tasks.id, existing[0].currentTaskId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete K8s pod
|
// Delete K8s pod and service
|
||||||
try {
|
try {
|
||||||
await deleteAgentPod(existing[0].podName)
|
await deleteAgentPod(existing[0].podName)
|
||||||
|
await deleteAgentService(existing[0].podName)
|
||||||
} catch (k8sError) {
|
} catch (k8sError) {
|
||||||
console.error('Failed to delete pod, continuing...', k8sError)
|
console.error('Failed to delete pod/service, continuing...', k8sError)
|
||||||
// Continue even if pod deletion fails
|
// Continue even if deletion fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete agent from DB
|
// Delete agent from DB
|
||||||
@@ -402,11 +403,12 @@ async function launchAgent(userId: string, req: Request): Promise<Response> {
|
|||||||
|
|
||||||
await db.insert(agents).values(newAgent)
|
await db.insert(agents).values(newAgent)
|
||||||
|
|
||||||
// Create K8s pod
|
// Create K8s pod and service
|
||||||
try {
|
try {
|
||||||
await createAgentPod(podName, userId)
|
await createAgentPod(podName, userId, agentId)
|
||||||
|
await createAgentService(podName, agentId)
|
||||||
} catch (k8sError: any) {
|
} catch (k8sError: any) {
|
||||||
// If pod creation fails, rollback DB entry
|
// If pod/service creation fails, rollback DB entry
|
||||||
await db.delete(agents).where(eq(agents.id, agentId))
|
await db.delete(agents).where(eq(agents.id, agentId))
|
||||||
throw new Error(`Failed to create pod: ${k8sError.message}`)
|
throw new Error(`Failed to create pod: ${k8sError.message}`)
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/index.ts
21
src/index.ts
@@ -9,7 +9,7 @@ import { handleAuthRoutes, handleProjectRoutes, handleTaskRoutes, handleAgentRou
|
|||||||
import { authenticateRequest } from './api/middleware/auth'
|
import { authenticateRequest } from './api/middleware/auth'
|
||||||
import { agents } from './db/schema'
|
import { agents } from './db/schema'
|
||||||
import { eq, and } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
import { initK8sClient, getPodIP } from './lib/k8s'
|
import { initK8sClient } from './lib/k8s'
|
||||||
|
|
||||||
console.log('🚀 Starting AiWorker Backend...')
|
console.log('🚀 Starting AiWorker Backend...')
|
||||||
console.log(`Bun version: ${Bun.version}`)
|
console.log(`Bun version: ${Bun.version}`)
|
||||||
@@ -110,24 +110,15 @@ const server = Bun.serve({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get pod IP
|
// Proxy to agent terminal via service DNS
|
||||||
const podIP = await getPodIP(agent.podName)
|
// Service name: {podName}-terminal.agents.svc.cluster.local:7681
|
||||||
if (!podIP) {
|
|
||||||
console.error(`Pod ${agent.podName} not found or has no IP`)
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, message: 'Agent pod not ready' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proxy to agent terminal (ttyd on port 7681)
|
|
||||||
const agentPath = url.pathname.replace(`/agent-terminal/${agentId}`, '') || '/'
|
const agentPath = url.pathname.replace(`/agent-terminal/${agentId}`, '') || '/'
|
||||||
const agentUrl = `http://${podIP}:7681${agentPath}${url.search}`
|
const serviceUrl = `http://${agent.podName}-terminal.agents.svc.cluster.local:7681${agentPath}${url.search}`
|
||||||
|
|
||||||
console.log(`🔄 Proxying terminal request to ${agentUrl}`)
|
console.log(`🔄 Proxying terminal request to ${serviceUrl}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(agentUrl, {
|
const response = await fetch(serviceUrl, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
body: req.body,
|
body: req.body,
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export function createAgentPodSpec(podName: string, userId: string): k8s.V1Pod {
|
|||||||
labels: {
|
labels: {
|
||||||
app: 'claude-agent',
|
app: 'claude-agent',
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
podName: podName,
|
||||||
'aiworker.io/agent': 'true',
|
'aiworker.io/agent': 'true',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -187,10 +188,73 @@ export function createAgentPodSpec(podName: string, userId: string): k8s.V1Pod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create service for agent pod (for terminal access)
|
||||||
|
*/
|
||||||
|
export async function createAgentService(podName: string, agentId: string): Promise<void> {
|
||||||
|
const client = getK8sClient()
|
||||||
|
|
||||||
|
const serviceSpec: k8s.V1Service = {
|
||||||
|
metadata: {
|
||||||
|
name: `${podName}-terminal`,
|
||||||
|
namespace: 'agents',
|
||||||
|
labels: {
|
||||||
|
app: 'claude-agent-terminal',
|
||||||
|
agentId: agentId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
selector: {
|
||||||
|
app: 'claude-agent',
|
||||||
|
podName: podName,
|
||||||
|
},
|
||||||
|
ports: [{
|
||||||
|
name: 'terminal',
|
||||||
|
port: 7681,
|
||||||
|
targetPort: 7681 as any,
|
||||||
|
protocol: 'TCP'
|
||||||
|
}],
|
||||||
|
type: 'ClusterIP'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.createNamespacedService({
|
||||||
|
namespace: 'agents',
|
||||||
|
body: serviceSpec
|
||||||
|
})
|
||||||
|
console.log(`✅ Service ${podName}-terminal created`)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ Failed to create service:`, error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete agent service
|
||||||
|
*/
|
||||||
|
export async function deleteAgentService(podName: string): Promise<void> {
|
||||||
|
const client = getK8sClient()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteNamespacedService({
|
||||||
|
name: `${podName}-terminal`,
|
||||||
|
namespace: 'agents'
|
||||||
|
})
|
||||||
|
console.log(`✅ Service ${podName}-terminal deleted`)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.statusCode === 404 || error.response?.statusCode === 404) {
|
||||||
|
console.log(`⚠️ Service ${podName}-terminal not found`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error(`❌ Error deleting service:`, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create agent pod in Kubernetes
|
* Create agent pod in Kubernetes
|
||||||
*/
|
*/
|
||||||
export async function createAgentPod(podName: string, userId: string): Promise<void> {
|
export async function createAgentPod(podName: string, userId: string, agentId: string): Promise<void> {
|
||||||
const { client, options } = getK8sClientWithOptions()
|
const { client, options } = getK8sClientWithOptions()
|
||||||
const podSpec = createAgentPodSpec(podName, userId)
|
const podSpec = createAgentPodSpec(podName, userId)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user