openk8s 1.0.5 → 1.2.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 +1 -1
- package/package.json +13 -10
- package/src/app/__tests__/app-state.test.ts +48 -6
- package/src/app/__tests__/utils.test.ts +6 -15
- package/src/app/app-actions.ts +5 -0
- package/src/app/app-state.ts +11 -2
- package/src/app/app.tsx +57 -46
- package/src/app/components/detail-sections.tsx +2 -2
- package/src/app/components/footer.tsx +23 -30
- package/src/app/components/inspector.tsx +40 -83
- package/src/app/components/kind-rows.tsx +1 -1
- package/src/app/components/notification-card.tsx +145 -0
- package/src/app/components/notification-tray.tsx +19 -38
- package/src/app/components/overlays/delete-confirm-overlay.tsx +15 -17
- package/src/app/components/overlays/helm-rollback-overlay.tsx +12 -66
- package/src/app/components/overlays/help-overlay.tsx +171 -0
- package/src/app/components/overlays/index.ts +4 -1
- package/src/app/components/overlays/logs-dialog.tsx +47 -67
- package/src/app/components/overlays/notification-history-overlay.tsx +79 -0
- package/src/app/components/overlays/port-forward-list-overlay.tsx +85 -0
- package/src/app/components/overlays/port-forward-overlay.tsx +16 -56
- package/src/app/components/overlays/scale-dialog.tsx +12 -67
- package/src/app/components/overlays/select-overlay.tsx +5 -14
- package/src/app/components/overlays/shared.tsx +85 -6
- package/src/app/components/resource-rows.tsx +1 -1
- package/src/app/constants.ts +24 -0
- package/src/app/hooks/keyboard/filter-handlers.ts +2 -1
- package/src/app/hooks/keyboard/global-handlers.ts +30 -21
- package/src/app/hooks/keyboard/helm-handlers.ts +14 -9
- package/src/app/hooks/keyboard/keys.ts +18 -0
- package/src/app/hooks/keyboard/logs-handlers.ts +3 -1
- package/src/app/hooks/keyboard/navigation-handlers.ts +5 -4
- package/src/app/hooks/keyboard/overlay-handlers.ts +11 -7
- package/src/app/hooks/keyboard/port-forward-handlers.ts +19 -14
- package/src/app/hooks/keyboard/shell-edit-handlers.ts +32 -16
- package/src/app/hooks/use-app-keyboard.ts +22 -2
- package/src/app/hooks/use-app-side-effects.ts +10 -7
- package/src/app/hooks/use-clipboard.ts +8 -10
- package/src/app/hooks/use-data-fetching.ts +10 -6
- package/src/app/hooks/use-log-stream.ts +8 -4
- package/src/app/hooks/use-notifications.ts +92 -0
- package/src/app/hooks/use-port-forward.ts +19 -4
- package/src/app/persistence.ts +7 -3
- package/src/app/syntax-theme.ts +31 -0
- package/src/app/theme.ts +2 -3
- package/src/app/use-footer-hints.ts +21 -16
- package/src/app/utils.ts +1 -9
- package/src/assets/tree-sitter/yaml/highlights.scm +79 -0
- package/src/assets/tree-sitter/yaml/tree-sitter-yaml.wasm +0 -0
- package/src/index.tsx +22 -2
- package/src/lib/k8s/__tests__/k8s-format.test.ts +17 -0
- package/src/lib/k8s/__tests__/resource-detail-builder.test.ts +4 -6
- package/src/lib/k8s/__tests__/resource-parser.test.ts +40 -2
- package/src/lib/k8s/detail-builders/event-builder.ts +18 -12
- package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +34 -31
- package/src/lib/k8s/detail-builders/node-builder.ts +22 -11
- package/src/lib/k8s/detail-builders/overview-builder.ts +4 -17
- package/src/lib/k8s/detail-builders/pod-builder.ts +52 -24
- package/src/lib/k8s/detail-builders/rbac-builder.ts +20 -29
- package/src/lib/k8s/k8s-format.ts +14 -9
- package/src/lib/k8s/resource-detail-builder.ts +4 -7
- package/src/lib/k8s/resource-parser.ts +17 -2
- package/src/lib/k8s/types.ts +12 -4
- package/src/lib/kubectl/__tests__/metrics-utils.test.ts +9 -0
- package/src/lib/kubectl/kubectl-helpers.ts +16 -11
- package/src/lib/kubectl/kubectl-service.ts +38 -54
- package/src/lib/kubectl/kubectl-types.ts +10 -1
- package/src/lib/kubectl/metrics-utils.ts +21 -7
- package/src/lib/kubectl/spawn-utils.ts +50 -11
- package/src/app/__tests__/components/inspector-tokens.test.ts +0 -101
- package/src/app/components/inspector-tokens.ts +0 -93
- package/src/app/components/port-forwards-tray.tsx +0 -57
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
2
|
|
|
3
|
+
import { dump as dumpYaml } from "js-yaml";
|
|
3
4
|
import { formatAge } from "../k8s/k8s-format";
|
|
4
5
|
import { buildResourceDetail, type KubernetesResource } from "../k8s/resource-detail-builder";
|
|
5
6
|
import type {
|
|
@@ -84,6 +85,24 @@ export type {
|
|
|
84
85
|
|
|
85
86
|
const KUBECTL_TIMEOUT_MS = 15_000;
|
|
86
87
|
|
|
88
|
+
interface NamespacedOptions {
|
|
89
|
+
context: string;
|
|
90
|
+
namespace?: string | undefined;
|
|
91
|
+
namespaced?: boolean | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildContextArgs(options: NamespacedOptions): string[] {
|
|
95
|
+
const args = ["--context", options.context];
|
|
96
|
+
if (options.namespaced && options.namespace !== undefined) {
|
|
97
|
+
args.push("-n", options.namespace);
|
|
98
|
+
}
|
|
99
|
+
return args;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildHelmBaseArgs(options: { context: string; namespace: string }): string[] {
|
|
103
|
+
return ["--kube-context", options.context, "-n", options.namespace];
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
export class KubectlService {
|
|
88
107
|
async isAvailable(): Promise<boolean> {
|
|
89
108
|
try {
|
|
@@ -201,25 +220,17 @@ export class KubectlService {
|
|
|
201
220
|
}
|
|
202
221
|
|
|
203
222
|
const jsonArgs = ["--context", options.context, "get", options.resourceKind.name, options.name];
|
|
204
|
-
const yamlArgs = ["--context", options.context, "get", options.resourceKind.name, options.name];
|
|
205
|
-
const describeArgs = ["--context", options.context, "describe", options.resourceKind.name, options.name];
|
|
206
223
|
|
|
207
224
|
if (options.resourceKind.namespaced) {
|
|
208
225
|
jsonArgs.push("-n", options.namespace);
|
|
209
|
-
yamlArgs.push("-n", options.namespace);
|
|
210
|
-
describeArgs.push("-n", options.namespace);
|
|
211
226
|
}
|
|
212
227
|
|
|
213
228
|
jsonArgs.push("-o", "json");
|
|
214
|
-
yamlArgs.push("-o", "yaml");
|
|
215
229
|
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
runText({ command: "kubectl", args: yamlArgs, timeoutMs: KUBECTL_TIMEOUT_MS }),
|
|
219
|
-
runText({ command: "kubectl", args: describeArgs, timeoutMs: KUBECTL_TIMEOUT_MS }).catch(() => ""),
|
|
220
|
-
]);
|
|
230
|
+
const resource = await runJson<KubernetesResource>({ command: "kubectl", args: jsonArgs, timeoutMs: KUBECTL_TIMEOUT_MS });
|
|
231
|
+
const yamlText = dumpYaml(resource, { lineWidth: -1, noRefs: true });
|
|
221
232
|
|
|
222
|
-
return buildResourceDetail({ resourceKind: options.resourceKind, resource, yaml
|
|
233
|
+
return buildResourceDetail({ resourceKind: options.resourceKind, resource, yaml: yamlText });
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
async listResourceEvents(options: ListResourceEventsOptions): Promise<EventItem[]> {
|
|
@@ -284,11 +295,7 @@ export class KubectlService {
|
|
|
284
295
|
throw new Error(`Logs are not supported yet for ${options.resourceRef.kind}`);
|
|
285
296
|
}
|
|
286
297
|
|
|
287
|
-
const args =
|
|
288
|
-
|
|
289
|
-
if (options.namespaced) {
|
|
290
|
-
args.push("-n", options.namespace);
|
|
291
|
-
}
|
|
298
|
+
const args = buildContextArgs(options);
|
|
292
299
|
|
|
293
300
|
const tail = options.logOptions?.tail ?? 120;
|
|
294
301
|
const since = options.logOptions?.since;
|
|
@@ -307,7 +314,7 @@ export class KubectlService {
|
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
if (logTarget.includeAllPods) {
|
|
310
|
-
args.push("--all-
|
|
317
|
+
args.push("--all-containers=true");
|
|
311
318
|
}
|
|
312
319
|
|
|
313
320
|
if (options.container) {
|
|
@@ -325,11 +332,7 @@ export class KubectlService {
|
|
|
325
332
|
throw new Error(`Scale is not supported yet for ${options.resourceRef.kind}`);
|
|
326
333
|
}
|
|
327
334
|
|
|
328
|
-
const args =
|
|
329
|
-
|
|
330
|
-
if (options.namespaced) {
|
|
331
|
-
args.push("-n", options.namespace);
|
|
332
|
-
}
|
|
335
|
+
const args = buildContextArgs(options);
|
|
333
336
|
|
|
334
337
|
args.push("scale", normalizeResourceTarget(options.resourceRef), `--replicas=${options.replicas}`);
|
|
335
338
|
|
|
@@ -341,11 +344,7 @@ export class KubectlService {
|
|
|
341
344
|
throw new Error(`Rollout restart is not supported yet for ${options.resourceRef.kind}`);
|
|
342
345
|
}
|
|
343
346
|
|
|
344
|
-
const args =
|
|
345
|
-
|
|
346
|
-
if (options.namespaced) {
|
|
347
|
-
args.push("-n", options.namespace);
|
|
348
|
-
}
|
|
347
|
+
const args = buildContextArgs(options);
|
|
349
348
|
|
|
350
349
|
args.push("rollout", "restart", normalizeResourceTarget(options.resourceRef));
|
|
351
350
|
|
|
@@ -357,11 +356,7 @@ export class KubectlService {
|
|
|
357
356
|
return this.uninstallHelmRelease(options);
|
|
358
357
|
}
|
|
359
358
|
|
|
360
|
-
const args =
|
|
361
|
-
|
|
362
|
-
if (options.namespaced) {
|
|
363
|
-
args.push("-n", options.namespace);
|
|
364
|
-
}
|
|
359
|
+
const args = buildContextArgs(options);
|
|
365
360
|
|
|
366
361
|
args.push("delete", normalizeResourceTarget(options.resourceRef));
|
|
367
362
|
|
|
@@ -369,11 +364,7 @@ export class KubectlService {
|
|
|
369
364
|
}
|
|
370
365
|
|
|
371
366
|
startShell(options: StartShellOptions): ChildProcess {
|
|
372
|
-
const args =
|
|
373
|
-
|
|
374
|
-
if (options.namespaced) {
|
|
375
|
-
args.push("-n", options.namespace);
|
|
376
|
-
}
|
|
367
|
+
const args = buildContextArgs(options);
|
|
377
368
|
|
|
378
369
|
const kind = normalizeKind(options.resourceRef.kind);
|
|
379
370
|
const target = kind === "pod" || kind === "pods" ? `pod/${options.resourceRef.name}` : normalizeResourceTarget(options.resourceRef);
|
|
@@ -384,11 +375,7 @@ export class KubectlService {
|
|
|
384
375
|
}
|
|
385
376
|
|
|
386
377
|
editResource(options: EditResourceOptions): ChildProcess {
|
|
387
|
-
const args =
|
|
388
|
-
|
|
389
|
-
if (options.namespaced) {
|
|
390
|
-
args.push("-n", options.namespace);
|
|
391
|
-
}
|
|
378
|
+
const args = buildContextArgs(options);
|
|
392
379
|
|
|
393
380
|
args.push("edit", normalizeResourceTarget(options.resourceRef));
|
|
394
381
|
|
|
@@ -529,7 +516,7 @@ export class KubectlService {
|
|
|
529
516
|
const parsed = await runJson<SecretResponse>({ command: "kubectl", args, timeoutMs: KUBECTL_TIMEOUT_MS });
|
|
530
517
|
const encoded = parsed.data?.[options.key];
|
|
531
518
|
|
|
532
|
-
if (
|
|
519
|
+
if (encoded === undefined) {
|
|
533
520
|
throw new Error(`Key "${options.key}" not found in secret "${options.name}"`);
|
|
534
521
|
}
|
|
535
522
|
|
|
@@ -547,7 +534,7 @@ export class KubectlService {
|
|
|
547
534
|
}
|
|
548
535
|
|
|
549
536
|
private async listHelmReleases(options: ListResourcesOptions): Promise<ResourceListItem[]> {
|
|
550
|
-
const args = ["list",
|
|
537
|
+
const args = ["list", ...buildHelmBaseArgs(options), "-o", "json"];
|
|
551
538
|
const result = await runCommand({ command: "helm", args, timeoutMs: KUBECTL_TIMEOUT_MS });
|
|
552
539
|
const items = JSON.parse(result.stdout) as HelmListItem[];
|
|
553
540
|
|
|
@@ -564,15 +551,17 @@ export class KubectlService {
|
|
|
564
551
|
}
|
|
565
552
|
|
|
566
553
|
private async getHelmReleaseDetail(options: GetResourceDetailOptions): Promise<ResourceDetail> {
|
|
567
|
-
const baseArgs =
|
|
554
|
+
const baseArgs = buildHelmBaseArgs(options);
|
|
568
555
|
|
|
569
|
-
const [statusResult, valuesResult] = await Promise.all([
|
|
556
|
+
const [statusResult, valuesResult, manifestResult] = await Promise.all([
|
|
570
557
|
runCommand({ command: "helm", args: ["status", options.name, ...baseArgs, "-o", "json"], timeoutMs: KUBECTL_TIMEOUT_MS }),
|
|
571
558
|
runCommand({ command: "helm", args: ["get", "values", options.name, ...baseArgs], timeoutMs: KUBECTL_TIMEOUT_MS }),
|
|
559
|
+
runCommand({ command: "helm", args: ["get", "manifest", options.name, ...baseArgs], timeoutMs: KUBECTL_TIMEOUT_MS }),
|
|
572
560
|
]);
|
|
573
561
|
|
|
574
562
|
const status = JSON.parse(statusResult.stdout) as HelmStatusResponse;
|
|
575
|
-
const
|
|
563
|
+
const helmValues = valuesResult.stdout;
|
|
564
|
+
const yaml = manifestResult.stdout;
|
|
576
565
|
|
|
577
566
|
const overviewLines: string[] = [
|
|
578
567
|
`name: ${status.name}`,
|
|
@@ -598,30 +587,25 @@ export class KubectlService {
|
|
|
598
587
|
...(notesLines.length > 0 ? [{ title: "Notes", lines: notesLines }] : []),
|
|
599
588
|
];
|
|
600
589
|
|
|
601
|
-
const summaryLines = summarySections.flatMap((section) => [
|
|
602
|
-
section.title,
|
|
603
|
-
...section.lines.map((line) => ` ${line}`),
|
|
604
|
-
]);
|
|
605
|
-
|
|
606
590
|
return {
|
|
607
591
|
ref: {
|
|
608
592
|
kind: "helmreleases",
|
|
609
593
|
name: status.name,
|
|
610
594
|
namespace: status.namespace,
|
|
611
595
|
},
|
|
612
|
-
summaryLines,
|
|
613
596
|
summarySections,
|
|
614
597
|
yaml,
|
|
615
598
|
replicas: undefined,
|
|
616
599
|
portForwards: undefined,
|
|
617
600
|
helmChart: status.chart.metadata.name,
|
|
601
|
+
helmValues,
|
|
618
602
|
};
|
|
619
603
|
}
|
|
620
604
|
|
|
621
605
|
private async uninstallHelmRelease(options: DeleteResourceOptions): Promise<void> {
|
|
622
606
|
await runCommand({
|
|
623
607
|
command: "helm",
|
|
624
|
-
args: ["uninstall", options.resourceRef.name,
|
|
608
|
+
args: ["uninstall", options.resourceRef.name, ...buildHelmBaseArgs(options)],
|
|
625
609
|
timeoutMs: KUBECTL_TIMEOUT_MS,
|
|
626
610
|
});
|
|
627
611
|
}
|
|
@@ -142,14 +142,23 @@ export interface GenericListItem {
|
|
|
142
142
|
name?: string | undefined;
|
|
143
143
|
namespace?: string | undefined;
|
|
144
144
|
creationTimestamp?: string | undefined;
|
|
145
|
+
labels?: Record<string, string> | undefined;
|
|
145
146
|
} | undefined;
|
|
146
147
|
spec?: Record<string, unknown> | undefined;
|
|
147
148
|
status?: Record<string, unknown> | undefined;
|
|
149
|
+
// Event-only fields
|
|
148
150
|
type?: string | undefined;
|
|
149
151
|
reason?: string | undefined;
|
|
150
152
|
message?: string | undefined;
|
|
151
153
|
lastTimestamp?: string | undefined;
|
|
152
154
|
firstTimestamp?: string | undefined;
|
|
155
|
+
// RBAC fields
|
|
156
|
+
rules?: unknown[] | undefined;
|
|
157
|
+
roleRef?: Record<string, unknown> | undefined;
|
|
158
|
+
subjects?: unknown[] | undefined;
|
|
159
|
+
// ServiceAccount fields
|
|
160
|
+
secrets?: unknown[] | undefined;
|
|
161
|
+
imagePullSecrets?: unknown[] | undefined;
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
export interface GenericListResponse {
|
|
@@ -218,7 +227,7 @@ export interface HelmStatusResponse {
|
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
export interface ApiResourcesResponse {
|
|
221
|
-
resources
|
|
230
|
+
resources?: ApiResourceItem[] | undefined;
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
export interface ApiResourceItem {
|
|
@@ -8,16 +8,30 @@ export function parseCpuNano(value: string): number {
|
|
|
8
8
|
|
|
9
9
|
export function parseMemBytes(value: string): number {
|
|
10
10
|
if (!value) return 0;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// Order matters: multi-character (binary) suffixes must be checked before their
|
|
12
|
+
// single-letter decimal counterparts (e.g. "Ki" before "K").
|
|
13
|
+
const units: Array<[string, number]> = [
|
|
14
|
+
["Ei", 1024 ** 6],
|
|
15
|
+
["Pi", 1024 ** 5],
|
|
16
|
+
["Ti", 1024 ** 4],
|
|
17
|
+
["Gi", 1024 ** 3],
|
|
18
|
+
["Mi", 1024 ** 2],
|
|
19
|
+
["Ki", 1024],
|
|
20
|
+
["E", 1000 ** 6],
|
|
21
|
+
["P", 1000 ** 5],
|
|
22
|
+
["T", 1000 ** 4],
|
|
23
|
+
["G", 1000 ** 3],
|
|
24
|
+
["M", 1000 ** 2],
|
|
25
|
+
["K", 1000],
|
|
26
|
+
];
|
|
27
|
+
for (const [suffix, mult] of units) {
|
|
16
28
|
if (value.endsWith(suffix)) {
|
|
17
|
-
|
|
29
|
+
const n = Number.parseFloat(value);
|
|
30
|
+
return Number.isFinite(n) ? n * mult : 0;
|
|
18
31
|
}
|
|
19
32
|
}
|
|
20
|
-
|
|
33
|
+
const n = Number.parseFloat(value);
|
|
34
|
+
return Number.isFinite(n) ? n : 0;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
export function formatCpu(nano: number): string {
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { spawn, type ChildProcess, type StdioOptions } from "node:child_process";
|
|
2
|
+
import { parseKubectlError } from "./kubectl-helpers";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
4
5
|
let shutdownController: AbortController | undefined;
|
|
5
6
|
|
|
7
|
+
const persistentProcesses = new Set<ChildProcess>();
|
|
8
|
+
|
|
6
9
|
export function setAbortController(controller: AbortController): void {
|
|
7
10
|
shutdownController = controller;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export function abortRunningProcesses(): void {
|
|
11
14
|
shutdownController?.abort();
|
|
15
|
+
for (const child of persistentProcesses) {
|
|
16
|
+
safeKill(child, "SIGTERM");
|
|
17
|
+
}
|
|
18
|
+
persistentProcesses.clear();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function safeKill(child: ChildProcess, signal: NodeJS.Signals = "SIGTERM"): void {
|
|
22
|
+
try {
|
|
23
|
+
if (child.exitCode === null && !child.killed) {
|
|
24
|
+
child.kill(signal);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// ESRCH: process already exited. Safe to ignore.
|
|
28
|
+
}
|
|
12
29
|
}
|
|
13
30
|
|
|
14
31
|
export interface RunCommandOptions {
|
|
@@ -28,23 +45,30 @@ export function runCommand(options: RunCommandOptions): Promise<CommandResult> {
|
|
|
28
45
|
const child = spawn(options.command, options.args, { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
29
46
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
30
47
|
|
|
31
|
-
const
|
|
48
|
+
const onShutdown = (): void => {
|
|
32
49
|
if (settled) {
|
|
33
50
|
return;
|
|
34
51
|
}
|
|
35
52
|
settled = true;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
clearTimeout(timeoutId);
|
|
54
|
+
safeKill(child, "SIGTERM");
|
|
55
|
+
reject(new Error("Application shutting down"));
|
|
56
|
+
};
|
|
39
57
|
|
|
40
|
-
const
|
|
58
|
+
const timeoutId = setTimeout(() => {
|
|
41
59
|
if (settled) {
|
|
42
60
|
return;
|
|
43
61
|
}
|
|
44
62
|
settled = true;
|
|
45
|
-
|
|
46
|
-
child
|
|
47
|
-
reject(new Error(
|
|
63
|
+
removeAbortListener();
|
|
64
|
+
safeKill(child, "SIGTERM");
|
|
65
|
+
reject(new Error(`${options.command} ${options.args.slice(0, 4).join(" ")} timed out after ${timeoutMs / 1_000}s`));
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
|
|
68
|
+
const removeAbortListener = (): void => {
|
|
69
|
+
if (shutdownController) {
|
|
70
|
+
shutdownController.signal.removeEventListener("abort", onShutdown);
|
|
71
|
+
}
|
|
48
72
|
};
|
|
49
73
|
|
|
50
74
|
if (shutdownController) {
|
|
@@ -68,12 +92,15 @@ export function runCommand(options: RunCommandOptions): Promise<CommandResult> {
|
|
|
68
92
|
|
|
69
93
|
child.on("error", (error) => {
|
|
70
94
|
clearTimeout(timeoutId);
|
|
95
|
+
removeAbortListener();
|
|
96
|
+
if (settled) return;
|
|
71
97
|
settled = true;
|
|
72
98
|
reject(error);
|
|
73
99
|
});
|
|
74
100
|
|
|
75
101
|
child.on("close", (code) => {
|
|
76
102
|
clearTimeout(timeoutId);
|
|
103
|
+
removeAbortListener();
|
|
77
104
|
|
|
78
105
|
if (settled) {
|
|
79
106
|
return;
|
|
@@ -86,8 +113,7 @@ export function runCommand(options: RunCommandOptions): Promise<CommandResult> {
|
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
settled = true;
|
|
89
|
-
|
|
90
|
-
reject(new Error(message));
|
|
116
|
+
reject(parseKubectlError(stderr, options.args, options.command));
|
|
91
117
|
});
|
|
92
118
|
});
|
|
93
119
|
}
|
|
@@ -101,8 +127,21 @@ export function runJson<T>(options: RunCommandOptions): Promise<T> {
|
|
|
101
127
|
}
|
|
102
128
|
|
|
103
129
|
export function startPersistentProcess(options: { command: string; args: string[]; stdio?: StdioOptions }): ChildProcess {
|
|
104
|
-
|
|
130
|
+
const child = spawn(options.command, options.args, {
|
|
105
131
|
env: process.env,
|
|
106
132
|
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
|
|
107
133
|
});
|
|
134
|
+
persistentProcesses.add(child);
|
|
135
|
+
child.on("close", () => {
|
|
136
|
+
persistentProcesses.delete(child);
|
|
137
|
+
});
|
|
138
|
+
child.on("error", () => {
|
|
139
|
+
persistentProcesses.delete(child);
|
|
140
|
+
});
|
|
141
|
+
return child;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function stopPersistentProcess(child: ChildProcess): void {
|
|
145
|
+
persistentProcesses.delete(child);
|
|
146
|
+
safeKill(child, "SIGINT");
|
|
108
147
|
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { tokenizeYamlLine, tokenizeDescribeLine } from "../../components/inspector-tokens";
|
|
3
|
-
import { YAML_COMMENT, YAML_KEY, TEXT_SUBTLE, YAML_VALUE } from "../../theme";
|
|
4
|
-
|
|
5
|
-
describe("tokenizeYamlLine", () => {
|
|
6
|
-
test("empty line", () => {
|
|
7
|
-
const tokens = tokenizeYamlLine("");
|
|
8
|
-
expect(tokens[0]?.text).toBe(" ");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("whitespace line", () => {
|
|
12
|
-
const tokens = tokenizeYamlLine(" ");
|
|
13
|
-
expect(tokens[0]?.text).toBe(" ");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("comment line", () => {
|
|
17
|
-
const tokens = tokenizeYamlLine("# this is a comment");
|
|
18
|
-
expect(tokens).toHaveLength(1);
|
|
19
|
-
expect(tokens[0]?.fg).toBe(YAML_COMMENT);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("key: value line", () => {
|
|
23
|
-
const tokens = tokenizeYamlLine("name: nginx");
|
|
24
|
-
expect(tokens).toHaveLength(4);
|
|
25
|
-
expect(tokens[0]?.text).toBe("");
|
|
26
|
-
expect(tokens[1]?.text).toBe("name");
|
|
27
|
-
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
28
|
-
expect(tokens[3]?.text).toBe("nginx");
|
|
29
|
-
expect(tokens[3]?.fg).toBe(YAML_VALUE);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("indented key: value", () => {
|
|
33
|
-
const tokens = tokenizeYamlLine(" name: nginx");
|
|
34
|
-
expect(tokens[0]?.text).toBe(" ");
|
|
35
|
-
expect(tokens[1]?.text).toBe("name");
|
|
36
|
-
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("list item with value", () => {
|
|
40
|
-
const tokens = tokenizeYamlLine("- name: nginx");
|
|
41
|
-
expect(tokens[0]?.text).toBe("- ");
|
|
42
|
-
expect(tokens[1]?.text).toBe("name");
|
|
43
|
-
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("bare list marker", () => {
|
|
47
|
-
const tokens = tokenizeYamlLine("-");
|
|
48
|
-
expect(tokens).toHaveLength(1);
|
|
49
|
-
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("key only (ends with colon)", () => {
|
|
53
|
-
const tokens = tokenizeYamlLine("metadata:");
|
|
54
|
-
expect(tokens).toHaveLength(3);
|
|
55
|
-
expect(tokens[1]?.text).toBe("metadata");
|
|
56
|
-
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
57
|
-
expect(tokens[2]?.text).toBe(":");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("plain text fallthrough", () => {
|
|
61
|
-
const tokens = tokenizeYamlLine("some random text");
|
|
62
|
-
expect(tokens).toHaveLength(1);
|
|
63
|
-
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("tokenizeDescribeLine", () => {
|
|
68
|
-
test("empty line", () => {
|
|
69
|
-
const tokens = tokenizeDescribeLine("");
|
|
70
|
-
expect(tokens[0]?.text).toBe(" ");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("section header", () => {
|
|
74
|
-
const tokens = tokenizeDescribeLine("Name: test-pod");
|
|
75
|
-
expect(tokens).toHaveLength(3);
|
|
76
|
-
expect(tokens[0]?.text).toBe("Name");
|
|
77
|
-
expect(tokens[0]?.fg).toBe(YAML_KEY);
|
|
78
|
-
expect(tokens[2]?.fg).toBe(YAML_VALUE);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("indented key: value (double space after colon)", () => {
|
|
82
|
-
const tokens = tokenizeDescribeLine(" Namespace: default");
|
|
83
|
-
expect(tokens).toHaveLength(2);
|
|
84
|
-
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
85
|
-
expect(tokens[1]?.fg).toBe(YAML_VALUE);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("key: single value", () => {
|
|
89
|
-
const tokens = tokenizeDescribeLine("key:value");
|
|
90
|
-
expect(tokens).toHaveLength(3);
|
|
91
|
-
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
92
|
-
expect(tokens[1]?.text).toBe(":");
|
|
93
|
-
expect(tokens[2]?.text).toBe("value");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("plain text", () => {
|
|
97
|
-
const tokens = tokenizeDescribeLine("Some plain description text");
|
|
98
|
-
expect(tokens).toHaveLength(1);
|
|
99
|
-
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { YAML_COMMENT, YAML_KEY, TEXT_SUBTLE, YAML_VALUE } from "../theme";
|
|
2
|
-
|
|
3
|
-
export interface YamlToken {
|
|
4
|
-
text: string;
|
|
5
|
-
fg: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function tokenizeDescribeLine(line: string): YamlToken[] {
|
|
9
|
-
if (!line.trim()) {
|
|
10
|
-
return [{ text: line || " ", fg: TEXT_SUBTLE }];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (/^[A-Z][A-Za-z ]+:/.test(line.trimStart()) && !line.startsWith(" ")) {
|
|
14
|
-
const colonIdx = line.indexOf(":");
|
|
15
|
-
const key = line.slice(0, colonIdx);
|
|
16
|
-
const rest = line.slice(colonIdx + 1);
|
|
17
|
-
return [
|
|
18
|
-
{ text: key, fg: YAML_KEY },
|
|
19
|
-
{ text: ":", fg: TEXT_SUBTLE },
|
|
20
|
-
{ text: rest, fg: YAML_VALUE },
|
|
21
|
-
];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const colonSpaceIdx = line.indexOf(": ");
|
|
25
|
-
if (colonSpaceIdx > 0) {
|
|
26
|
-
const key = line.slice(0, colonSpaceIdx);
|
|
27
|
-
const value = line.slice(colonSpaceIdx + 1);
|
|
28
|
-
return [
|
|
29
|
-
{ text: key, fg: TEXT_SUBTLE },
|
|
30
|
-
{ text: value, fg: YAML_VALUE },
|
|
31
|
-
];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const colonIdx = line.indexOf(":");
|
|
35
|
-
if (colonIdx > 0 && colonIdx < line.length - 1) {
|
|
36
|
-
const key = line.slice(0, colonIdx);
|
|
37
|
-
const value = line.slice(colonIdx + 1);
|
|
38
|
-
return [
|
|
39
|
-
{ text: key, fg: TEXT_SUBTLE },
|
|
40
|
-
{ text: ":", fg: TEXT_SUBTLE },
|
|
41
|
-
{ text: value, fg: YAML_VALUE },
|
|
42
|
-
];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return [{ text: line, fg: TEXT_SUBTLE }];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function tokenizeYamlLine(line: string): YamlToken[] {
|
|
49
|
-
if (!line.trim()) {
|
|
50
|
-
return [{ text: line || " ", fg: TEXT_SUBTLE }];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (/^\s*#/.test(line)) {
|
|
54
|
-
return [{ text: line, fg: YAML_COMMENT }];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const indentLen = line.length - line.trimStart().length;
|
|
58
|
-
const indent = line.slice(0, indentLen);
|
|
59
|
-
let rest = line.trimStart();
|
|
60
|
-
|
|
61
|
-
let listMarker = "";
|
|
62
|
-
if (rest.startsWith("- ")) {
|
|
63
|
-
listMarker = "- ";
|
|
64
|
-
rest = rest.slice(2);
|
|
65
|
-
} else if (rest === "-") {
|
|
66
|
-
return [{ text: line, fg: TEXT_SUBTLE }];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const colonSpaceIdx = rest.indexOf(": ");
|
|
70
|
-
const isKeyOnly = rest === rest.trimEnd() && rest.endsWith(":");
|
|
71
|
-
|
|
72
|
-
if (colonSpaceIdx !== -1) {
|
|
73
|
-
const key = rest.slice(0, colonSpaceIdx);
|
|
74
|
-
const value = rest.slice(colonSpaceIdx + 2);
|
|
75
|
-
return [
|
|
76
|
-
{ text: indent + listMarker, fg: TEXT_SUBTLE },
|
|
77
|
-
{ text: key, fg: YAML_KEY },
|
|
78
|
-
{ text: ": ", fg: TEXT_SUBTLE },
|
|
79
|
-
{ text: value, fg: YAML_VALUE },
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (isKeyOnly) {
|
|
84
|
-
const key = rest.slice(0, -1);
|
|
85
|
-
return [
|
|
86
|
-
{ text: indent + listMarker, fg: TEXT_SUBTLE },
|
|
87
|
-
{ text: key, fg: YAML_KEY },
|
|
88
|
-
{ text: ":", fg: TEXT_SUBTLE },
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return [{ text: line, fg: TEXT_SUBTLE }];
|
|
93
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { TextAttributes } from "@opentui/core";
|
|
2
|
-
|
|
3
|
-
import { DANGER, GLYPHS, KEY_HINT, OVERLAY_SURFACE, PANEL_BORDER, TEXT_PRIMARY, TEXT_SUBTLE } from "../theme";
|
|
4
|
-
import { activePortForwardLabel } from "../utils";
|
|
5
|
-
import type { ActivePortForward } from "../../lib/k8s/types";
|
|
6
|
-
|
|
7
|
-
export interface PortForwardsTrayProps {
|
|
8
|
-
forwards: ActivePortForward[];
|
|
9
|
-
onStop: (id: string) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function PortForwardsTray({ forwards, onStop }: PortForwardsTrayProps) {
|
|
13
|
-
if (forwards.length === 0) {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<box
|
|
19
|
-
style={{
|
|
20
|
-
border: true,
|
|
21
|
-
borderColor: PANEL_BORDER,
|
|
22
|
-
borderStyle: "rounded",
|
|
23
|
-
backgroundColor: OVERLAY_SURFACE,
|
|
24
|
-
paddingLeft: 1,
|
|
25
|
-
paddingRight: 1,
|
|
26
|
-
flexDirection: "column",
|
|
27
|
-
}}
|
|
28
|
-
>
|
|
29
|
-
{/* Tray heading */}
|
|
30
|
-
<text fg={KEY_HINT}>
|
|
31
|
-
{GLYPHS.forward}
|
|
32
|
-
<span fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>{" Port Forwards"}</span>
|
|
33
|
-
</text>
|
|
34
|
-
<box style={{ flexDirection: "column", width: "100%" }}>
|
|
35
|
-
{forwards.map((forward) => {
|
|
36
|
-
const isReady = forward.status === "ready";
|
|
37
|
-
const statusDot = isReady ? GLYPHS.dot : GLYPHS.dotEmpty;
|
|
38
|
-
const statusFg = isReady ? KEY_HINT : TEXT_SUBTLE;
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<box key={forward.id} style={{ flexDirection: "row", justifyContent: "space-between", width: "100%" }}>
|
|
42
|
-
<text fg={isReady ? TEXT_PRIMARY : TEXT_SUBTLE}>
|
|
43
|
-
<span fg={statusFg}>{statusDot}</span>
|
|
44
|
-
{` ${activePortForwardLabel(forward)} `}
|
|
45
|
-
<span fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>{forward.status}</span>
|
|
46
|
-
</text>
|
|
47
|
-
<text fg={DANGER} onMouseDown={() => onStop(forward.id)}>
|
|
48
|
-
<span fg={DANGER}>{GLYPHS.stop}</span>
|
|
49
|
-
{" stop"}
|
|
50
|
-
</text>
|
|
51
|
-
</box>
|
|
52
|
-
);
|
|
53
|
-
})}
|
|
54
|
-
</box>
|
|
55
|
-
</box>
|
|
56
|
-
);
|
|
57
|
-
}
|