openk8s 1.0.1 → 1.0.2

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 (47) hide show
  1. package/package.json +5 -2
  2. package/src/app/__tests__/app-state.test.ts +376 -0
  3. package/src/app/__tests__/components/inspector-tokens.test.ts +101 -0
  4. package/src/app/__tests__/utils.test.ts +358 -0
  5. package/src/app/app-actions.ts +262 -0
  6. package/src/app/app-state.ts +4 -262
  7. package/src/app/app.tsx +22 -170
  8. package/src/app/components/detail-sections.tsx +131 -0
  9. package/src/app/components/footer.tsx +52 -0
  10. package/src/app/components/header.tsx +37 -0
  11. package/src/app/components/inspector-tokens.ts +93 -0
  12. package/src/app/components/inspector.tsx +3 -239
  13. package/src/app/components/resource-rows.tsx +5 -0
  14. package/src/app/hooks/keyboard/filter-handlers.ts +40 -0
  15. package/src/app/hooks/keyboard/global-handlers.ts +134 -0
  16. package/src/app/hooks/keyboard/helm-handlers.ts +104 -0
  17. package/src/app/hooks/keyboard/logs-handlers.ts +80 -0
  18. package/src/app/hooks/keyboard/navigation-handlers.ts +103 -0
  19. package/src/app/hooks/keyboard/overlay-handlers.ts +138 -0
  20. package/src/app/hooks/keyboard/port-forward-handlers.ts +71 -0
  21. package/src/app/hooks/keyboard/shell-edit-handlers.ts +253 -0
  22. package/src/app/hooks/use-app-keyboard.ts +56 -621
  23. package/src/app/hooks/use-app-side-effects.ts +1 -1
  24. package/src/app/hooks/use-clipboard.ts +1 -1
  25. package/src/app/hooks/use-data-fetching.ts +2 -9
  26. package/src/app/hooks/use-log-stream.ts +1 -1
  27. package/src/app/hooks/use-port-forward.ts +1 -1
  28. package/src/app/use-footer-hints.ts +107 -0
  29. package/src/index.tsx +4 -0
  30. package/src/lib/k8s/__tests__/k8s-format.test.ts +42 -0
  31. package/src/lib/k8s/__tests__/resource-detail-builder.test.ts +215 -0
  32. package/src/lib/k8s/__tests__/resource-parser.test.ts +455 -0
  33. package/src/lib/k8s/detail-builders/event-builder.ts +21 -0
  34. package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +63 -0
  35. package/src/lib/k8s/detail-builders/node-builder.ts +41 -0
  36. package/src/lib/k8s/detail-builders/overview-builder.ts +103 -0
  37. package/src/lib/k8s/detail-builders/pod-builder.ts +140 -0
  38. package/src/lib/k8s/detail-builders/rbac-builder.ts +57 -0
  39. package/src/lib/k8s/resource-detail-builder.ts +22 -502
  40. package/src/lib/kubectl/__tests__/kubectl-helpers.test.ts +343 -0
  41. package/src/lib/kubectl/__tests__/metrics-utils.test.ts +84 -0
  42. package/src/lib/kubectl/__tests__/spawn-utils.test.ts +56 -0
  43. package/src/lib/kubectl/kubectl-helpers.ts +246 -0
  44. package/src/lib/kubectl/kubectl-service.ts +77 -565
  45. package/src/lib/kubectl/kubectl-types.ts +248 -0
  46. package/src/lib/kubectl/metrics-utils.ts +33 -0
  47. package/src/lib/kubectl/spawn-utils.ts +27 -0
@@ -0,0 +1,246 @@
1
+ import { formatAge } from "../k8s/k8s-format";
2
+ import type { ResourceKind, ResourceRef } from "../k8s/types";
3
+ import type { GenericListItem } from "./kubectl-types";
4
+
5
+ export function normalizeKind(kind: string): string {
6
+ return kind.trim().toLowerCase();
7
+ }
8
+
9
+ export function normalizeResourceTarget(ref: ResourceRef): string {
10
+ const kind = normalizeKind(ref.kind);
11
+ return `${kind}/${ref.name}`;
12
+ }
13
+
14
+ export function normalizeLogTarget(ref: ResourceRef): { target: string; includeAllPods: boolean } | undefined {
15
+ const kind = normalizeKind(ref.kind);
16
+
17
+ if (kind === "pod" || kind === "pods") {
18
+ return { target: `pod/${ref.name}`, includeAllPods: false };
19
+ }
20
+
21
+ if (kind === "deployment" || kind === "deployments") {
22
+ return { target: `deployment/${ref.name}`, includeAllPods: true };
23
+ }
24
+
25
+ if (kind === "job" || kind === "jobs") {
26
+ return { target: `job/${ref.name}`, includeAllPods: true };
27
+ }
28
+
29
+ if (kind === "replicaset" || kind === "replicasets") {
30
+ return { target: `replicaset/${ref.name}`, includeAllPods: true };
31
+ }
32
+
33
+ if (kind === "statefulset" || kind === "statefulsets") {
34
+ return { target: `statefulset/${ref.name}`, includeAllPods: true };
35
+ }
36
+
37
+ if (kind === "daemonset" || kind === "daemonsets") {
38
+ return { target: `daemonset/${ref.name}`, includeAllPods: true };
39
+ }
40
+
41
+ return undefined;
42
+ }
43
+
44
+ export function normalizePortForwardTarget(ref: ResourceRef): string | undefined {
45
+ const kind = normalizeKind(ref.kind);
46
+
47
+ if (kind === "pod" || kind === "pods") {
48
+ return `pod/${ref.name}`;
49
+ }
50
+
51
+ if (kind === "service" || kind === "services") {
52
+ return `service/${ref.name}`;
53
+ }
54
+
55
+ if (kind === "deployment" || kind === "deployments") {
56
+ return `deployment/${ref.name}`;
57
+ }
58
+
59
+ if (kind === "replicaset" || kind === "replicasets") {
60
+ return `replicaset/${ref.name}`;
61
+ }
62
+
63
+ if (kind === "statefulset" || kind === "statefulsets") {
64
+ return `statefulset/${ref.name}`;
65
+ }
66
+
67
+ return undefined;
68
+ }
69
+
70
+ export function supportsRolloutRestart(ref: ResourceRef): boolean {
71
+ const kind = normalizeKind(ref.kind);
72
+ return ["deployment", "deployments", "statefulset", "statefulsets", "daemonset", "daemonsets"].includes(kind);
73
+ }
74
+
75
+ export function supportsScale(ref: ResourceRef): boolean {
76
+ const kind = normalizeKind(ref.kind);
77
+ return ["deployment", "deployments", "statefulset", "statefulsets", "replicaset", "replicasets"].includes(kind);
78
+ }
79
+
80
+ export function parseKubectlError(stderr: string, args: string[]): Error {
81
+ const message = stderr.trim() || `kubectl ${args.join(" ")} failed`;
82
+ return new Error(message);
83
+ }
84
+
85
+ export function resourceStatus(resource: GenericListItem): string {
86
+ const kind = (resource.kind ?? "").toLowerCase();
87
+ const status = resource.status ?? {};
88
+ const spec = resource.spec ?? {};
89
+
90
+ if (kind === "event" || kind === "events") {
91
+ return resource.type ?? "Normal";
92
+ }
93
+
94
+ if (kind === "horizontalpodautoscaler") {
95
+ const current = typeof status.currentReplicas === "number" ? status.currentReplicas : "-";
96
+ const max = typeof spec.maxReplicas === "number" ? spec.maxReplicas : "-";
97
+ return `${current}/${max} replicas`;
98
+ }
99
+
100
+ if (kind === "cronjob") {
101
+ const active = Array.isArray(status.active) ? status.active : [];
102
+ return active.length > 0 ? "active" : "idle";
103
+ }
104
+
105
+ if (kind === "node") {
106
+ const conditions = Array.isArray(status.conditions) ? (status.conditions as Record<string, unknown>[]) : [];
107
+ const ready = conditions.find((c) => c.type === "Ready");
108
+ const memPressure = conditions.find((c) => c.type === "MemoryPressure" && c.status === "True");
109
+ const diskPressure = conditions.find((c) => c.type === "DiskPressure" && c.status === "True");
110
+
111
+ if (memPressure) return "MemoryPressure";
112
+ if (diskPressure) return "DiskPressure";
113
+ return ready?.status === "True" ? "Ready" : "NotReady";
114
+ }
115
+
116
+ if (kind === "rolebinding" || kind === "clusterrolebinding") {
117
+ const subjects = Array.isArray((resource as unknown as Record<string, unknown>).subjects)
118
+ ? ((resource as unknown as Record<string, unknown>).subjects as unknown[])
119
+ : [];
120
+ return `${subjects.length} subjects`;
121
+ }
122
+
123
+ if (typeof status.phase === "string") {
124
+ return status.phase;
125
+ }
126
+
127
+ if (typeof status.readyReplicas === "number" && typeof spec.replicas === "number") {
128
+ return `${status.readyReplicas}/${spec.replicas} ready`;
129
+ }
130
+
131
+ if (typeof status.availableReplicas === "number" && typeof spec.replicas === "number") {
132
+ return `${status.availableReplicas}/${spec.replicas} available`;
133
+ }
134
+
135
+ if (typeof status.capacity === "object") {
136
+ return "capacity";
137
+ }
138
+
139
+ return "-";
140
+ }
141
+
142
+ export function resourceSummary(resource: GenericListItem): string {
143
+ const kind = (resource.kind ?? "").toLowerCase();
144
+ const spec = resource.spec ?? {};
145
+ const status = resource.status ?? {};
146
+
147
+ if (kind === "event" || kind === "events") {
148
+ const reason = resource.reason ?? "";
149
+ const message = (resource.message ?? "").slice(0, 60);
150
+ return reason ? `${reason}: ${message}` : message;
151
+ }
152
+
153
+ if (kind === "horizontalpodautoscaler") {
154
+ const min = spec.minReplicas ?? "-";
155
+ const max = spec.maxReplicas ?? "-";
156
+ const ref = (spec.scaleTargetRef as Record<string, unknown> | undefined)?.name ?? "-";
157
+ return `min:${min} max:${max} ref:${ref}`;
158
+ }
159
+
160
+ if (kind === "cronjob") {
161
+ const schedule = typeof spec.schedule === "string" ? spec.schedule : "-";
162
+ const lastRun = typeof status.lastScheduleTime === "string" ? formatAge(status.lastScheduleTime) : "-";
163
+ return `${schedule} last: ${lastRun}`;
164
+ }
165
+
166
+ if (kind === "node") {
167
+ const nodeInfo = (status.nodeInfo ?? {}) as Record<string, unknown>;
168
+ const kubeletVersion = typeof nodeInfo.kubeletVersion === "string" ? nodeInfo.kubeletVersion : "";
169
+ const labels = (resource as unknown as { metadata?: { labels?: Record<string, string> } }).metadata?.labels ?? {};
170
+ const roles = Object.keys(labels)
171
+ .filter((k) => k.startsWith("node-role.kubernetes.io/"))
172
+ .map((k) => k.replace("node-role.kubernetes.io/", ""))
173
+ .join(",") || "worker";
174
+ return kubeletVersion ? `${kubeletVersion} roles: ${roles}` : `roles: ${roles}`;
175
+ }
176
+
177
+ if (kind === "role" || kind === "clusterrole") {
178
+ const rules = (resource as unknown as { rules?: unknown[] }).rules ?? [];
179
+ return `${rules.length} rules`;
180
+ }
181
+
182
+ if (kind === "rolebinding" || kind === "clusterrolebinding") {
183
+ const roleRef = (resource as unknown as { roleRef?: Record<string, unknown> }).roleRef;
184
+ if (roleRef) {
185
+ return `\u2192 ${roleRef.kind ?? ""}/${roleRef.name ?? ""}`;
186
+ }
187
+ return "";
188
+ }
189
+
190
+ if (typeof spec.clusterIP === "string") {
191
+ return `clusterIP ${spec.clusterIP}`;
192
+ }
193
+
194
+ if (typeof spec.nodeName === "string") {
195
+ return `node ${spec.nodeName}`;
196
+ }
197
+
198
+ if (typeof status.hostIP === "string") {
199
+ return `host ${status.hostIP}`;
200
+ }
201
+
202
+ if (typeof status.podIP === "string") {
203
+ return `podIP ${status.podIP}`;
204
+ }
205
+
206
+ return "";
207
+ }
208
+
209
+ export function curatedKinds(kinds: ResourceKind[]): ResourceKind[] {
210
+ const preferredOrder = [
211
+ "pods",
212
+ "deployments",
213
+ "statefulsets",
214
+ "daemonsets",
215
+ "services",
216
+ "ingresses",
217
+ "jobs",
218
+ "cronjobs",
219
+ "horizontalpodautoscalers",
220
+ "configmaps",
221
+ "secrets",
222
+ "nodes",
223
+ "namespaces",
224
+ "events",
225
+ "persistentvolumeclaims",
226
+ "persistentvolumes",
227
+ "serviceaccounts",
228
+ "roles",
229
+ "clusterroles",
230
+ "rolebindings",
231
+ "clusterrolebindings",
232
+ "networkpolicies",
233
+ "poddisruptionbudgets",
234
+ ];
235
+
236
+ const kindMap = new Map(kinds.map((kind) => [kind.name, kind]));
237
+ const curated = preferredOrder
238
+ .map((name) => kindMap.get(name))
239
+ .filter((kind): kind is ResourceKind => kind !== undefined);
240
+
241
+ if (curated.length > 0) {
242
+ return curated;
243
+ }
244
+
245
+ return kinds.slice(0, 20);
246
+ }