openk8s 1.0.1 → 1.0.3
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 +1 -1
- package/package.json +5 -2
- package/src/app/__tests__/app-state.test.ts +376 -0
- package/src/app/__tests__/components/inspector-tokens.test.ts +101 -0
- package/src/app/__tests__/utils.test.ts +358 -0
- package/src/app/app-actions.ts +262 -0
- package/src/app/app-state.ts +16 -263
- package/src/app/app.tsx +22 -170
- package/src/app/components/detail-sections.tsx +131 -0
- package/src/app/components/footer.tsx +52 -0
- package/src/app/components/header.tsx +37 -0
- package/src/app/components/inspector-tokens.ts +93 -0
- package/src/app/components/inspector.tsx +3 -239
- package/src/app/components/resource-rows.tsx +5 -1
- package/src/app/hooks/keyboard/filter-handlers.ts +40 -0
- package/src/app/hooks/keyboard/global-handlers.ts +134 -0
- package/src/app/hooks/keyboard/helm-handlers.ts +104 -0
- package/src/app/hooks/keyboard/logs-handlers.ts +80 -0
- package/src/app/hooks/keyboard/navigation-handlers.ts +103 -0
- package/src/app/hooks/keyboard/overlay-handlers.ts +138 -0
- package/src/app/hooks/keyboard/port-forward-handlers.ts +71 -0
- package/src/app/hooks/keyboard/shell-edit-handlers.ts +253 -0
- package/src/app/hooks/use-app-keyboard.ts +56 -621
- package/src/app/hooks/use-app-side-effects.ts +1 -1
- package/src/app/hooks/use-clipboard.ts +1 -1
- package/src/app/hooks/use-data-fetching.ts +2 -11
- package/src/app/hooks/use-log-stream.ts +1 -1
- package/src/app/hooks/use-port-forward.ts +1 -1
- package/src/app/use-footer-hints.ts +107 -0
- package/src/index.tsx +4 -0
- package/src/lib/k8s/__tests__/k8s-format.test.ts +42 -0
- package/src/lib/k8s/__tests__/resource-detail-builder.test.ts +215 -0
- package/src/lib/k8s/__tests__/resource-parser.test.ts +455 -0
- package/src/lib/k8s/detail-builders/event-builder.ts +21 -0
- package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +63 -0
- package/src/lib/k8s/detail-builders/node-builder.ts +41 -0
- package/src/lib/k8s/detail-builders/overview-builder.ts +103 -0
- package/src/lib/k8s/detail-builders/pod-builder.ts +140 -0
- package/src/lib/k8s/detail-builders/rbac-builder.ts +57 -0
- package/src/lib/k8s/resource-detail-builder.ts +22 -502
- package/src/lib/kubectl/__tests__/kubectl-helpers.test.ts +343 -0
- package/src/lib/kubectl/__tests__/metrics-utils.test.ts +84 -0
- package/src/lib/kubectl/__tests__/spawn-utils.test.ts +56 -0
- package/src/lib/kubectl/kubectl-helpers.ts +246 -0
- package/src/lib/kubectl/kubectl-service.ts +77 -565
- package/src/lib/kubectl/kubectl-types.ts +248 -0
- package/src/lib/kubectl/metrics-utils.ts +33 -0
- package/src/lib/kubectl/spawn-utils.ts +27 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
import type { AppAction } from "../../app-actions";
|
|
5
|
+
import type { AppState, ResourceRef, ResourceKind, ResourceDetail, NotificationTone } from "../../../lib/k8s/types";
|
|
6
|
+
import { statusLine } from "../../utils";
|
|
7
|
+
import { KubectlService } from "../../../lib/kubectl/kubectl-service";
|
|
8
|
+
|
|
9
|
+
export function handleShellKeys(
|
|
10
|
+
key: { name: string; shift?: boolean },
|
|
11
|
+
state: AppState,
|
|
12
|
+
dispatch: React.Dispatch<AppAction>,
|
|
13
|
+
renderer: { suspend: () => void; resume: () => void },
|
|
14
|
+
activeResourceRef: ResourceRef | undefined,
|
|
15
|
+
selectedKind: ResourceKind | undefined,
|
|
16
|
+
kubectl: KubectlService,
|
|
17
|
+
toastError: (error: unknown) => void,
|
|
18
|
+
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (state.overlay) return false;
|
|
21
|
+
|
|
22
|
+
if (key.name === "x" && activeResourceRef && selectedKind && state.activeContext) {
|
|
23
|
+
if (activeResourceRef.kind.toLowerCase() === "helmreleases") {
|
|
24
|
+
dispatch({ type: "setStatusMessage", message: "Shell not supported for Helm releases" });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dispatch({ type: "setStatusMessage", message: `Opening shell in ${statusLine({ ref: activeResourceRef })}` });
|
|
29
|
+
dispatch({ type: "setError", error: undefined });
|
|
30
|
+
renderer.suspend();
|
|
31
|
+
|
|
32
|
+
const context = state.activeContext;
|
|
33
|
+
const namespace = state.activeNamespace;
|
|
34
|
+
const namespaced = selectedKind.namespaced;
|
|
35
|
+
const target = activeResourceRef;
|
|
36
|
+
|
|
37
|
+
const child = kubectl.startShell({ context, namespace, resourceRef: target, namespaced });
|
|
38
|
+
child.on("close", () => {
|
|
39
|
+
renderer.resume();
|
|
40
|
+
dispatch({ type: "setError", error: undefined });
|
|
41
|
+
dispatch({ type: "setStatusMessage", message: `Returned from shell in ${statusLine({ ref: target })}` });
|
|
42
|
+
setManualRefreshNonce((current) => current + 1);
|
|
43
|
+
});
|
|
44
|
+
child.on("error", (error) => {
|
|
45
|
+
renderer.resume();
|
|
46
|
+
toastError(error);
|
|
47
|
+
dispatch({ type: "setStatusMessage", message: `Shell failed for ${statusLine({ ref: target })}` });
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function handleEditKeys(
|
|
56
|
+
key: { name: string; shift?: boolean },
|
|
57
|
+
state: AppState,
|
|
58
|
+
dispatch: React.Dispatch<AppAction>,
|
|
59
|
+
renderer: { suspend: () => void; resume: () => void },
|
|
60
|
+
activeResourceRef: ResourceRef | undefined,
|
|
61
|
+
selectedKind: ResourceKind | undefined,
|
|
62
|
+
activeDetail: ResourceDetail | undefined,
|
|
63
|
+
kubectl: KubectlService,
|
|
64
|
+
toast: (tone: NotificationTone, message: string) => void,
|
|
65
|
+
toastError: (error: unknown) => void,
|
|
66
|
+
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
67
|
+
): boolean {
|
|
68
|
+
if (state.overlay) return false;
|
|
69
|
+
|
|
70
|
+
if (key.name === "e" && !key.shift && activeResourceRef && selectedKind && state.activeContext) {
|
|
71
|
+
const context = state.activeContext;
|
|
72
|
+
const namespace = state.activeNamespace;
|
|
73
|
+
const namespaced = selectedKind.namespaced;
|
|
74
|
+
const target = activeResourceRef;
|
|
75
|
+
const isHelm = target.kind.toLowerCase() === "helmreleases";
|
|
76
|
+
|
|
77
|
+
if (isHelm) {
|
|
78
|
+
const chartName = activeDetail?.helmChart ?? "";
|
|
79
|
+
const values = activeDetail?.yaml ?? "";
|
|
80
|
+
const tmpPath = `/tmp/openk8s-${target.name}-${Date.now()}.yaml`;
|
|
81
|
+
const cleanupTmp = (): void => { try { unlinkSync(tmpPath); } catch { /* ignore */ } };
|
|
82
|
+
|
|
83
|
+
if (!chartName) {
|
|
84
|
+
dispatch({ type: "setStatusMessage", message: "Chart name unavailable — load the detail pane first" });
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(tmpPath, values, "utf8");
|
|
90
|
+
} catch (err) {
|
|
91
|
+
toastError(err);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
dispatch({ type: "setStatusMessage", message: `Editing values for ${target.name}` });
|
|
96
|
+
dispatch({ type: "setError", error: undefined });
|
|
97
|
+
renderer.suspend();
|
|
98
|
+
|
|
99
|
+
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
100
|
+
const editorChild = spawn(editor, [tmpPath], { env: process.env, stdio: "inherit" });
|
|
101
|
+
|
|
102
|
+
editorChild.on("close", (code) => {
|
|
103
|
+
if (code === 0) {
|
|
104
|
+
kubectl
|
|
105
|
+
.helmUpgradeValues({ context, namespace, name: target.name, chart: chartName, valuesFile: tmpPath })
|
|
106
|
+
.then(() => {
|
|
107
|
+
renderer.resume();
|
|
108
|
+
toast("success", `${target.name} upgraded`);
|
|
109
|
+
setManualRefreshNonce((current) => current + 1);
|
|
110
|
+
})
|
|
111
|
+
.catch((err: unknown) => {
|
|
112
|
+
renderer.resume();
|
|
113
|
+
toastError(err);
|
|
114
|
+
})
|
|
115
|
+
.finally(cleanupTmp);
|
|
116
|
+
} else {
|
|
117
|
+
renderer.resume();
|
|
118
|
+
dispatch({ type: "setStatusMessage", message: "Edit cancelled" });
|
|
119
|
+
cleanupTmp();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
editorChild.on("error", (err) => {
|
|
124
|
+
renderer.resume();
|
|
125
|
+
toastError(err);
|
|
126
|
+
cleanupTmp();
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
dispatch({ type: "setStatusMessage", message: `Editing ${statusLine({ ref: target })}` });
|
|
130
|
+
dispatch({ type: "setError", error: undefined });
|
|
131
|
+
renderer.suspend();
|
|
132
|
+
|
|
133
|
+
const editChild = kubectl.editResource({ context, namespace, resourceRef: target, namespaced });
|
|
134
|
+
|
|
135
|
+
editChild.on("close", (code) => {
|
|
136
|
+
renderer.resume();
|
|
137
|
+
if (code === 0) {
|
|
138
|
+
dispatch({ type: "setStatusMessage", message: `Saved ${statusLine({ ref: target })}` });
|
|
139
|
+
setManualRefreshNonce((current) => current + 1);
|
|
140
|
+
} else if (code !== null) {
|
|
141
|
+
dispatch({ type: "setStatusMessage", message: `Edit cancelled for ${statusLine({ ref: target })}` });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
editChild.on("error", (err) => {
|
|
146
|
+
renderer.resume();
|
|
147
|
+
toastError(err);
|
|
148
|
+
dispatch({ type: "setStatusMessage", message: `Edit failed for ${statusLine({ ref: target })}` });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Shift+S → scale dialog
|
|
155
|
+
if (key.shift && key.name === "s" && activeResourceRef && selectedKind && state.activeContext && kubectl.canScale(activeResourceRef)) {
|
|
156
|
+
return false; // handled by scale handler
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function handleScaleKeys(
|
|
163
|
+
key: { name: string },
|
|
164
|
+
state: AppState,
|
|
165
|
+
dispatch: React.Dispatch<AppAction>,
|
|
166
|
+
activeResourceRef: ResourceRef | undefined,
|
|
167
|
+
selectedKind: ResourceKind | undefined,
|
|
168
|
+
scaleReplicasInput: string,
|
|
169
|
+
kubectl: KubectlService,
|
|
170
|
+
toastError: (error: unknown) => void,
|
|
171
|
+
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
172
|
+
): boolean {
|
|
173
|
+
if (state.overlay !== "scale") return false;
|
|
174
|
+
|
|
175
|
+
if (key.name === "return" && activeResourceRef && selectedKind && state.activeContext) {
|
|
176
|
+
const replicas = Number.parseInt(scaleReplicasInput, 10);
|
|
177
|
+
|
|
178
|
+
if (!Number.isNaN(replicas) && replicas >= 0) {
|
|
179
|
+
const context = state.activeContext;
|
|
180
|
+
const namespace = state.activeNamespace;
|
|
181
|
+
const namespaced = selectedKind.namespaced;
|
|
182
|
+
const target = activeResourceRef;
|
|
183
|
+
|
|
184
|
+
dispatch({ type: "setStatusMessage", message: `Scaling ${statusLine({ ref: target })} to ${replicas}` });
|
|
185
|
+
dispatch({ type: "setError", error: undefined });
|
|
186
|
+
dispatch({ type: "setOverlay", overlay: undefined });
|
|
187
|
+
|
|
188
|
+
void kubectl
|
|
189
|
+
.scaleResource({ context, namespace, resourceRef: target, namespaced, replicas })
|
|
190
|
+
.then(() => {
|
|
191
|
+
dispatch({
|
|
192
|
+
type: "setStatusMessage",
|
|
193
|
+
message: `Scaled ${statusLine({ ref: target })} to ${replicas}`,
|
|
194
|
+
});
|
|
195
|
+
setManualRefreshNonce((current) => current + 1);
|
|
196
|
+
})
|
|
197
|
+
.catch((error: unknown) => {
|
|
198
|
+
toastError(error);
|
|
199
|
+
dispatch({
|
|
200
|
+
type: "setStatusMessage",
|
|
201
|
+
message: `Scale failed for ${statusLine({ ref: target })}`,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function handleRolloutRestartKeys(
|
|
212
|
+
key: { name: string; shift?: boolean },
|
|
213
|
+
state: AppState,
|
|
214
|
+
dispatch: React.Dispatch<AppAction>,
|
|
215
|
+
activeResourceRef: ResourceRef | undefined,
|
|
216
|
+
selectedKind: ResourceKind | undefined,
|
|
217
|
+
kubectl: KubectlService,
|
|
218
|
+
toastError: (error: unknown) => void,
|
|
219
|
+
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
220
|
+
): boolean {
|
|
221
|
+
if (state.overlay) return false;
|
|
222
|
+
|
|
223
|
+
if (key.shift && key.name === "r") {
|
|
224
|
+
if (activeResourceRef && selectedKind && state.activeContext && kubectl.canRolloutRestart(activeResourceRef)) {
|
|
225
|
+
const context = state.activeContext;
|
|
226
|
+
const namespace = state.activeNamespace;
|
|
227
|
+
const namespaced = selectedKind.namespaced;
|
|
228
|
+
const target = activeResourceRef;
|
|
229
|
+
|
|
230
|
+
dispatch({ type: "setStatusMessage", message: `Restarting ${statusLine({ ref: target })}` });
|
|
231
|
+
dispatch({ type: "setError", error: undefined });
|
|
232
|
+
|
|
233
|
+
void kubectl
|
|
234
|
+
.rolloutRestart({ context, namespace, resourceRef: target, namespaced })
|
|
235
|
+
.then(() => {
|
|
236
|
+
dispatch({ type: "setStatusMessage", message: `Restarted ${statusLine({ ref: target })}` });
|
|
237
|
+
setManualRefreshNonce((current) => current + 1);
|
|
238
|
+
})
|
|
239
|
+
.catch((error: unknown) => {
|
|
240
|
+
toastError(error);
|
|
241
|
+
dispatch({
|
|
242
|
+
type: "setStatusMessage",
|
|
243
|
+
message: `Rollout restart failed for ${statusLine({ ref: target })}`,
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
dispatch({ type: "setStatusMessage", message: "Rollout restart not supported for this resource" });
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return false;
|
|
253
|
+
}
|