#!/usr/bin/env node /** * AWOOOI 自動化 QA - K8s Executor 端對端測試 * ========================================== * Phase 3: 驗證 K8s 執行器實際運作 * * 測試流程: * 1. 檢查 K8s 叢集連線 * 2. 創建/驗證 sandbox namespace * 3. 部署測試 Pod * 4. 發射告警 → 創建 Approval * 5. 模擬簽核 (Multi-Sig) * 6. 驗證 Pod 被實際重啟 (AGE 歸零) * * 用法: node scripts/test-k8s-executor.js */ const http = require('http') const { execSync } = require('child_process') const API_URL = 'http://localhost:8000' const NAMESPACE = 'awoooi-sandbox' const TEST_POD_NAME = 'qa-test-pod' const KUBECONFIG = '/Users/ogt/awoooi/apps/api/k3s-prod.yaml' // ANSI Colors const GREEN = '\x1b[32m' const RED = '\x1b[31m' const YELLOW = '\x1b[33m' const CYAN = '\x1b[36m' const RESET = '\x1b[0m' function log(status, message) { const icon = status === 'pass' ? `${GREEN}✓${RESET}` : status === 'fail' ? `${RED}✗${RESET}` : status === 'info' ? `${CYAN}→${RESET}` : `${YELLOW}?${RESET}` console.log(`${icon} ${message}`) } function kubectl(cmd) { try { return execSync(`KUBECONFIG=${KUBECONFIG} kubectl ${cmd}`, { encoding: 'utf-8', timeout: 30000 }).trim() } catch (err) { return null } } function httpRequest(method, path, body = null) { return new Promise((resolve, reject) => { const url = new URL(path, API_URL) const options = { hostname: url.hostname, port: url.port, path: url.pathname, method, headers: { 'Content-Type': 'application/json' }, } const req = http.request(options, (res) => { let data = '' res.on('data', (chunk) => (data += chunk)) res.on('end', () => { try { resolve({ status: res.statusCode, data: JSON.parse(data) }) } catch (e) { resolve({ status: res.statusCode, data: data }) } }) }) req.on('error', reject) if (body) req.write(JSON.stringify(body)) req.end() }) } async function main() { console.log('\n' + '═'.repeat(55)) console.log(' AWOOOI Phase 3 QA - K8s Executor 端對端測試') console.log('═'.repeat(55) + '\n') // Step 1: Check K8s cluster connection log('info', '步驟 1: 檢查 K8s 叢集連線...') const version = kubectl('version --client -o json') if (version) { log('pass', 'kubectl 可用') } else { log('fail', 'kubectl 不可用,請確認 PATH') process.exit(1) } const clusterInfo = kubectl('cluster-info 2>/dev/null') if (clusterInfo && clusterInfo.includes('running')) { log('pass', 'K8s 叢集連線正常') } else { log('fail', 'K8s 叢集連線失敗') process.exit(1) } // Step 2: Create sandbox namespace log('info', `步驟 2: 創建 ${NAMESPACE} namespace...`) const nsExists = kubectl(`get namespace ${NAMESPACE} 2>/dev/null`) if (!nsExists) { kubectl(`create namespace ${NAMESPACE}`) log('pass', `Namespace ${NAMESPACE} 已創建`) } else { log('info', `Namespace ${NAMESPACE} 已存在`) } // Step 3: Deploy test pod log('info', '步驟 3: 部署測試 Pod...') const podManifest = ` apiVersion: v1 kind: Pod metadata: name: ${TEST_POD_NAME} namespace: ${NAMESPACE} labels: app: qa-test spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 ` // Delete existing pod first kubectl(`delete pod ${TEST_POD_NAME} -n ${NAMESPACE} --ignore-not-found=true`) // Create new pod try { execSync(`echo '${podManifest}' | KUBECONFIG=${KUBECONFIG} kubectl apply -f -`, { encoding: 'utf-8' }) log('pass', `Pod ${TEST_POD_NAME} 已部署`) } catch (err) { log('fail', `Pod 部署失敗: ${err.message}`) process.exit(1) } // Wait for pod to be ready log('info', '等待 Pod 就緒...') for (let i = 0; i < 30; i++) { const status = kubectl(`get pod ${TEST_POD_NAME} -n ${NAMESPACE} -o jsonpath='{.status.phase}'`) if (status === 'Running') { log('pass', 'Pod 狀態: Running') break } await new Promise(r => setTimeout(r, 1000)) } // Record initial pod creation time const initialCreationTime = kubectl(`get pod ${TEST_POD_NAME} -n ${NAMESPACE} -o jsonpath='{.metadata.creationTimestamp}'`) log('info', `初始創建時間: ${initialCreationTime}`) // Step 4: Fire custom alert targeting our test pod log('info', '步驟 4: 發射測試告警...') try { const alertPayload = { alert_type: 'k8s_pod_crash', severity: 'critical', source: 'awoooi-qa-test', target_resource: TEST_POD_NAME, namespace: NAMESPACE, message: `[QA TEST] Pod ${TEST_POD_NAME} CrashLoopBackOff - needs restart`, metrics: { restart_count: 5, cpu_percent: 95, test: true } } const { status, data } = await httpRequest('POST', '/api/v1/webhooks/alerts', alertPayload) if (status === 200 || status === 201) { log('pass', `告警已發射,Approval ID: ${data.approval_id?.slice(0, 8) || data.id?.slice(0, 8)}...`) } else { log('fail', `告警發射失敗: ${JSON.stringify(data)}`) } } catch (err) { log('fail', `告警發射失敗: ${err.message}`) } // Step 5: Get approval and sign it log('info', '步驟 5: 取得並簽核 Approval...') await new Promise(r => setTimeout(r, 1000)) // Wait for approval to be created let approvalId = null try { const { data } = await httpRequest('GET', '/api/v1/approvals/pending') if (data.approvals && data.approvals.length > 0) { approvalId = data.approvals[0].id log('info', `找到 Approval: ${approvalId.slice(0, 8)}...`) // Get required signatures const requiredSigs = data.approvals[0].required_signatures || 2 // Sign with multiple users for (let i = 1; i <= requiredSigs; i++) { const signResult = await httpRequest('POST', `/api/v1/approvals/${approvalId}/sign`, { signer_id: `qa-signer-${i}`, signer_name: `QA Signer ${i}`, comment: `K8s executor test signature ${i}`, }) if (signResult.status === 200) { log('pass', `簽核 ${i}/${requiredSigs} 成功`) } else if (signResult.status === 409) { log('info', `簽核 ${i} 跳過 (重複)`) } } } else { log('fail', '無待簽核項目') } } catch (err) { log('fail', `簽核失敗: ${err.message}`) } // Step 6: Wait and verify pod was restarted log('info', '步驟 6: 等待 K8s 執行器執行...') await new Promise(r => setTimeout(r, 5000)) // Give executor time to run const finalCreationTime = kubectl(`get pod ${TEST_POD_NAME} -n ${NAMESPACE} -o jsonpath='{.metadata.creationTimestamp}' 2>/dev/null`) console.log('\n' + '─'.repeat(55)) console.log(' 驗證結果') console.log('─'.repeat(55)) if (!finalCreationTime) { log('info', 'Pod 已被刪除 (executor 執行 delete pod)') log('pass', '✨ K8s Executor 執行成功!') } else if (finalCreationTime !== initialCreationTime) { log('pass', `Pod 創建時間已更新: ${finalCreationTime}`) log('pass', '✨ K8s Executor 執行成功!(Pod 重啟)') } else { log('info', `Pod 創建時間未變: ${finalCreationTime}`) log('info', '可能原因: 執行器正在處理或 kubeconfig 權限問題') // Check API logs for execution status log('info', '檢查後端日誌...') } // Cleanup log('info', '清理測試資源...') kubectl(`delete pod ${TEST_POD_NAME} -n ${NAMESPACE} --ignore-not-found=true`) console.log('\n' + '═'.repeat(55)) console.log(' Phase 3 K8s Executor 測試完成') console.log('═'.repeat(55) + '\n') } main().catch((err) => { log('fail', `測試失敗: ${err.message}`) process.exit(1) })