All checks were successful
Build and Push Backend / build (push) Successful in 6s
- Add debug logging to getPodIP - Handle both response.body and direct response - Apply same fix to getPodStatus for consistency - Fixes 500 error when accessing agent terminal
298 lines
7.5 KiB
TypeScript
298 lines
7.5 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'
|
|
})
|
|
|
|
// Handle different response structures
|
|
const pod = response.body || response
|
|
return pod?.status?.phase || null
|
|
} catch (error: any) {
|
|
if (error.statusCode === 404 || error.response?.statusCode === 404) {
|
|
return null
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get pod IP address
|
|
*/
|
|
export async function getPodIP(podName: string): Promise<string | null> {
|
|
const client = getK8sClient()
|
|
|
|
try {
|
|
console.log(`🔍 Getting IP for pod: ${podName}`)
|
|
const response = await client.readNamespacedPod({
|
|
name: podName,
|
|
namespace: 'agents'
|
|
})
|
|
|
|
console.log(`🔍 Response type: ${typeof response}`)
|
|
console.log(`🔍 Has body: ${'body' in response}`)
|
|
|
|
// Handle different response structures
|
|
const pod = response.body || response
|
|
const podIP = pod?.status?.podIP
|
|
|
|
console.log(`🔍 Pod IP: ${podIP}`)
|
|
return podIP || null
|
|
} catch (error: any) {
|
|
console.error(`❌ Error getting pod IP for ${podName}:`, error.message)
|
|
if (error.statusCode === 404 || error.response?.statusCode === 404) {
|
|
return null
|
|
}
|
|
throw error
|
|
}
|
|
}
|