|
1 | 1 | import * as k8s from '@kubernetes/client-node'; |
2 | 2 | import { configService } from './config'; |
3 | | -import type { DeploymentStatus, PodStatus, ClusterStatus, PodPhase, DeploymentConfig, RuntimeStatus, ModelDeployment, GatewayInfo, GatewayModelInfo } from '@kubeairunway/shared'; |
| 3 | +import type { DeploymentStatus, PodStatus, ClusterStatus, PodPhase, DeploymentConfig, RuntimeStatus, ModelDeployment, GatewayInfo, GatewayModelInfo, GatewayCRDStatus } from '@kubeairunway/shared'; |
4 | 4 | import { toModelDeploymentManifest, toDeploymentStatus } from '@kubeairunway/shared'; |
5 | 5 | import { withRetry } from '../lib/retry'; |
6 | 6 | import logger from '../lib/logger'; |
@@ -1480,6 +1480,108 @@ class KubernetesService { |
1480 | 1480 |
|
1481 | 1481 | return models; |
1482 | 1482 | } |
| 1483 | + |
| 1484 | + /** |
| 1485 | + * Check Gateway API and GAIE CRD installation status. |
| 1486 | + * Also includes live gateway availability info. |
| 1487 | + */ |
| 1488 | + async checkGatewayCRDStatus(): Promise<GatewayCRDStatus> { |
| 1489 | + const { PINNED_GAIE_VERSION, GAIE_CRD_URL, GATEWAY_API_CRD_URL } = await import('@kubeairunway/shared'); |
| 1490 | + |
| 1491 | + const [gatewayApiInstalled, inferenceExtInstalled] = await Promise.all([ |
| 1492 | + this.checkCRDExists('gateways.gateway.networking.k8s.io'), |
| 1493 | + this.checkCRDExists('inferencepools.inference.networking.k8s.io'), |
| 1494 | + ]); |
| 1495 | + |
| 1496 | + // Get live gateway status |
| 1497 | + let gatewayAvailable = false; |
| 1498 | + let gatewayEndpoint: string | undefined; |
| 1499 | + if (gatewayApiInstalled && inferenceExtInstalled) { |
| 1500 | + try { |
| 1501 | + const gwStatus = await this.getGatewayStatus(); |
| 1502 | + gatewayAvailable = gwStatus.available; |
| 1503 | + gatewayEndpoint = gwStatus.endpoint; |
| 1504 | + } catch { |
| 1505 | + // Gateway status check failed, not critical |
| 1506 | + } |
| 1507 | + } |
| 1508 | + |
| 1509 | + const allInstalled = gatewayApiInstalled && inferenceExtInstalled; |
| 1510 | + let message: string; |
| 1511 | + if (allInstalled && gatewayAvailable) { |
| 1512 | + message = 'Gateway API and Inference Extension CRDs are installed. Gateway is available.'; |
| 1513 | + } else if (allInstalled) { |
| 1514 | + message = 'Gateway API and Inference Extension CRDs are installed. No active gateway detected.'; |
| 1515 | + } else if (!gatewayApiInstalled && !inferenceExtInstalled) { |
| 1516 | + message = 'Gateway API and Inference Extension CRDs are not installed.'; |
| 1517 | + } else if (!gatewayApiInstalled) { |
| 1518 | + message = 'Gateway API CRDs are not installed.'; |
| 1519 | + } else { |
| 1520 | + message = 'Inference Extension CRDs are not installed.'; |
| 1521 | + } |
| 1522 | + |
| 1523 | + return { |
| 1524 | + gatewayApiInstalled, |
| 1525 | + inferenceExtInstalled, |
| 1526 | + pinnedVersion: PINNED_GAIE_VERSION, |
| 1527 | + gatewayAvailable, |
| 1528 | + gatewayEndpoint, |
| 1529 | + message, |
| 1530 | + installCommands: [ |
| 1531 | + `kubectl apply -f ${GATEWAY_API_CRD_URL}`, |
| 1532 | + `kubectl apply -f ${GAIE_CRD_URL}`, |
| 1533 | + ], |
| 1534 | + }; |
| 1535 | + } |
| 1536 | + |
| 1537 | + /** |
| 1538 | + * Proxy a GET request to a Kubernetes service through the API server. |
| 1539 | + * This allows fetching service endpoints (e.g. /metrics) even when running off-cluster. |
| 1540 | + * Uses raw fetch instead of the generated client to support text/plain responses. |
| 1541 | + */ |
| 1542 | + async proxyServiceGet(serviceName: string, namespace: string, port: number, path: string): Promise<string> { |
| 1543 | + const cluster = this.kc.getCurrentCluster(); |
| 1544 | + if (!cluster) { |
| 1545 | + throw new Error('No active Kubernetes cluster configured'); |
| 1546 | + } |
| 1547 | + |
| 1548 | + // Build proxy URL: /api/v1/namespaces/{ns}/services/{name}:{port}/proxy/{path} |
| 1549 | + const proxyUrl = `${cluster.server}/api/v1/namespaces/${encodeURIComponent(namespace)}/services/${encodeURIComponent(serviceName)}:${port}/proxy/${path}`; |
| 1550 | + |
| 1551 | + // Extract auth headers from KubeConfig |
| 1552 | + const reqOpts: { headers: Record<string, string>; strictSSL?: boolean } = { headers: {} }; |
| 1553 | + await this.kc.applyToRequest(reqOpts as any); |
| 1554 | + |
| 1555 | + // Extract TLS options (CA cert, client cert/key) from KubeConfig |
| 1556 | + const httpsOpts: { ca?: Buffer; cert?: Buffer; key?: Buffer; rejectUnauthorized?: boolean } = {}; |
| 1557 | + this.kc.applyToHTTPSOptions(httpsOpts as any); |
| 1558 | + |
| 1559 | + const tlsOpts: Record<string, any> = {}; |
| 1560 | + if (httpsOpts.ca) tlsOpts.ca = httpsOpts.ca; |
| 1561 | + if (httpsOpts.cert) tlsOpts.cert = httpsOpts.cert; |
| 1562 | + if (httpsOpts.key) tlsOpts.key = httpsOpts.key; |
| 1563 | + if (cluster.skipTLSVerify || httpsOpts.rejectUnauthorized === false) { |
| 1564 | + tlsOpts.rejectUnauthorized = false; |
| 1565 | + } |
| 1566 | + |
| 1567 | + const fetchOpts: RequestInit & { tls?: Record<string, any> } = { |
| 1568 | + method: 'GET', |
| 1569 | + headers: { |
| 1570 | + ...reqOpts.headers, |
| 1571 | + 'Accept': 'text/plain', |
| 1572 | + }, |
| 1573 | + }; |
| 1574 | + |
| 1575 | + if (Object.keys(tlsOpts).length > 0) { |
| 1576 | + fetchOpts.tls = tlsOpts; |
| 1577 | + } |
| 1578 | + |
| 1579 | + const response = await fetch(proxyUrl, fetchOpts); |
| 1580 | + if (!response.ok) { |
| 1581 | + throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
| 1582 | + } |
| 1583 | + return await response.text(); |
| 1584 | + } |
1483 | 1585 | } |
1484 | 1586 |
|
1485 | 1587 | export const kubernetesService = new KubernetesService(); |
0 commit comments