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.
Files changed (160) hide show
  1. package/README.md +219 -0
  2. package/electron/main.js +429 -0
  3. package/electron/preload.js +13 -0
  4. package/electron-builder.yml +60 -0
  5. package/next.config.ts +8 -0
  6. package/package.json +98 -0
  7. package/postcss.config.mjs +7 -0
  8. package/resources/icon.icns +0 -0
  9. package/resources/icon.ico +0 -0
  10. package/resources/icon.png +0 -0
  11. package/resources/icon.svg +86 -0
  12. package/scripts/build-server.mjs +20 -0
  13. package/scripts/generate-icons.mjs +61 -0
  14. package/server.ts +58 -0
  15. package/src/app/api/clusters/[clusterId]/health/route.ts +27 -0
  16. package/src/app/api/clusters/[clusterId]/metrics/route.ts +196 -0
  17. package/src/app/api/clusters/[clusterId]/namespaces/route.ts +48 -0
  18. package/src/app/api/clusters/[clusterId]/nodes/[nodeName]/route.ts +21 -0
  19. package/src/app/api/clusters/[clusterId]/nodes/route.ts +31 -0
  20. package/src/app/api/clusters/[clusterId]/resources/[namespace]/[...resourcePath]/route.ts +204 -0
  21. package/src/app/api/clusters/route.ts +48 -0
  22. package/src/app/api/kubeconfig/route.ts +25 -0
  23. package/src/app/api/port-forward/route.ts +143 -0
  24. package/src/app/api/tsh/login/route.ts +50 -0
  25. package/src/app/clusters/[clusterId]/app-map/page.tsx +42 -0
  26. package/src/app/clusters/[clusterId]/clusterrolebindings/[name]/page.tsx +5 -0
  27. package/src/app/clusters/[clusterId]/clusterrolebindings/page.tsx +5 -0
  28. package/src/app/clusters/[clusterId]/clusterroles/[name]/page.tsx +5 -0
  29. package/src/app/clusters/[clusterId]/clusterroles/page.tsx +5 -0
  30. package/src/app/clusters/[clusterId]/layout.tsx +9 -0
  31. package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/[name]/page.tsx +5 -0
  32. package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/page.tsx +5 -0
  33. package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/[name]/page.tsx +5 -0
  34. package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/page.tsx +5 -0
  35. package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/[name]/page.tsx +5 -0
  36. package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/page.tsx +5 -0
  37. package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/[name]/page.tsx +457 -0
  38. package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/page.tsx +5 -0
  39. package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/[name]/page.tsx +5 -0
  40. package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/page.tsx +5 -0
  41. package/src/app/clusters/[clusterId]/namespaces/[namespace]/events/page.tsx +5 -0
  42. package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/[name]/page.tsx +5 -0
  43. package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/page.tsx +5 -0
  44. package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/[name]/page.tsx +5 -0
  45. package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/page.tsx +5 -0
  46. package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/[name]/page.tsx +5 -0
  47. package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/page.tsx +5 -0
  48. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/exec/page.tsx +173 -0
  49. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/logs/page.tsx +137 -0
  50. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/page.tsx +448 -0
  51. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/page.tsx +5 -0
  52. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/[name]/page.tsx +5 -0
  53. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/page.tsx +5 -0
  54. package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/[name]/page.tsx +5 -0
  55. package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/page.tsx +5 -0
  56. package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/[name]/page.tsx +5 -0
  57. package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/page.tsx +5 -0
  58. package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/[name]/page.tsx +5 -0
  59. package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/page.tsx +5 -0
  60. package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/[name]/page.tsx +168 -0
  61. package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/page.tsx +5 -0
  62. package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/[name]/page.tsx +5 -0
  63. package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/page.tsx +5 -0
  64. package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/[name]/page.tsx +5 -0
  65. package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/page.tsx +5 -0
  66. package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/[name]/page.tsx +302 -0
  67. package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/page.tsx +5 -0
  68. package/src/app/clusters/[clusterId]/nodes/[nodeName]/page.tsx +5 -0
  69. package/src/app/clusters/[clusterId]/nodes/page.tsx +5 -0
  70. package/src/app/clusters/[clusterId]/page.tsx +635 -0
  71. package/src/app/clusters/[clusterId]/port-forwarding/page.tsx +145 -0
  72. package/src/app/clusters/[clusterId]/pvs/[name]/page.tsx +5 -0
  73. package/src/app/clusters/[clusterId]/pvs/page.tsx +5 -0
  74. package/src/app/clusters/page.tsx +166 -0
  75. package/src/app/favicon.ico +0 -0
  76. package/src/app/globals.css +167 -0
  77. package/src/app/layout.tsx +48 -0
  78. package/src/app/page.tsx +5 -0
  79. package/src/components/clusters/cluster-selector.tsx +64 -0
  80. package/src/components/layout/app-shell.tsx +26 -0
  81. package/src/components/layout/breadcrumbs.tsx +97 -0
  82. package/src/components/layout/command-palette.tsx +112 -0
  83. package/src/components/layout/header.tsx +184 -0
  84. package/src/components/layout/sidebar.tsx +84 -0
  85. package/src/components/layout/theme-toggle.tsx +21 -0
  86. package/src/components/namespaces/namespace-selector.tsx +165 -0
  87. package/src/components/panel/bottom-panel.tsx +127 -0
  88. package/src/components/panel/logs-tab.tsx +109 -0
  89. package/src/components/panel/terminal-tab.tsx +180 -0
  90. package/src/components/pods/pod-watch-button.tsx +44 -0
  91. package/src/components/resources/resource-columns.tsx +320 -0
  92. package/src/components/resources/resource-detail-page.tsx +191 -0
  93. package/src/components/resources/resource-list-page.tsx +78 -0
  94. package/src/components/resources/scale-dialog.tsx +107 -0
  95. package/src/components/settings/settings-dialog.tsx +103 -0
  96. package/src/components/shared/age-display.tsx +27 -0
  97. package/src/components/shared/confirm-dialog.tsx +52 -0
  98. package/src/components/shared/data-table.tsx +149 -0
  99. package/src/components/shared/env-value-resolver.tsx +570 -0
  100. package/src/components/shared/error-display.tsx +109 -0
  101. package/src/components/shared/loading-skeleton.tsx +25 -0
  102. package/src/components/shared/metrics-charts-impl.tsx +434 -0
  103. package/src/components/shared/metrics-charts.tsx +24 -0
  104. package/src/components/shared/port-forward-btn.tsx +60 -0
  105. package/src/components/shared/resource-info-drawer.tsx +542 -0
  106. package/src/components/shared/resource-node.tsx +157 -0
  107. package/src/components/shared/resource-tree-impl.tsx +228 -0
  108. package/src/components/shared/resource-tree.tsx +20 -0
  109. package/src/components/shared/status-badge.tsx +35 -0
  110. package/src/components/shared/yaml-editor.tsx +438 -0
  111. package/src/components/ui/badge.tsx +48 -0
  112. package/src/components/ui/button.tsx +64 -0
  113. package/src/components/ui/card.tsx +92 -0
  114. package/src/components/ui/command.tsx +184 -0
  115. package/src/components/ui/dialog.tsx +158 -0
  116. package/src/components/ui/dropdown-menu.tsx +257 -0
  117. package/src/components/ui/input.tsx +21 -0
  118. package/src/components/ui/popover.tsx +89 -0
  119. package/src/components/ui/scroll-area.tsx +58 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +28 -0
  122. package/src/components/ui/sheet.tsx +143 -0
  123. package/src/components/ui/skeleton.tsx +13 -0
  124. package/src/components/ui/sonner.tsx +40 -0
  125. package/src/components/ui/table.tsx +116 -0
  126. package/src/components/ui/tabs.tsx +91 -0
  127. package/src/components/ui/tooltip.tsx +57 -0
  128. package/src/hooks/use-age-tick.ts +40 -0
  129. package/src/hooks/use-auto-update.ts +100 -0
  130. package/src/hooks/use-clusters.ts +32 -0
  131. package/src/hooks/use-namespaces.ts +17 -0
  132. package/src/hooks/use-pod-watcher.ts +79 -0
  133. package/src/hooks/use-port-forwards.ts +18 -0
  134. package/src/hooks/use-resource-detail.ts +28 -0
  135. package/src/hooks/use-resource-list.ts +26 -0
  136. package/src/hooks/use-resource-tree.ts +440 -0
  137. package/src/lib/api-client.ts +31 -0
  138. package/src/lib/constants.ts +126 -0
  139. package/src/lib/k8s/client-factory.ts +57 -0
  140. package/src/lib/k8s/kubeconfig-manager.ts +43 -0
  141. package/src/lib/k8s/resource-api.ts +223 -0
  142. package/src/lib/k8s/types.ts +29 -0
  143. package/src/lib/utils.ts +6 -0
  144. package/src/providers/swr-provider.tsx +20 -0
  145. package/src/providers/theme-provider.tsx +17 -0
  146. package/src/stores/cluster-store.ts +32 -0
  147. package/src/stores/namespace-store.ts +27 -0
  148. package/src/stores/panel-store.ts +61 -0
  149. package/src/stores/pod-watcher-store.ts +69 -0
  150. package/src/stores/settings-store.ts +24 -0
  151. package/src/stores/sidebar-store.ts +22 -0
  152. package/src/types/cluster.ts +19 -0
  153. package/src/types/css.d.ts +6 -0
  154. package/src/types/electron.d.ts +25 -0
  155. package/src/types/navigation.ts +4 -0
  156. package/src/types/resource.ts +27 -0
  157. package/tsconfig.json +34 -0
  158. package/ws/exec-handler.ts +112 -0
  159. package/ws/index.ts +2 -0
  160. 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,2 @@
1
+ export { handleLogsConnection } from './logs-handler';
2
+ export { handleExecConnection } from './exec-handler';
@@ -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
+ }