395 lines
12 KiB
JavaScript
395 lines
12 KiB
JavaScript
const express = require('express');
|
|
const k8s = require('@kubernetes/client-node');
|
|
|
|
const app = express();
|
|
const port = process.env.PORT || 3000;
|
|
|
|
// Kubernetes client setup
|
|
const kc = new k8s.KubeConfig();
|
|
kc.loadFromDefault();
|
|
|
|
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
|
|
const k8sAppsApi = kc.makeApiClient(k8s.AppsV1Api);
|
|
const k8sBatchApi = kc.makeApiClient(k8s.BatchV1Api);
|
|
|
|
app.use(express.json());
|
|
|
|
// CORS middleware
|
|
app.use((req, res, next) => {
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
if (req.method === 'OPTIONS') {
|
|
return res.sendStatus(200);
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Health check
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
service: 'mcp-kubernetes',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// Root endpoint
|
|
app.get('/', (req, res) => {
|
|
res.json({
|
|
service: 'MCP Kubernetes Server',
|
|
version: '1.0.0',
|
|
endpoints: {
|
|
health: 'GET /health',
|
|
pods: {
|
|
list: 'POST /api/pods/list',
|
|
get: 'POST /api/pods/get',
|
|
logs: 'POST /api/pods/logs',
|
|
delete: 'POST /api/pods/delete'
|
|
},
|
|
deployments: {
|
|
list: 'POST /api/deployments/list',
|
|
get: 'POST /api/deployments/get',
|
|
scale: 'POST /api/deployments/scale'
|
|
},
|
|
services: {
|
|
list: 'POST /api/services/list',
|
|
get: 'POST /api/services/get'
|
|
},
|
|
namespaces: {
|
|
list: 'POST /api/namespaces/list'
|
|
},
|
|
nodes: {
|
|
list: 'POST /api/nodes/list'
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// PODS
|
|
// ============================================================================
|
|
|
|
// List pods
|
|
app.post('/api/pods/list', async (req, res) => {
|
|
try {
|
|
const namespace = req.body.namespace || 'default';
|
|
const labelSelector = req.body.labelSelector || '';
|
|
|
|
const response = await k8sApi.listNamespacedPod(
|
|
namespace,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
labelSelector
|
|
);
|
|
|
|
const pods = response.body.items.map(pod => ({
|
|
name: pod.metadata.name,
|
|
namespace: pod.metadata.namespace,
|
|
status: pod.status.phase,
|
|
ready: pod.status.containerStatuses ?
|
|
`${pod.status.containerStatuses.filter(c => c.ready).length}/${pod.status.containerStatuses.length}` :
|
|
'0/0',
|
|
restarts: pod.status.containerStatuses ?
|
|
pod.status.containerStatuses.reduce((sum, c) => sum + c.restartCount, 0) :
|
|
0,
|
|
age: pod.metadata.creationTimestamp,
|
|
node: pod.spec.nodeName,
|
|
ip: pod.status.podIP
|
|
}));
|
|
|
|
res.json({ count: pods.length, pods });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// Get pod details
|
|
app.post('/api/pods/get', async (req, res) => {
|
|
try {
|
|
const { name, namespace } = req.body;
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Pod name is required' });
|
|
}
|
|
|
|
const response = await k8sApi.readNamespacedPod(
|
|
name,
|
|
namespace || 'default'
|
|
);
|
|
res.json(response.body);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// Get pod logs
|
|
app.post('/api/pods/logs', async (req, res) => {
|
|
try {
|
|
const { name, namespace, container, tailLines, since, timestamps } = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Pod name is required' });
|
|
}
|
|
|
|
const response = await k8sApi.readNamespacedPodLog(
|
|
name,
|
|
namespace || 'default',
|
|
container,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
since,
|
|
tailLines || 100,
|
|
timestamps || false
|
|
);
|
|
|
|
res.json({
|
|
pod: name,
|
|
namespace: namespace || 'default',
|
|
container: container || 'default',
|
|
logs: response.body
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// Delete pod
|
|
app.post('/api/pods/delete', async (req, res) => {
|
|
try {
|
|
const { name, namespace } = req.body;
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Pod name is required' });
|
|
}
|
|
|
|
const response = await k8sApi.deleteNamespacedPod(
|
|
name,
|
|
namespace || 'default'
|
|
);
|
|
res.json({
|
|
message: 'Pod deleted successfully',
|
|
pod: name,
|
|
namespace: namespace || 'default'
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// DEPLOYMENTS
|
|
// ============================================================================
|
|
|
|
// List deployments
|
|
app.post('/api/deployments/list', async (req, res) => {
|
|
try {
|
|
const namespace = req.body.namespace || 'default';
|
|
const response = await k8sAppsApi.listNamespacedDeployment(namespace);
|
|
|
|
const deployments = response.body.items.map(dep => ({
|
|
name: dep.metadata.name,
|
|
namespace: dep.metadata.namespace,
|
|
replicas: `${dep.status.readyReplicas || 0}/${dep.spec.replicas}`,
|
|
available: dep.status.availableReplicas || 0,
|
|
age: dep.metadata.creationTimestamp,
|
|
image: dep.spec.template.spec.containers[0].image
|
|
}));
|
|
|
|
res.json({ count: deployments.length, deployments });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// Get deployment
|
|
app.post('/api/deployments/get', async (req, res) => {
|
|
try {
|
|
const { name, namespace } = req.body;
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Deployment name is required' });
|
|
}
|
|
|
|
const response = await k8sAppsApi.readNamespacedDeployment(
|
|
name,
|
|
namespace || 'default'
|
|
);
|
|
res.json(response.body);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// Scale deployment
|
|
app.post('/api/deployments/scale', async (req, res) => {
|
|
try {
|
|
const { name, namespace, replicas } = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Deployment name is required' });
|
|
}
|
|
if (replicas === undefined) {
|
|
return res.status(400).json({ error: 'Replicas count is required' });
|
|
}
|
|
|
|
const patch = {
|
|
spec: {
|
|
replicas: parseInt(replicas)
|
|
}
|
|
};
|
|
|
|
const options = { headers: { 'Content-Type': 'application/strategic-merge-patch+json' } };
|
|
const response = await k8sAppsApi.patchNamespacedDeployment(
|
|
name,
|
|
namespace || 'default',
|
|
patch,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
options
|
|
);
|
|
|
|
res.json({
|
|
message: 'Deployment scaled successfully',
|
|
deployment: name,
|
|
namespace: namespace || 'default',
|
|
replicas: replicas
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// SERVICES
|
|
// ============================================================================
|
|
|
|
// List services
|
|
app.post('/api/services/list', async (req, res) => {
|
|
try {
|
|
const namespace = req.body.namespace || 'default';
|
|
const response = await k8sApi.listNamespacedService(namespace);
|
|
|
|
const services = response.body.items.map(svc => ({
|
|
name: svc.metadata.name,
|
|
namespace: svc.metadata.namespace,
|
|
type: svc.spec.type,
|
|
clusterIP: svc.spec.clusterIP,
|
|
externalIP: svc.status.loadBalancer?.ingress?.[0]?.ip || 'none',
|
|
ports: svc.spec.ports?.map(p => `${p.port}:${p.targetPort}/${p.protocol}`).join(', '),
|
|
age: svc.metadata.creationTimestamp
|
|
}));
|
|
|
|
res.json({ count: services.length, services });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// Get service
|
|
app.post('/api/services/get', async (req, res) => {
|
|
try {
|
|
const { name, namespace } = req.body;
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Service name is required' });
|
|
}
|
|
|
|
const response = await k8sApi.readNamespacedService(
|
|
name,
|
|
namespace || 'default'
|
|
);
|
|
res.json(response.body);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// NAMESPACES
|
|
// ============================================================================
|
|
|
|
// List namespaces
|
|
app.post('/api/namespaces/list', async (req, res) => {
|
|
try {
|
|
const response = await k8sApi.listNamespace();
|
|
|
|
const namespaces = response.body.items.map(ns => ({
|
|
name: ns.metadata.name,
|
|
status: ns.status.phase,
|
|
age: ns.metadata.creationTimestamp
|
|
}));
|
|
|
|
res.json({ count: namespaces.length, namespaces });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// NODES
|
|
// ============================================================================
|
|
|
|
// List nodes
|
|
app.post('/api/nodes/list', async (req, res) => {
|
|
try {
|
|
const response = await k8sApi.listNode();
|
|
|
|
const nodes = response.body.items.map(node => ({
|
|
name: node.metadata.name,
|
|
status: node.status.conditions?.find(c => c.type === 'Ready')?.status === 'True' ? 'Ready' : 'NotReady',
|
|
roles: node.metadata.labels?.['node-role.kubernetes.io/master'] ? 'master' : 'worker',
|
|
age: node.metadata.creationTimestamp,
|
|
version: node.status.nodeInfo.kubeletVersion,
|
|
internalIP: node.status.addresses?.find(a => a.type === 'InternalIP')?.address,
|
|
os: `${node.status.nodeInfo.osImage}`,
|
|
kernel: node.status.nodeInfo.kernelVersion,
|
|
containerRuntime: node.status.nodeInfo.containerRuntimeVersion
|
|
}));
|
|
|
|
res.json({ count: nodes.length, nodes });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message, details: error.body });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING
|
|
// ============================================================================
|
|
|
|
// 404 handler
|
|
app.use((req, res) => {
|
|
res.status(404).json({
|
|
error: 'Endpoint not found',
|
|
message: `${req.method} ${req.path} is not a valid endpoint`,
|
|
availableEndpoints: 'GET / for list of available endpoints'
|
|
});
|
|
});
|
|
|
|
// Error handler
|
|
app.use((err, req, res, next) => {
|
|
console.error('Error:', err);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: err.message
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// START SERVER
|
|
// ============================================================================
|
|
|
|
app.listen(port, '0.0.0.0', () => {
|
|
console.log(`╔═══════════════════════════════════════════════════════╗`);
|
|
console.log(`║ ║`);
|
|
console.log(`║ MCP Kubernetes Server ║`);
|
|
console.log(`║ Running on port ${port} ║`);
|
|
console.log(`║ ║`);
|
|
console.log(`╚═══════════════════════════════════════════════════════╝`);
|
|
console.log(`\nEndpoints available at http://0.0.0.0:${port}`);
|
|
console.log(`Health check: http://0.0.0.0:${port}/health`);
|
|
});
|