diff --git a/undefined b/undefined new file mode 100644 index 0000000..d43107d --- /dev/null +++ b/undefined @@ -0,0 +1,172 @@ +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 = (( \ No newline at end of file