kubeops 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +96 -35
  2. package/electron/main.js +5 -0
  3. package/package.json +3 -1
  4. package/server.ts +19 -0
  5. package/src/app/api/clusters/[clusterId]/helm/install/route.ts +65 -0
  6. package/src/app/api/clusters/[clusterId]/helm/releases/[name]/history/route.ts +44 -0
  7. package/src/app/api/clusters/[clusterId]/helm/releases/[name]/rollback/route.ts +47 -0
  8. package/src/app/api/clusters/[clusterId]/helm/releases/[name]/route.ts +71 -0
  9. package/src/app/api/clusters/[clusterId]/helm/releases/[name]/upgrade/route.ts +64 -0
  10. package/src/app/api/clusters/[clusterId]/helm/releases/[name]/values/route.ts +51 -0
  11. package/src/app/api/clusters/[clusterId]/helm/releases/route.ts +39 -0
  12. package/src/app/api/clusters/[clusterId]/helm/repos/route.ts +70 -0
  13. package/src/app/api/clusters/[clusterId]/helm/search/route.ts +41 -0
  14. package/src/app/api/clusters/[clusterId]/rbac/access-review/route.ts +62 -0
  15. package/src/app/api/clusters/[clusterId]/rbac/summary/route.ts +125 -0
  16. package/src/app/api/port-forward/route.ts +9 -1
  17. package/src/app/clusters/[clusterId]/helm/[releaseName]/page.tsx +346 -0
  18. package/src/app/clusters/[clusterId]/helm/page.tsx +110 -0
  19. package/src/app/clusters/[clusterId]/page.tsx +12 -8
  20. package/src/app/clusters/[clusterId]/port-forwarding/page.tsx +1 -1
  21. package/src/app/clusters/[clusterId]/rbac/page.tsx +65 -0
  22. package/src/app/clusters/page.tsx +228 -112
  23. package/src/app/globals.css +3 -3
  24. package/src/app/layout.tsx +2 -0
  25. package/src/components/clusters/cluster-card.tsx +106 -0
  26. package/src/components/clusters/cluster-tag-editor.tsx +109 -0
  27. package/src/components/helm/helm-install-dialog.tsx +277 -0
  28. package/src/components/helm/helm-rollback-dialog.tsx +86 -0
  29. package/src/components/helm/helm-upgrade-dialog.tsx +168 -0
  30. package/src/components/layout/electron-class.tsx +12 -0
  31. package/src/components/layout/header.tsx +4 -4
  32. package/src/components/namespaces/namespace-selector.tsx +48 -32
  33. package/src/components/panel/terminal-tab.tsx +34 -1
  34. package/src/components/rbac/access-review-form.tsx +240 -0
  35. package/src/components/rbac/rbac-summary.tsx +306 -0
  36. package/src/components/resources/resource-columns.tsx +52 -0
  37. package/src/components/shared/status-badge.tsx +8 -0
  38. package/src/components/shared/yaml-diff-view.tsx +117 -0
  39. package/src/components/shared/yaml-editor.tsx +30 -3
  40. package/src/hooks/use-clusters-filtering.ts +79 -0
  41. package/src/hooks/use-helm-release-detail.ts +9 -0
  42. package/src/hooks/use-helm-releases.ts +13 -0
  43. package/src/hooks/use-rbac-summary.ts +8 -0
  44. package/src/hooks/use-resource-list.ts +1 -1
  45. package/src/hooks/use-resource-tree.ts +36 -2
  46. package/src/lib/cluster-names.ts +10 -0
  47. package/src/lib/constants.ts +11 -0
  48. package/src/lib/helm/helm-runner.ts +90 -0
  49. package/src/lib/helm/helpers.ts +56 -0
  50. package/src/lib/k8s/client-factory.ts +4 -0
  51. package/src/lib/k8s/error-handling.ts +13 -0
  52. package/src/providers/swr-provider.tsx +19 -1
  53. package/src/stores/cluster-catalog-store.ts +112 -0
  54. package/src/types/helm.ts +53 -0
  55. package/src/types/rbac.ts +42 -0
package/README.md CHANGED
@@ -33,18 +33,45 @@ Download the latest version for your platform from the **[Releases](https://gith
33
33
 
34
34
  > The app supports auto-update after installation.
35
35
 
36
+ ### Requirements
37
+
38
+ - **kubectl** installed and available on your `PATH`
39
+ - A valid `~/.kube/config` with at least one cluster context
40
+ - *(Optional)* **helm** CLI for Helm chart management
41
+ - *(Optional)* `metrics-server` in the cluster for CPU/memory charts
42
+ - *(Optional)* Prometheus for network I/O and filesystem charts
43
+ - *(Optional)* `tsh` CLI for Teleport-managed clusters
44
+
36
45
  ---
37
46
 
38
47
  ## Why KubeOps?
39
48
 
40
- - **Zero config** Reads your `~/.kube/config` and auto-detects every cluster. No setup, no YAML to write.
41
- - **Visual topology** — See how Ingresses, Services, Deployments, and Pods connect in an interactive App Map.
42
- - **Built-in terminal & logs** Open shell sessions and live log streams right inside the app. No more switching between terminal tabs.
43
- - **Port forwarding dashboard** Start, monitor, and stop port forwards from a single page.
44
- - **Real-time metrics** CPU, memory, network I/O, and filesystem charts per pod (with metrics-server / Prometheus).
45
- - **ArgoCD-style status** Health badges on every resource so you can spot problems at a glance.
46
- - **Fast keyboard navigation** Command palette (`Cmd+K`) to jump to any cluster, namespace, or resource type instantly.
47
- - **Cross-platform** Runs natively on macOS, Windows, and Linux.
49
+ There are many Kubernetes tools out there. Here's how KubeOps compares:
50
+
51
+ | | KubeOps | Lens | k9s | K8s Dashboard |
52
+ | --- | :---: | :---: | :---: | :---: |
53
+ | Free & open source | **Yes** | Freemium | Yes | Yes (archived) |
54
+ | Visual resource topology | **Yes** | Extension | No | Limited |
55
+ | Built-in terminal & logs | **Yes** | Yes | Yes | Yes |
56
+ | Real-time metrics & charts | **Yes** | Yes | Basic | Basic |
57
+ | Helm chart management | **Yes** | Yes | View only | No |
58
+ | RBAC visualization | **Yes** | No | Basic | No |
59
+ | YAML diff view | **Yes** | No | No | No |
60
+ | Desktop app (no server install) | **Yes** | Yes | Terminal | Needs deploy |
61
+ | Zero config (reads kubeconfig) | **Yes** | Yes | Yes | Needs deploy |
62
+ | CRD browser | **Yes** | Yes | Yes | Limited |
63
+ | Teleport auth | **Yes** | No | No | No |
64
+
65
+ ### In short
66
+
67
+ - **Free forever** — No subscriptions, no feature gates, no telemetry. MIT licensed.
68
+ - **Visual-first** — Interactive App Map shows how Ingresses, Services, Deployments, and Pods connect. Not just resource lists.
69
+ - **All-in-one desktop app** — Terminal, logs, port forwarding, metrics charts, YAML editor, Helm management in a single window. No server to deploy, no browser extension to install.
70
+ - **Helm chart management** — Browse, install, upgrade, rollback, and uninstall Helm releases directly from the UI.
71
+ - **RBAC visualization** — See who can do what with a permission matrix and interactive access review.
72
+ - **Modern & lightweight** — Fast startup, small footprint. No Electron bloat from bundled IDE features you don't need.
73
+ - **30+ resource types** — Pods, Deployments, StatefulSets, DaemonSets, CronJobs, Services, Ingresses, ConfigMaps, Secrets, CRDs, and more.
74
+ - **Cross-platform** — Runs natively on macOS, Windows, and Linux with auto-update.
48
75
 
49
76
  ---
50
77
 
@@ -90,13 +117,26 @@ Start port forwards from pod container ports, service ports, or YAML editor fiel
90
117
 
91
118
  ![KubeOps forward](assets/port-forward.png)
92
119
 
93
- ### YAML Editor (Table / YAML / Edit)
120
+ ### Helm Chart Management
121
+
122
+ Full Helm release lifecycle from the UI. Browse installed releases across all namespaces, view release details (status, chart info, revision history, values), install new charts from configured repositories, upgrade with modified values, rollback to previous revisions, and uninstall releases. Requires the `helm` CLI on your PATH.
123
+
124
+ ### RBAC Visualization
125
+
126
+ A dedicated RBAC Summary page aggregates all Roles, ClusterRoles, RoleBindings, and ClusterRoleBindings into a permission matrix showing who (User / Group / ServiceAccount) can do what. Filter by subject type or search by name. The Access Review tab lets you check if a specific subject is allowed to perform an action on a resource.
94
127
 
95
- Three viewing modes for every resource manifest:
128
+ ### Cluster Catalog
129
+
130
+ Organize clusters with tags, groups, and favorites. Switch between list and card views. Group clusters by environment (Production / Staging / Development) or create custom groups. Tag clusters for quick filtering. Star your most-used clusters for fast access. All metadata persists across sessions.
131
+
132
+ ### YAML Editor (Table / YAML / Edit / Diff)
133
+
134
+ Four viewing modes for every resource manifest:
96
135
 
97
136
  - **Table view** — Structured, collapsible sections with smart value rendering
98
137
  - **YAML view** — Read-only formatted output
99
138
  - **Edit mode** — Syntax-highlighted editor with validation, save with `Cmd+S`
139
+ - **Review Changes** — Side-by-side diff showing additions (green) and deletions (red) before saving
100
140
 
101
141
  <!-- Screenshot: YAML Editor -->
102
142
 
@@ -118,33 +158,46 @@ Click the info icon on any App Map node to open a right-side drawer with Overvie
118
158
 
119
159
  ![KubeOps drawer](assets/drawer.png)
120
160
 
161
+ ### Deployment Scaling
162
+
163
+ Scale Deployments and StatefulSets directly from the UI. A dedicated scale dialog lets you set the desired replica count and apply it instantly.
164
+
165
+ ### Custom Resource Definitions (CRDs)
166
+
167
+ Browse cluster-installed CRDs and their instances. The sidebar lists discovered CRD groups, and each instance supports the same Table / YAML / Edit views as built-in resources.
168
+
169
+ ### Dark / Light Mode
170
+
171
+ Toggle between dark and light themes from the header. Powered by `next-themes` with system preference detection. Your choice persists across sessions.
172
+
173
+ ### Teleport Authentication
174
+
175
+ For clusters behind [Teleport](https://goteleport.com/), KubeOps detects Teleport contexts and provides a built-in login flow via `tsh kube login` — no manual terminal steps required.
176
+
121
177
  ### Pod Restart Watcher
122
178
 
123
179
  Enable Watch on any pod to monitor restart counts in the background. Polls every 10 seconds and sends desktop notifications when restarts increase. Watched pods persist across sessions.
124
180
 
125
- ---
181
+ ### Auto-Update
126
182
 
127
- ## Getting Started
183
+ An update indicator in the header shows the current state: checking → available → downloading → ready to install. Updates are downloaded in the background and applied on the next restart.
128
184
 
129
- ### Prerequisites
185
+ ---
130
186
 
131
- - **Node.js** v18+
132
- - A valid `~/.kube/config` with at least one context
187
+ ## Development
133
188
 
134
189
  ### Run from Source
135
190
 
136
191
  ```bash
137
192
  git clone https://github.com/trustspirit/kubeops.git
138
193
  cd kubeops
139
- npm install
194
+ npm install # Node.js v18+ required
140
195
  npm run electron:dev
141
196
  ```
142
197
 
143
198
  The app opens automatically once the dev server is ready (port 51230).
144
199
 
145
- ---
146
-
147
- ## Build
200
+ ### Build
148
201
 
149
202
  Create a distributable package for your platform:
150
203
 
@@ -158,22 +211,6 @@ Output is written to `dist-electron/`.
158
211
 
159
212
  ---
160
213
 
161
- ## Architecture
162
-
163
- | Layer | Technology |
164
- | -------------- | ------------------------------------------------ |
165
- | Desktop shell | Electron |
166
- | Frontend | Next.js 16 (App Router), React 19, Tailwind CSS |
167
- | State | Zustand (persisted to localStorage) |
168
- | Data fetching | SWR with auto-refresh |
169
- | K8s API | `@kubernetes/client-node` via Next.js API routes |
170
- | Terminal | xterm.js + node-pty over WebSocket |
171
- | Charts | Recharts |
172
- | Resource graph | React Flow + Dagre |
173
- | YAML | js-yaml |
174
-
175
- ---
176
-
177
214
  ## Keyboard Shortcuts
178
215
 
179
216
  | Shortcut | Action |
@@ -216,6 +253,23 @@ Logs rotate automatically at 5 MB (previous log kept as `error.log.old`).
216
253
 
217
254
  ---
218
255
 
256
+ ## Supported Resources
257
+
258
+ KubeOps provides full browse / detail / YAML / edit support for 30+ Kubernetes resource types:
259
+
260
+ | Category | Resources |
261
+ | --------------- | -------------------------------------------------------------- |
262
+ | Workloads | Pods, Deployments, StatefulSets, DaemonSets, ReplicaSets, Jobs, CronJobs |
263
+ | Network | Services, Ingresses, Endpoints, Network Policies |
264
+ | Config | ConfigMaps, Secrets, Service Accounts |
265
+ | Storage | Persistent Volumes, Persistent Volume Claims |
266
+ | Access Control | Roles, Role Bindings, Cluster Roles, Cluster Role Bindings, RBAC Summary |
267
+ | Cluster | Nodes, Namespaces, Events |
268
+ | Helm | Releases (install, upgrade, rollback, uninstall) |
269
+ | Custom | Any installed CRD and its instances |
270
+
271
+ ---
272
+
219
273
  ## Troubleshooting
220
274
 
221
275
  | Problem | Solution |
@@ -226,6 +280,7 @@ Logs rotate automatically at 5 MB (previous log kept as `error.log.old`).
226
280
  | Metrics charts empty | Ensure `metrics-server` is installed in the cluster |
227
281
  | Network/FS charts missing | Requires Prometheus with `container_network_*` and `container_fs_*` metrics |
228
282
  | Port forward fails | Check that `kubectl` is on your PATH and the target pod is running |
283
+ | Teleport login fails | Ensure `tsh` is installed and on your PATH |
229
284
  | Diagnosing crashes | Open **Help → Open Error Log** to see captured errors |
230
285
 
231
286
  ### macOS Gatekeeper
@@ -245,3 +300,9 @@ Because the app is not yet code-signed with an Apple Developer certificate, macO
245
300
  ```bash
246
301
  xattr -cr /Applications/KubeOps.app
247
302
  ```
303
+
304
+ ---
305
+
306
+ ## License
307
+
308
+ [MIT](LICENSE)
package/electron/main.js CHANGED
@@ -58,6 +58,11 @@ autoUpdater.autoDownload = false;
58
58
  autoUpdater.autoInstallOnAppQuit = true;
59
59
  autoUpdater.logger = null;
60
60
 
61
+ // Skip code signing verification for unsigned builds
62
+ if (process.platform === 'darwin') {
63
+ autoUpdater.forceCodeSigning = false;
64
+ }
65
+
61
66
  function sendUpdateStatus(status) {
62
67
  if (mainWindow && !mainWindow.isDestroyed()) {
63
68
  mainWindow.webContents.send('updater:status', status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubeops",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A modern desktop client for Kubernetes cluster management",
5
5
  "main": "electron/main.js",
6
6
  "repository": {
@@ -58,6 +58,7 @@
58
58
  "clsx": "^2.1.1",
59
59
  "cmdk": "^1.1.1",
60
60
  "dagre": "^0.8.5",
61
+ "diff": "^8.0.3",
61
62
  "electron-updater": "^6.7.3",
62
63
  "js-yaml": "^4.1.1",
63
64
  "lucide-react": "^0.563.0",
@@ -76,6 +77,7 @@
76
77
  },
77
78
  "devDependencies": {
78
79
  "@tailwindcss/postcss": "^4",
80
+ "@types/diff": "^7.0.2",
79
81
  "@types/js-yaml": "^4.0.9",
80
82
  "@types/node": "^20",
81
83
  "@types/react": "^19",
package/server.ts CHANGED
@@ -52,6 +52,25 @@ app.prepare().then(() => {
52
52
  }
53
53
  });
54
54
 
55
+ // Cleanup child processes on shutdown
56
+ const shutdown = () => {
57
+ console.log('> Shutting down — cleaning up child processes...');
58
+ try {
59
+ const { cleanupAllForwards } = require('./src/app/api/port-forward/route');
60
+ cleanupAllForwards();
61
+ } catch { /* module may not be loaded yet */ }
62
+
63
+ // Close all WebSocket connections
64
+ wssLogs.clients.forEach((ws) => ws.terminate());
65
+ wssExec.clients.forEach((ws) => ws.terminate());
66
+
67
+ server.close();
68
+ process.exit(0);
69
+ };
70
+
71
+ process.on('SIGTERM', shutdown);
72
+ process.on('SIGINT', shutdown);
73
+
55
74
  server.listen(port, () => {
56
75
  console.log(`> KubeOps running on http://${hostname}:${port}`);
57
76
  });
@@ -0,0 +1,65 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { runHelm, isValidHelmName } from '@/lib/helm/helm-runner';
3
+ import { requireHelm, withTempValuesFile } from '@/lib/helm/helpers';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface RouteParams {
8
+ params: Promise<{ clusterId: string }>;
9
+ }
10
+
11
+ export async function POST(req: NextRequest, { params }: RouteParams) {
12
+ const helmCheck = requireHelm();
13
+ if (helmCheck) return helmCheck;
14
+
15
+ const { clusterId } = await params;
16
+ const contextName = decodeURIComponent(clusterId);
17
+
18
+ let body: any;
19
+ try {
20
+ body = await req.json();
21
+ } catch {
22
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
23
+ }
24
+
25
+ const { releaseName, chart, namespace, values, createNamespace } = body;
26
+
27
+ if (!releaseName || !isValidHelmName(releaseName)) {
28
+ return NextResponse.json({ error: 'Valid releaseName is required (alphanumeric, hyphens, dots, underscores)' }, { status: 400 });
29
+ }
30
+ if (!chart) {
31
+ return NextResponse.json({ error: 'chart is required' }, { status: 400 });
32
+ }
33
+ if (!namespace) {
34
+ return NextResponse.json({ error: 'namespace is required' }, { status: 400 });
35
+ }
36
+
37
+ return withTempValuesFile(values, async (tmpFile) => {
38
+ const args = ['install', releaseName, chart, '-n', namespace, '--output', 'json'];
39
+
40
+ if (createNamespace !== false) {
41
+ args.push('--create-namespace');
42
+ }
43
+
44
+ if (tmpFile) {
45
+ args.push('-f', tmpFile);
46
+ }
47
+
48
+ const result = await runHelm(args, contextName);
49
+
50
+ if (result.code !== 0) {
51
+ console.error(`[Helm] install ${releaseName} failed: ${result.stderr}`);
52
+ return NextResponse.json(
53
+ { error: result.stderr || `Failed to install chart "${chart}" as "${releaseName}"` },
54
+ { status: 500 }
55
+ );
56
+ }
57
+
58
+ try {
59
+ const installed = JSON.parse(result.stdout);
60
+ return NextResponse.json(installed);
61
+ } catch {
62
+ return NextResponse.json({ success: true, message: result.stdout.trim() });
63
+ }
64
+ });
65
+ }
@@ -0,0 +1,44 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { runHelm } from '@/lib/helm/helm-runner';
3
+ import { requireHelm, requireNamespaceParam } from '@/lib/helm/helpers';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface RouteParams {
8
+ params: Promise<{ clusterId: string; name: string }>;
9
+ }
10
+
11
+ export async function GET(req: NextRequest, { params }: RouteParams) {
12
+ const helmCheck = requireHelm();
13
+ if (helmCheck) return helmCheck;
14
+
15
+ const { clusterId, name } = await params;
16
+ const contextName = decodeURIComponent(clusterId);
17
+ const releaseName = decodeURIComponent(name);
18
+ const { searchParams } = new URL(req.url);
19
+ const namespace = searchParams.get('namespace');
20
+
21
+ const nsCheck = requireNamespaceParam(namespace);
22
+ if (nsCheck) return nsCheck;
23
+
24
+ const args = ['history', releaseName, '-n', namespace!, '--output', 'json'];
25
+ const result = await runHelm(args, contextName);
26
+
27
+ if (result.code !== 0) {
28
+ console.error(`[Helm] history ${releaseName} failed: ${result.stderr}`);
29
+ return NextResponse.json(
30
+ { error: result.stderr || `Failed to get history for release "${releaseName}"` },
31
+ { status: 500 }
32
+ );
33
+ }
34
+
35
+ try {
36
+ const history = result.stdout.trim() ? JSON.parse(result.stdout) : [];
37
+ return NextResponse.json({ history });
38
+ } catch {
39
+ return NextResponse.json(
40
+ { error: 'Failed to parse Helm output' },
41
+ { status: 500 }
42
+ );
43
+ }
44
+ }
@@ -0,0 +1,47 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { runHelm } from '@/lib/helm/helm-runner';
3
+ import { requireHelm } from '@/lib/helm/helpers';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface RouteParams {
8
+ params: Promise<{ clusterId: string; name: string }>;
9
+ }
10
+
11
+ export async function POST(req: NextRequest, { params }: RouteParams) {
12
+ const helmCheck = requireHelm();
13
+ if (helmCheck) return helmCheck;
14
+
15
+ const { clusterId, name } = await params;
16
+ const contextName = decodeURIComponent(clusterId);
17
+ const releaseName = decodeURIComponent(name);
18
+
19
+ let body: any;
20
+ try {
21
+ body = await req.json();
22
+ } catch {
23
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
24
+ }
25
+
26
+ const { revision, namespace } = body;
27
+
28
+ if (!revision) {
29
+ return NextResponse.json({ error: 'revision is required' }, { status: 400 });
30
+ }
31
+ if (!namespace) {
32
+ return NextResponse.json({ error: 'namespace is required' }, { status: 400 });
33
+ }
34
+
35
+ const args = ['rollback', releaseName, String(revision), '-n', namespace];
36
+ const result = await runHelm(args, contextName);
37
+
38
+ if (result.code !== 0) {
39
+ console.error(`[Helm] rollback ${releaseName} to ${revision} failed: ${result.stderr}`);
40
+ return NextResponse.json(
41
+ { error: result.stderr || `Failed to rollback release "${releaseName}" to revision ${revision}` },
42
+ { status: 500 }
43
+ );
44
+ }
45
+
46
+ return NextResponse.json({ success: true, message: result.stdout.trim() || `Rolled back "${releaseName}" to revision ${revision}` });
47
+ }
@@ -0,0 +1,71 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { runHelm } from '@/lib/helm/helm-runner';
3
+ import { requireHelm, requireNamespaceParam } from '@/lib/helm/helpers';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface RouteParams {
8
+ params: Promise<{ clusterId: string; name: string }>;
9
+ }
10
+
11
+ export async function GET(req: NextRequest, { params }: RouteParams) {
12
+ const helmCheck = requireHelm();
13
+ if (helmCheck) return helmCheck;
14
+
15
+ const { clusterId, name } = await params;
16
+ const contextName = decodeURIComponent(clusterId);
17
+ const releaseName = decodeURIComponent(name);
18
+ const { searchParams } = new URL(req.url);
19
+ const namespace = searchParams.get('namespace');
20
+
21
+ const nsCheck = requireNamespaceParam(namespace);
22
+ if (nsCheck) return nsCheck;
23
+
24
+ const args = ['status', releaseName, '-n', namespace!, '--output', 'json'];
25
+ const result = await runHelm(args, contextName);
26
+
27
+ if (result.code !== 0) {
28
+ console.error(`[Helm] status ${releaseName} failed: ${result.stderr}`);
29
+ return NextResponse.json(
30
+ { error: result.stderr || `Failed to get status for release "${releaseName}"` },
31
+ { status: 500 }
32
+ );
33
+ }
34
+
35
+ try {
36
+ const detail = JSON.parse(result.stdout);
37
+ return NextResponse.json(detail);
38
+ } catch {
39
+ return NextResponse.json(
40
+ { error: 'Failed to parse Helm output' },
41
+ { status: 500 }
42
+ );
43
+ }
44
+ }
45
+
46
+ export async function DELETE(req: NextRequest, { params }: RouteParams) {
47
+ const helmCheck = requireHelm();
48
+ if (helmCheck) return helmCheck;
49
+
50
+ const { clusterId, name } = await params;
51
+ const contextName = decodeURIComponent(clusterId);
52
+ const releaseName = decodeURIComponent(name);
53
+ const { searchParams } = new URL(req.url);
54
+ const namespace = searchParams.get('namespace');
55
+
56
+ const nsCheck = requireNamespaceParam(namespace);
57
+ if (nsCheck) return nsCheck;
58
+
59
+ const args = ['uninstall', releaseName, '-n', namespace!];
60
+ const result = await runHelm(args, contextName);
61
+
62
+ if (result.code !== 0) {
63
+ console.error(`[Helm] uninstall ${releaseName} failed: ${result.stderr}`);
64
+ return NextResponse.json(
65
+ { error: result.stderr || `Failed to uninstall release "${releaseName}"` },
66
+ { status: 500 }
67
+ );
68
+ }
69
+
70
+ return NextResponse.json({ success: true, message: result.stdout.trim() });
71
+ }
@@ -0,0 +1,64 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { runHelm } from '@/lib/helm/helm-runner';
3
+ import { requireHelm, withTempValuesFile } from '@/lib/helm/helpers';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface RouteParams {
8
+ params: Promise<{ clusterId: string; name: string }>;
9
+ }
10
+
11
+ export async function POST(req: NextRequest, { params }: RouteParams) {
12
+ const helmCheck = requireHelm();
13
+ if (helmCheck) return helmCheck;
14
+
15
+ const { clusterId, name } = await params;
16
+ const contextName = decodeURIComponent(clusterId);
17
+ const releaseName = decodeURIComponent(name);
18
+
19
+ let body: any;
20
+ try {
21
+ body = await req.json();
22
+ } catch {
23
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
24
+ }
25
+
26
+ const { chart, namespace, values, reuseValues } = body;
27
+
28
+ if (!chart) {
29
+ return NextResponse.json({ error: 'chart is required' }, { status: 400 });
30
+ }
31
+ if (!namespace) {
32
+ return NextResponse.json({ error: 'namespace is required' }, { status: 400 });
33
+ }
34
+
35
+ return withTempValuesFile(values, async (tmpFile) => {
36
+ const args = ['upgrade', releaseName, chart, '-n', namespace, '--output', 'json'];
37
+
38
+ if (reuseValues) {
39
+ args.push('--reuse-values');
40
+ }
41
+
42
+ if (tmpFile) {
43
+ args.push('-f', tmpFile);
44
+ }
45
+
46
+ const result = await runHelm(args, contextName);
47
+
48
+ if (result.code !== 0) {
49
+ console.error(`[Helm] upgrade ${releaseName} failed: ${result.stderr}`);
50
+ return NextResponse.json(
51
+ { error: result.stderr || `Failed to upgrade release "${releaseName}"` },
52
+ { status: 500 }
53
+ );
54
+ }
55
+
56
+ try {
57
+ const upgraded = JSON.parse(result.stdout);
58
+ return NextResponse.json(upgraded);
59
+ } catch {
60
+ // upgrade succeeded but output may not be JSON
61
+ return NextResponse.json({ success: true, message: result.stdout.trim() });
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,51 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { runHelm } from '@/lib/helm/helm-runner';
3
+ import { requireHelm, requireNamespaceParam } from '@/lib/helm/helpers';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface RouteParams {
8
+ params: Promise<{ clusterId: string; name: string }>;
9
+ }
10
+
11
+ export async function GET(req: NextRequest, { params }: RouteParams) {
12
+ const helmCheck = requireHelm();
13
+ if (helmCheck) return helmCheck;
14
+
15
+ const { clusterId, name } = await params;
16
+ const contextName = decodeURIComponent(clusterId);
17
+ const releaseName = decodeURIComponent(name);
18
+ const { searchParams } = new URL(req.url);
19
+ const namespace = searchParams.get('namespace');
20
+ const all = searchParams.get('all') === 'true';
21
+
22
+ const nsCheck = requireNamespaceParam(namespace);
23
+ if (nsCheck) return nsCheck;
24
+
25
+ const args = ['get', 'values', releaseName, '-n', namespace!, '--output', 'json'];
26
+ if (all) {
27
+ args.push('--all');
28
+ }
29
+
30
+ const result = await runHelm(args, contextName);
31
+
32
+ if (result.code !== 0) {
33
+ console.error(`[Helm] get values ${releaseName} failed: ${result.stderr}`);
34
+ return NextResponse.json(
35
+ { error: result.stderr || `Failed to get values for release "${releaseName}"` },
36
+ { status: 500 }
37
+ );
38
+ }
39
+
40
+ try {
41
+ // helm get values may return "null" for releases with no custom values
42
+ const trimmed = result.stdout.trim();
43
+ const values = trimmed && trimmed !== 'null' ? JSON.parse(trimmed) : {};
44
+ return NextResponse.json({ values });
45
+ } catch {
46
+ return NextResponse.json(
47
+ { error: 'Failed to parse Helm output' },
48
+ { status: 500 }
49
+ );
50
+ }
51
+ }
@@ -0,0 +1,39 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { runHelm } from '@/lib/helm/helm-runner';
3
+ import { requireHelm, parseHelmJson } from '@/lib/helm/helpers';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ interface RouteParams {
8
+ params: Promise<{ clusterId: string }>;
9
+ }
10
+
11
+ export async function GET(req: NextRequest, { params }: RouteParams) {
12
+ const helmCheck = requireHelm();
13
+ if (helmCheck) return helmCheck;
14
+
15
+ const { clusterId } = await params;
16
+ const contextName = decodeURIComponent(clusterId);
17
+ const { searchParams } = new URL(req.url);
18
+ const namespace = searchParams.get('namespace');
19
+
20
+ const args = ['list', '--output', 'json'];
21
+ if (namespace) {
22
+ args.push('-n', namespace);
23
+ } else {
24
+ args.push('-A');
25
+ }
26
+
27
+ const result = await runHelm(args, contextName);
28
+
29
+ if (result.code !== 0) {
30
+ console.error(`[Helm] list releases failed: ${result.stderr}`);
31
+ return NextResponse.json(
32
+ { error: result.stderr || 'Failed to list Helm releases' },
33
+ { status: 500 }
34
+ );
35
+ }
36
+
37
+ const releases = parseHelmJson(result.stdout) ?? [];
38
+ return NextResponse.json({ releases });
39
+ }