Files
k3s-gitops/undefined
2026-03-01 17:10:56 +00:00

172 lines
5.7 KiB
Plaintext

const express = require('express');
const axios = require('axios');
const https = require('https');
const app = express();
const port = process.env.PORT || 3000;
const ARGOCD_URL = (process.env.ARGOCD_URL || 'https://argocd.thedevops.dev').replace(/\/$/, '');
const ARGOCD_TOKEN = process.env.ARGOCD_TOKEN || '';
// Allow self-signed certs in homelab
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
const argoApi = axios.create({
baseURL: `${ARGOCD_URL}/api/v1`,
httpsAgent,
headers: ARGOCD_TOKEN ? { Authorization: `Bearer ${ARGOCD_TOKEN}` } : {}
});
app.use(express.json());
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});
app.get('/health', (req, res) => res.json({ status: 'healthy', service: 'mcp-argocd' }));
app.get('/', (req, res) => res.json({
service: 'MCP ArgoCD Server',
version: '1.0.0',
argocd: ARGOCD_URL,
endpoints: [
'POST /api/apps/list - List all applications with sync/health status',
'POST /api/apps/get - Get detailed app info (name required)',
'POST /api/apps/sync - Trigger sync for an app (name required)',
'POST /api/apps/out_of_sync - List all out-of-sync applications',
'POST /api/apps/unhealthy - List all unhealthy applications',
'POST /api/apps/history - Get deployment history for an app',
'POST /api/apps/rollback - Rollback app to previous revision',
'POST /api/clusters/list - List connected clusters',
'POST /api/repos/list - List connected repositories',
]
}));
// List all apps
app.post('/api/apps/list', async (req, res) => {
try {
const r = await argoApi.get('/applications');
const apps = (r.data.items || []).map(a => ({
name: a.metadata.name,
namespace: a.spec.destination?.namespace,
project: a.spec.project,
syncStatus: a.status?.sync?.status,
healthStatus: a.status?.health?.status,
repo: a.spec.source?.repoURL,
path: a.spec.source?.path,
targetRevision: a.spec.source?.targetRevision,
lastSyncedAt: a.status?.operationState?.finishedAt,
}));
res.json({ count: apps.length, apps });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// Get app details
app.post('/api/apps/get', async (req, res) => {
try {
const { name } = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
const r = await argoApi.get(`/applications/${name}`);
const a = r.data;
res.json({
name: a.metadata.name,
project: a.spec.project,
source: a.spec.source,
destination: a.spec.destination,
syncPolicy: a.spec.syncPolicy,
syncStatus: a.status?.sync,
healthStatus: a.status?.health,
conditions: a.status?.conditions,
resources: (a.status?.resources || []).map(r => ({
kind: r.kind,
name: r.name,
namespace: r.namespace,
status: r.status,
health: r.health?.status
}))
});
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// Sync app
app.post('/api/apps/sync', async (req, res) => {
try {
const { name, prune, force } = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
const body = { prune: prune || false };
if (force) body.strategy = { apply: { force: true } };
const r = await argoApi.post(`/applications/${name}/sync`, body);
res.json({
message: `Sync triggered for ${name}`,
phase: r.data.status?.operationState?.phase,
startedAt: r.data.status?.operationState?.startedAt
});
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// Out-of-sync apps
app.post('/api/apps/out_of_sync', async (req, res) => {
try {
const r = await argoApi.get('/applications');
const apps = (r.data.items || [])
.filter(a => a.status?.sync?.status !== 'Synced')
.map(a => ({
name: a.metadata.name,
syncStatus: a.status?.sync?.status,
healthStatus: a.status?.health?.status,
namespace: a.spec.destination?.namespace,
lastSyncedAt: a.status?.operationState?.finishedAt
}));
res.json({ count: apps.length, apps });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// Unhealthy apps
app.post('/api/apps/unhealthy', async (req, res) => {
try {
const r = await argoApi.get('/applications');
const apps = (r.data.items || [])
.filter(a => !['Healthy', 'Progressing'].includes(a.status?.health?.status))
.map(a => ({
name: a.metadata.name,
healthStatus: a.status?.health?.status,
healthMessage: a.status?.health?.message,
syncStatus: a.status?.sync?.status,
namespace: a.spec.destination?.namespace,
conditions: a.status?.conditions
}));
res.json({ count: apps.length, apps });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// Deployment history
app.post('/api/apps/history', async (req, res) => {
try {
const { name, limit } = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
const r = await argoApi.get(`/applications/${name}/revisions`);
const history = ((r.data.history || []).slice(-(limit || 10))).reverse().map(h => ({
id: h.id,
revision: h.revision,
deployedAt: h.deployedAt,
initiatedBy: h.initiatedBy
}));
res.json({ app: name, count: history.length, history });
} catch (e) {
// history endpoint may differ by ArgoCD version
try {
const r2 = await argoApi.get(`/applications/${name}`);
const history = ((