Files
aiworker-backend/src/lib/k8s.ts
Hector Ros 3fef6030ea
All checks were successful
Build and Push Backend / build (push) Successful in 5s
Fix: Handle undefined result.body.metadata gracefully
Pod creation succeeds but response structure may not have metadata.
Add safe navigation to prevent error.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-01-20 18:35:21 +01:00

264 lines
6.6 KiB
TypeScript

/**
* Kubernetes API client utilities
*/
import * as k8s from '@kubernetes/client-node'
import * as https from 'https'
let k8sClient: k8s.CoreV1Api | null = null
let k8sConfig: k8s.KubeConfig | null = null
/**
* Initialize Kubernetes client
*/
export function initK8sClient() {
if (k8sClient) return k8sClient
k8sConfig = new k8s.KubeConfig()
// Check if running in cluster
const inCluster = process.env.K8S_IN_CLUSTER === 'true'
if (inCluster) {
k8sConfig.loadFromCluster()
console.log('📦 Loaded K8s config from cluster')
// Skip TLS verification when in cluster
// This is needed because the cluster uses self-signed certificates
const cluster = k8sConfig.getCurrentCluster()
console.log('📦 Current cluster:', cluster)
if (cluster) {
cluster.skipTLSVerify = true
console.log('🔓 Set skipTLSVerify = true')
}
// Create custom HTTPS agent that ignores certificate errors
const httpsAgent = new https.Agent({
rejectUnauthorized: false
})
console.log('🔓 Created HTTPS agent with rejectUnauthorized: false')
// Apply custom agent to the config
try {
k8sConfig.applyToHTTPSOptions({
httpsAgent: httpsAgent
} as any)
console.log('✅ Applied custom HTTPS agent to K8s config')
} catch (applyError: any) {
console.error('❌ Failed to apply HTTPS options:', applyError.message)
}
} else {
// Load from kubeconfig file
const configPath = process.env.K8S_CONFIG_PATH || process.env.KUBECONFIG || '~/.kube/config'
k8sConfig.loadFromFile(configPath)
}
k8sClient = k8sConfig.makeApiClient(k8s.CoreV1Api)
return k8sClient
}
/**
* Get Kubernetes client
*/
export function getK8sClient(): k8s.CoreV1Api {
if (!k8sClient) {
return initK8sClient()
}
return k8sClient
}
/**
* Get Kubernetes client with custom request options
* This ensures the HTTPS agent is used for each request
*/
export function getK8sClientWithOptions(): { client: k8s.CoreV1Api, options: any } {
const client = getK8sClient()
// Create request options with custom HTTPS agent
const options = {
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
}
return { client, options }
}
/**
* Create pod spec for agent
*/
export function createAgentPodSpec(podName: string, userId: string): k8s.V1Pod {
return {
metadata: {
name: podName,
labels: {
app: 'claude-agent',
userId: userId,
'aiworker.io/agent': 'true',
},
},
spec: {
serviceAccountName: 'agent-sa',
imagePullSecrets: [
{
name: 'gitea-registry',
},
],
containers: [
{
name: 'agent',
image: 'git.fuq.tv/admin/aiworker-agent:latest',
imagePullPolicy: 'Always',
ports: [
{
containerPort: 7681,
name: 'terminal',
},
],
env: [
{
name: 'BACKEND_URL',
value: 'https://api.fuq.tv',
},
{
name: 'MCP_ENDPOINT',
value: 'https://api.fuq.tv/api/mcp',
},
{
name: 'GITEA_URL',
value: 'https://git.fuq.tv',
},
{
name: 'GITEA_TOKEN',
valueFrom: {
secretKeyRef: {
name: 'agent-secrets',
key: 'gitea-token',
},
},
},
{
name: 'POD_NAME',
valueFrom: {
fieldRef: {
fieldPath: 'metadata.name',
},
},
},
{
name: 'NAMESPACE',
valueFrom: {
fieldRef: {
fieldPath: 'metadata.namespace',
},
},
},
{
name: 'USER_ID',
value: userId,
},
],
resources: {
requests: {
cpu: '500m',
memory: '1Gi',
},
limits: {
cpu: '2000m',
memory: '4Gi',
},
},
volumeMounts: [
{
name: 'workspace',
mountPath: '/workspace',
},
],
},
],
volumes: [
{
name: 'workspace',
emptyDir: {},
},
],
},
}
}
/**
* Create agent pod in Kubernetes
*/
export async function createAgentPod(podName: string, userId: string): Promise<void> {
const { client, options } = getK8sClientWithOptions()
const podSpec = createAgentPodSpec(podName, userId)
console.log(`🔧 Creating pod ${podName} for user ${userId}`)
console.log(`🔧 Using custom HTTPS agent with rejectUnauthorized: false`)
try {
const result = await client.createNamespacedPod({
namespace: 'agents',
body: podSpec
}, undefined, undefined, undefined, undefined, options)
console.log(`✅ Pod ${podName} created successfully`)
if (result?.body?.metadata?.uid) {
console.log(`✅ Pod UID: ${result.body.metadata.uid}`)
}
} catch (error: any) {
console.error(`❌ Failed to create pod ${podName}`)
console.error(`❌ Error message:`, error.message)
console.error(`❌ Error code:`, error.code)
if (error.response) {
console.error(`❌ Response status:`, error.response.statusCode)
console.error(`❌ Response body:`, error.response.body)
}
throw error
}
}
/**
* Delete agent pod from Kubernetes
*/
export async function deleteAgentPod(podName: string): Promise<void> {
const client = getK8sClient()
try {
await client.deleteNamespacedPod({
name: podName,
namespace: 'agents'
})
console.log(`✅ Pod ${podName} deleted successfully`)
} catch (error: any) {
// Ignore 404 errors (pod already deleted)
if (error.statusCode === 404 || error.response?.statusCode === 404) {
console.log(`⚠️ Pod ${podName} not found (already deleted)`)
return
}
console.error(`❌ Failed to delete pod ${podName}:`, error.message)
throw error
}
}
/**
* Get pod status
*/
export async function getPodStatus(podName: string): Promise<string | null> {
const client = getK8sClient()
try {
const response = await client.readNamespacedPod({
name: podName,
namespace: 'agents'
})
return response.body.status?.phase || null
} catch (error: any) {
if (error.statusCode === 404 || error.response?.statusCode === 404) {
return null
}
throw error
}
}