diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 04b400d..8d29241 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -51,7 +51,7 @@ spec: - name: k8s-pilot # Replace with your Gitea registry path: # image: git.it-microevo.com//k8s-pilot:latest - image: k8s-pilot:latest + image: gitea.it-microevo.com/dev/k8s-pilot:latest imagePullPolicy: Always ports: - containerPort: 3000 diff --git a/k8s/rbac.yaml b/k8s/rbac.yaml index e5bb15a..43de876 100644 --- a/k8s/rbac.yaml +++ b/k8s/rbac.yaml @@ -23,6 +23,9 @@ rules: - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] - apiGroups: ["apps"] resources: ["deployments", "replicasets"] verbs: ["get", "list", "watch", "patch", "update"] diff --git a/src/public/index.html b/src/public/index.html index ed02481..3da3b27 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -5,662 +5,897 @@ K8s Pilot - +
- diff --git a/src/server.js b/src/server.js index 25a0b69..8517d8c 100644 --- a/src/server.js +++ b/src/server.js @@ -4,6 +4,9 @@ const { WebSocketServer } = require('ws'); const http = require('http'); const path = require('path'); const { PassThrough } = require('stream'); +const zlib = require('zlib'); +const { promisify } = require('util'); +const gunzip = promisify(zlib.gunzip); const fetch = require('node-fetch'); const app = express(); @@ -361,6 +364,70 @@ app.get('/api/vulnerabilities', async (req, res) => { } }); +// Helm releases (reads helm.sh/release.v1 secrets) +app.get('/api/helm/releases', async (req, res) => { + try { + const result = await coreApi.listSecretForAllNamespaces(); + const helmSecrets = result.body.items.filter(s => s.type === 'helm.sh/release.v1'); + + // Keep only latest revision per release + const latestMap = {}; + for (const s of helmSecrets) { + const key = `${s.metadata.namespace}/${s.metadata.labels?.name}`; + const v = parseInt(s.metadata.labels?.version || '0'); + if (!latestMap[key] || v > latestMap[key].v) latestMap[key] = { s, v }; + } + + const releases = []; + for (const { s } of Object.values(latestMap)) { + try { + const compressed = Buffer.from(s.data.release, 'base64'); + const json = await gunzip(compressed); + const r = JSON.parse(json.toString()); + releases.push({ + name: r.name, + namespace: r.namespace, + status: r.info?.status, + chartName: r.chart?.metadata?.name, + chartVersion: r.chart?.metadata?.version, + appVersion: r.chart?.metadata?.appVersion, + description: r.chart?.metadata?.description, + deployedAt: r.info?.last_deployed, + revision: parseInt(s.metadata.labels?.version || '1'), + }); + } catch (e) { console.error('[helm parse]', e.message); } + } + + res.json(releases.sort((a, b) => a.name.localeCompare(b.name))); + } catch (err) { + console.error('[helm]', err.message); + res.status(500).json({ error: err.message }); + } +}); + +// Check Artifact Hub for latest chart version +app.get('/api/helm/check-update/:chart', async (req, res) => { + const { chart } = req.params; + try { + const response = await fetch( + `https://artifacthub.io/api/v1/packages/search?ts_query_web=${encodeURIComponent(chart)}&kind=0&limit=10` + ); + const data = await response.json(); + const packages = data.packages || []; + const match = packages.find(p => p.name === chart) || packages[0]; + if (!match) return res.json({ found: false }); + res.json({ + found: true, + latestVersion: match.version, + appVersion: match.app_version, + repoName: match.repository?.name, + artifactHubUrl: `https://artifacthub.io/packages/helm/${match.repository?.name}/${match.name}`, + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // ── SSE: Live log stream ────────────────────────────────────────────────────── app.get('/api/logs/:namespace/:pod/:container', async (req, res) => { const { namespace, pod, container } = req.params;