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.
- package/README.md +96 -35
- package/electron/main.js +5 -0
- package/package.json +3 -1
- package/server.ts +19 -0
- package/src/app/api/clusters/[clusterId]/helm/install/route.ts +65 -0
- package/src/app/api/clusters/[clusterId]/helm/releases/[name]/history/route.ts +44 -0
- package/src/app/api/clusters/[clusterId]/helm/releases/[name]/rollback/route.ts +47 -0
- package/src/app/api/clusters/[clusterId]/helm/releases/[name]/route.ts +71 -0
- package/src/app/api/clusters/[clusterId]/helm/releases/[name]/upgrade/route.ts +64 -0
- package/src/app/api/clusters/[clusterId]/helm/releases/[name]/values/route.ts +51 -0
- package/src/app/api/clusters/[clusterId]/helm/releases/route.ts +39 -0
- package/src/app/api/clusters/[clusterId]/helm/repos/route.ts +70 -0
- package/src/app/api/clusters/[clusterId]/helm/search/route.ts +41 -0
- package/src/app/api/clusters/[clusterId]/rbac/access-review/route.ts +62 -0
- package/src/app/api/clusters/[clusterId]/rbac/summary/route.ts +125 -0
- package/src/app/api/port-forward/route.ts +9 -1
- package/src/app/clusters/[clusterId]/helm/[releaseName]/page.tsx +346 -0
- package/src/app/clusters/[clusterId]/helm/page.tsx +110 -0
- package/src/app/clusters/[clusterId]/page.tsx +12 -8
- package/src/app/clusters/[clusterId]/port-forwarding/page.tsx +1 -1
- package/src/app/clusters/[clusterId]/rbac/page.tsx +65 -0
- package/src/app/clusters/page.tsx +228 -112
- package/src/app/globals.css +3 -3
- package/src/app/layout.tsx +2 -0
- package/src/components/clusters/cluster-card.tsx +106 -0
- package/src/components/clusters/cluster-tag-editor.tsx +109 -0
- package/src/components/helm/helm-install-dialog.tsx +277 -0
- package/src/components/helm/helm-rollback-dialog.tsx +86 -0
- package/src/components/helm/helm-upgrade-dialog.tsx +168 -0
- package/src/components/layout/electron-class.tsx +12 -0
- package/src/components/layout/header.tsx +4 -4
- package/src/components/namespaces/namespace-selector.tsx +48 -32
- package/src/components/panel/terminal-tab.tsx +34 -1
- package/src/components/rbac/access-review-form.tsx +240 -0
- package/src/components/rbac/rbac-summary.tsx +306 -0
- package/src/components/resources/resource-columns.tsx +52 -0
- package/src/components/shared/status-badge.tsx +8 -0
- package/src/components/shared/yaml-diff-view.tsx +117 -0
- package/src/components/shared/yaml-editor.tsx +30 -3
- package/src/hooks/use-clusters-filtering.ts +79 -0
- package/src/hooks/use-helm-release-detail.ts +9 -0
- package/src/hooks/use-helm-releases.ts +13 -0
- package/src/hooks/use-rbac-summary.ts +8 -0
- package/src/hooks/use-resource-list.ts +1 -1
- package/src/hooks/use-resource-tree.ts +36 -2
- package/src/lib/cluster-names.ts +10 -0
- package/src/lib/constants.ts +11 -0
- package/src/lib/helm/helm-runner.ts +90 -0
- package/src/lib/helm/helpers.ts +56 -0
- package/src/lib/k8s/client-factory.ts +4 -0
- package/src/lib/k8s/error-handling.ts +13 -0
- package/src/providers/swr-provider.tsx +19 -1
- package/src/stores/cluster-catalog-store.ts +112 -0
- package/src/types/helm.ts +53 -0
- 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
- **
|
|
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
|

|
|
92
119
|
|
|
93
|
-
###
|
|
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
|
-
|
|
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
|

|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
---
|
|
130
186
|
|
|
131
|
-
|
|
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.
|
|
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
|
+
}
|