kubeops 0.1.0
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 +219 -0
- package/electron/main.js +429 -0
- package/electron/preload.js +13 -0
- package/electron-builder.yml +60 -0
- package/next.config.ts +8 -0
- package/package.json +98 -0
- package/postcss.config.mjs +7 -0
- package/resources/icon.icns +0 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/resources/icon.svg +86 -0
- package/scripts/build-server.mjs +20 -0
- package/scripts/generate-icons.mjs +61 -0
- package/server.ts +58 -0
- package/src/app/api/clusters/[clusterId]/health/route.ts +27 -0
- package/src/app/api/clusters/[clusterId]/metrics/route.ts +196 -0
- package/src/app/api/clusters/[clusterId]/namespaces/route.ts +48 -0
- package/src/app/api/clusters/[clusterId]/nodes/[nodeName]/route.ts +21 -0
- package/src/app/api/clusters/[clusterId]/nodes/route.ts +31 -0
- package/src/app/api/clusters/[clusterId]/resources/[namespace]/[...resourcePath]/route.ts +204 -0
- package/src/app/api/clusters/route.ts +48 -0
- package/src/app/api/kubeconfig/route.ts +25 -0
- package/src/app/api/port-forward/route.ts +143 -0
- package/src/app/api/tsh/login/route.ts +50 -0
- package/src/app/clusters/[clusterId]/app-map/page.tsx +42 -0
- package/src/app/clusters/[clusterId]/clusterrolebindings/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/clusterrolebindings/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/clusterroles/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/clusterroles/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/layout.tsx +9 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/[name]/page.tsx +457 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/events/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/exec/page.tsx +173 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/logs/page.tsx +137 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/page.tsx +448 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/[name]/page.tsx +168 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/[name]/page.tsx +302 -0
- package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/nodes/[nodeName]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/nodes/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/page.tsx +635 -0
- package/src/app/clusters/[clusterId]/port-forwarding/page.tsx +145 -0
- package/src/app/clusters/[clusterId]/pvs/[name]/page.tsx +5 -0
- package/src/app/clusters/[clusterId]/pvs/page.tsx +5 -0
- package/src/app/clusters/page.tsx +166 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +167 -0
- package/src/app/layout.tsx +48 -0
- package/src/app/page.tsx +5 -0
- package/src/components/clusters/cluster-selector.tsx +64 -0
- package/src/components/layout/app-shell.tsx +26 -0
- package/src/components/layout/breadcrumbs.tsx +97 -0
- package/src/components/layout/command-palette.tsx +112 -0
- package/src/components/layout/header.tsx +184 -0
- package/src/components/layout/sidebar.tsx +84 -0
- package/src/components/layout/theme-toggle.tsx +21 -0
- package/src/components/namespaces/namespace-selector.tsx +165 -0
- package/src/components/panel/bottom-panel.tsx +127 -0
- package/src/components/panel/logs-tab.tsx +109 -0
- package/src/components/panel/terminal-tab.tsx +180 -0
- package/src/components/pods/pod-watch-button.tsx +44 -0
- package/src/components/resources/resource-columns.tsx +320 -0
- package/src/components/resources/resource-detail-page.tsx +191 -0
- package/src/components/resources/resource-list-page.tsx +78 -0
- package/src/components/resources/scale-dialog.tsx +107 -0
- package/src/components/settings/settings-dialog.tsx +103 -0
- package/src/components/shared/age-display.tsx +27 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/data-table.tsx +149 -0
- package/src/components/shared/env-value-resolver.tsx +570 -0
- package/src/components/shared/error-display.tsx +109 -0
- package/src/components/shared/loading-skeleton.tsx +25 -0
- package/src/components/shared/metrics-charts-impl.tsx +434 -0
- package/src/components/shared/metrics-charts.tsx +24 -0
- package/src/components/shared/port-forward-btn.tsx +60 -0
- package/src/components/shared/resource-info-drawer.tsx +542 -0
- package/src/components/shared/resource-node.tsx +157 -0
- package/src/components/shared/resource-tree-impl.tsx +228 -0
- package/src/components/shared/resource-tree.tsx +20 -0
- package/src/components/shared/status-badge.tsx +35 -0
- package/src/components/shared/yaml-editor.tsx +438 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-age-tick.ts +40 -0
- package/src/hooks/use-auto-update.ts +100 -0
- package/src/hooks/use-clusters.ts +32 -0
- package/src/hooks/use-namespaces.ts +17 -0
- package/src/hooks/use-pod-watcher.ts +79 -0
- package/src/hooks/use-port-forwards.ts +18 -0
- package/src/hooks/use-resource-detail.ts +28 -0
- package/src/hooks/use-resource-list.ts +26 -0
- package/src/hooks/use-resource-tree.ts +440 -0
- package/src/lib/api-client.ts +31 -0
- package/src/lib/constants.ts +126 -0
- package/src/lib/k8s/client-factory.ts +57 -0
- package/src/lib/k8s/kubeconfig-manager.ts +43 -0
- package/src/lib/k8s/resource-api.ts +223 -0
- package/src/lib/k8s/types.ts +29 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/swr-provider.tsx +20 -0
- package/src/providers/theme-provider.tsx +17 -0
- package/src/stores/cluster-store.ts +32 -0
- package/src/stores/namespace-store.ts +27 -0
- package/src/stores/panel-store.ts +61 -0
- package/src/stores/pod-watcher-store.ts +69 -0
- package/src/stores/settings-store.ts +24 -0
- package/src/stores/sidebar-store.ts +22 -0
- package/src/types/cluster.ts +19 -0
- package/src/types/css.d.ts +6 -0
- package/src/types/electron.d.ts +25 -0
- package/src/types/navigation.ts +4 -0
- package/src/types/resource.ts +27 -0
- package/tsconfig.json +34 -0
- package/ws/exec-handler.ts +112 -0
- package/ws/index.ts +2 -0
- package/ws/logs-handler.ts +70 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import { parse } from 'url';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import * as pty from 'node-pty';
|
|
6
|
+
|
|
7
|
+
// Resolve kubectl path at startup
|
|
8
|
+
let kubectlPath = '/usr/local/bin/kubectl';
|
|
9
|
+
try {
|
|
10
|
+
const paths = execSync('which -a kubectl', { encoding: 'utf-8' }).trim().split('\n');
|
|
11
|
+
kubectlPath = paths.find(p => !p.includes(' ') && !p.includes('.rd/bin'))
|
|
12
|
+
|| paths.find(p => !p.includes(' '))
|
|
13
|
+
|| paths[0]
|
|
14
|
+
|| kubectlPath;
|
|
15
|
+
} catch { /* use default */ }
|
|
16
|
+
console.log(`[Exec] Using kubectl: ${kubectlPath}`);
|
|
17
|
+
|
|
18
|
+
export function handleExecConnection(ws: WebSocket, req: IncomingMessage) {
|
|
19
|
+
const { pathname, query } = parse(req.url!, true);
|
|
20
|
+
const parts = pathname!.split('/').filter(Boolean);
|
|
21
|
+
const clusterId = decodeURIComponent(parts[2]);
|
|
22
|
+
const namespace = parts[3];
|
|
23
|
+
const podName = parts[4];
|
|
24
|
+
const container = query.container as string;
|
|
25
|
+
|
|
26
|
+
console.log(`[Exec] Starting: ${namespace}/${podName} (${container})`);
|
|
27
|
+
|
|
28
|
+
let ptyProcess: pty.IPty;
|
|
29
|
+
try {
|
|
30
|
+
// OpenLens-style: kubectl exec -it with shell fallback chain
|
|
31
|
+
ptyProcess = pty.spawn(kubectlPath, [
|
|
32
|
+
'exec', '-i', '-t',
|
|
33
|
+
'--context', clusterId,
|
|
34
|
+
'-n', namespace,
|
|
35
|
+
'-c', container,
|
|
36
|
+
podName,
|
|
37
|
+
'--', 'sh', '-c', 'clear; (bash || ash || sh)',
|
|
38
|
+
], {
|
|
39
|
+
name: 'xterm-256color',
|
|
40
|
+
cols: 80,
|
|
41
|
+
rows: 24,
|
|
42
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
43
|
+
});
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
console.error(`[Exec] Failed to spawn PTY:`, err.message);
|
|
46
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
47
|
+
ws.send(JSON.stringify({ type: 'error', message: `Failed to start: ${err.message}` }));
|
|
48
|
+
ws.close();
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`[Exec] PTY spawned pid=${ptyProcess.pid} for ${namespace}/${podName}`);
|
|
54
|
+
ws.send(JSON.stringify({ type: 'connected' }));
|
|
55
|
+
|
|
56
|
+
// PTY output → browser
|
|
57
|
+
ptyProcess.onData((data: string) => {
|
|
58
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
59
|
+
ws.send(data);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
64
|
+
console.log(`[Exec] PTY exited pid=${ptyProcess.pid} code=${exitCode} signal=${signal}`);
|
|
65
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
66
|
+
ws.send(JSON.stringify({
|
|
67
|
+
type: 'exit',
|
|
68
|
+
reason: exitCode === 0 ? 'Session ended' : `Exit code ${exitCode}`,
|
|
69
|
+
}));
|
|
70
|
+
ws.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Browser input → PTY
|
|
75
|
+
ws.on('message', (rawData: Buffer | ArrayBuffer | Buffer[]) => {
|
|
76
|
+
let data: Buffer;
|
|
77
|
+
if (Buffer.isBuffer(rawData)) {
|
|
78
|
+
data = rawData;
|
|
79
|
+
} else if (rawData instanceof ArrayBuffer) {
|
|
80
|
+
data = Buffer.from(rawData);
|
|
81
|
+
} else if (Array.isArray(rawData)) {
|
|
82
|
+
data = Buffer.concat(rawData);
|
|
83
|
+
} else {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (data.length === 0) return;
|
|
88
|
+
|
|
89
|
+
const type = data[0];
|
|
90
|
+
const payload = data.subarray(1);
|
|
91
|
+
|
|
92
|
+
if (type === 0) {
|
|
93
|
+
// stdin
|
|
94
|
+
ptyProcess.write(payload.toString('utf-8'));
|
|
95
|
+
} else if (type === 1) {
|
|
96
|
+
// resize
|
|
97
|
+
try {
|
|
98
|
+
const { cols, rows } = JSON.parse(payload.toString());
|
|
99
|
+
ptyProcess.resize(cols, rows);
|
|
100
|
+
} catch { /* ignore */ }
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
ws.on('close', () => {
|
|
105
|
+
console.log(`[Exec] Client disconnected, killing PTY pid=${ptyProcess.pid}`);
|
|
106
|
+
try { ptyProcess.kill(); } catch { /* already dead */ }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
ws.on('error', () => {
|
|
110
|
+
try { ptyProcess.kill(); } catch { /* already dead */ }
|
|
111
|
+
});
|
|
112
|
+
}
|
package/ws/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import * as k8s from '@kubernetes/client-node';
|
|
4
|
+
import { parse } from 'url';
|
|
5
|
+
import { PassThrough } from 'stream';
|
|
6
|
+
|
|
7
|
+
export function handleLogsConnection(ws: WebSocket, req: IncomingMessage) {
|
|
8
|
+
const { pathname, query } = parse(req.url!, true);
|
|
9
|
+
const parts = pathname!.split('/').filter(Boolean);
|
|
10
|
+
// parts: ['ws', 'logs', clusterId, namespace, podName]
|
|
11
|
+
const clusterId = decodeURIComponent(parts[2]);
|
|
12
|
+
const namespace = parts[3];
|
|
13
|
+
const podName = parts[4];
|
|
14
|
+
const container = query.container as string;
|
|
15
|
+
const follow = query.follow === 'true';
|
|
16
|
+
const timestamps = query.timestamps === 'true';
|
|
17
|
+
const tailLines = parseInt(query.tailLines as string) || 100;
|
|
18
|
+
|
|
19
|
+
const kc = new k8s.KubeConfig();
|
|
20
|
+
kc.loadFromDefault();
|
|
21
|
+
kc.setCurrentContext(clusterId);
|
|
22
|
+
|
|
23
|
+
const log = new k8s.Log(kc);
|
|
24
|
+
const logStream = new PassThrough();
|
|
25
|
+
|
|
26
|
+
logStream.on('data', (chunk: Buffer) => {
|
|
27
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
28
|
+
ws.send(chunk.toString('utf-8'));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
logStream.on('error', (err) => {
|
|
33
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
34
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
35
|
+
ws.close();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
log
|
|
40
|
+
.log(namespace, podName, container, logStream, {
|
|
41
|
+
follow,
|
|
42
|
+
timestamps,
|
|
43
|
+
tailLines,
|
|
44
|
+
})
|
|
45
|
+
.catch((err) => {
|
|
46
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
47
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
48
|
+
ws.close();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
ws.on('message', (data: Buffer) => {
|
|
53
|
+
try {
|
|
54
|
+
const msg = JSON.parse(data.toString());
|
|
55
|
+
if (msg.type === 'ping') {
|
|
56
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore non-JSON messages
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ws.on('close', () => {
|
|
64
|
+
logStream.destroy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ws.on('error', () => {
|
|
68
|
+
logStream.destroy();
|
|
69
|
+
});
|
|
70
|
+
}
|