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.
- 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 +4 -262
- 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 -0
- 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 -9
- 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
|
@@ -5,15 +5,83 @@ import { buildResourceDetail, type KubernetesResource } from "../k8s/resource-de
|
|
|
5
5
|
import type {
|
|
6
6
|
ClusterContext,
|
|
7
7
|
EventItem,
|
|
8
|
-
LogOptions,
|
|
9
8
|
NamespaceItem,
|
|
10
9
|
ResourceDetail,
|
|
11
10
|
ResourceKind,
|
|
12
11
|
ResourceListItem,
|
|
13
12
|
ResourceRef,
|
|
14
13
|
} from "../k8s/types";
|
|
14
|
+
import {
|
|
15
|
+
normalizeKind,
|
|
16
|
+
normalizeLogTarget,
|
|
17
|
+
normalizePortForwardTarget,
|
|
18
|
+
normalizeResourceTarget,
|
|
19
|
+
resourceStatus,
|
|
20
|
+
resourceSummary,
|
|
21
|
+
curatedKinds,
|
|
22
|
+
supportsScale,
|
|
23
|
+
supportsRolloutRestart,
|
|
24
|
+
} from "./kubectl-helpers";
|
|
25
|
+
import type {
|
|
26
|
+
ListNamespacesOptions,
|
|
27
|
+
ListResourceKindsOptions,
|
|
28
|
+
ListResourcesOptions,
|
|
29
|
+
GetResourceDetailOptions,
|
|
30
|
+
ListResourceEventsOptions,
|
|
31
|
+
StreamLogsOptions,
|
|
32
|
+
StreamLogsResult,
|
|
33
|
+
ScaleResourceOptions,
|
|
34
|
+
RolloutRestartOptions,
|
|
35
|
+
DeleteResourceOptions,
|
|
36
|
+
StartShellOptions,
|
|
37
|
+
EditResourceOptions,
|
|
38
|
+
HelmUpgradeValuesOptions,
|
|
39
|
+
HelmRollbackOptions,
|
|
40
|
+
HelmUpgradeOptions,
|
|
41
|
+
StartPortForwardOptions,
|
|
42
|
+
GetSecretValueOptions,
|
|
43
|
+
GetAllSecretValuesOptions,
|
|
44
|
+
PodMetricsMap,
|
|
45
|
+
NodeMetricsMap,
|
|
46
|
+
KubeConfigContextResponse,
|
|
47
|
+
NamespaceListResponse,
|
|
48
|
+
GenericListResponse,
|
|
49
|
+
EventListResponse,
|
|
50
|
+
SecretResponse,
|
|
51
|
+
HelmListItem,
|
|
52
|
+
HelmStatusResponse,
|
|
53
|
+
ApiResourcesResponse,
|
|
54
|
+
PodMetricsItem,
|
|
55
|
+
NodeMetricsItem,
|
|
56
|
+
AllocatableCapacity,
|
|
57
|
+
} from "./kubectl-types";
|
|
58
|
+
import { parseCpuNano, parseMemBytes, formatCpu, formatMem } from "./metrics-utils";
|
|
15
59
|
import { runCommand, runJson, runText, startPersistentProcess } from "./spawn-utils";
|
|
16
60
|
|
|
61
|
+
export { normalizeKind, resourceStatus, resourceSummary } from "./kubectl-helpers";
|
|
62
|
+
export type {
|
|
63
|
+
ListNamespacesOptions,
|
|
64
|
+
ListResourceKindsOptions,
|
|
65
|
+
ListResourcesOptions,
|
|
66
|
+
GetResourceDetailOptions,
|
|
67
|
+
ListResourceEventsOptions,
|
|
68
|
+
StreamLogsOptions,
|
|
69
|
+
StreamLogsResult,
|
|
70
|
+
ScaleResourceOptions,
|
|
71
|
+
RolloutRestartOptions,
|
|
72
|
+
DeleteResourceOptions,
|
|
73
|
+
StartShellOptions,
|
|
74
|
+
EditResourceOptions,
|
|
75
|
+
HelmUpgradeValuesOptions,
|
|
76
|
+
HelmRollbackOptions,
|
|
77
|
+
HelmUpgradeOptions,
|
|
78
|
+
StartPortForwardOptions,
|
|
79
|
+
GetSecretValueOptions,
|
|
80
|
+
GetAllSecretValuesOptions,
|
|
81
|
+
PodMetricsMap,
|
|
82
|
+
NodeMetricsMap,
|
|
83
|
+
} from "./kubectl-types";
|
|
84
|
+
|
|
17
85
|
const KUBECTL_TIMEOUT_MS = 15_000;
|
|
18
86
|
|
|
19
87
|
export class KubectlService {
|
|
@@ -64,40 +132,16 @@ export class KubectlService {
|
|
|
64
132
|
|
|
65
133
|
async listResourceKinds(options: ListResourceKindsOptions): Promise<ResourceKind[]> {
|
|
66
134
|
const [result, helmAvailable] = await Promise.all([
|
|
67
|
-
|
|
135
|
+
runJson<ApiResourcesResponse>({ command: "kubectl", args: ["--context", options.context, "api-resources", "--cached=false", "-o", "json"], timeoutMs: KUBECTL_TIMEOUT_MS }),
|
|
68
136
|
this.isHelmAvailable(),
|
|
69
137
|
]);
|
|
70
138
|
|
|
71
|
-
const
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const parsed = dataLines
|
|
78
|
-
.map((line): ApiResourcesLine | undefined => {
|
|
79
|
-
const columns = line.split(/\s{2,}/).map((part) => part.trim());
|
|
80
|
-
const [name, shortNamesText = "", groupText = "", namespacedText = "", kindText = ""] = columns;
|
|
81
|
-
|
|
82
|
-
if (columns.length < 5 || !name || !namespacedText || !kindText) {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
name,
|
|
88
|
-
shortNames: shortNamesText === "<none>" ? [] : shortNamesText.split(","),
|
|
89
|
-
group: groupText === "<none>" ? "" : groupText,
|
|
90
|
-
namespaced: namespacedText === "true",
|
|
91
|
-
kind: kindText,
|
|
92
|
-
};
|
|
93
|
-
})
|
|
94
|
-
.filter((value): value is ApiResourcesLine => value !== undefined)
|
|
95
|
-
.map((value) => ({
|
|
96
|
-
name: value.name,
|
|
97
|
-
namespaced: value.namespaced,
|
|
98
|
-
group: value.group || undefined,
|
|
99
|
-
shortNames: value.shortNames,
|
|
100
|
-
}));
|
|
139
|
+
const parsed = (result.resources ?? []).map((item) => ({
|
|
140
|
+
name: item.name,
|
|
141
|
+
namespaced: item.namespaced,
|
|
142
|
+
group: item.group || undefined,
|
|
143
|
+
shortNames: item.shortNames ?? [],
|
|
144
|
+
}));
|
|
101
145
|
|
|
102
146
|
const curated = curatedKinds(parsed);
|
|
103
147
|
|
|
@@ -118,7 +162,6 @@ export class KubectlService {
|
|
|
118
162
|
const args = ["--context", options.context, "get", options.resourceKind.name];
|
|
119
163
|
|
|
120
164
|
if (isEvents) {
|
|
121
|
-
// Always fetch all namespaces for events, sorted by time
|
|
122
165
|
args.push("-A");
|
|
123
166
|
} else if (options.resourceKind.namespaced) {
|
|
124
167
|
args.push("-n", options.namespace);
|
|
@@ -332,11 +375,9 @@ export class KubectlService {
|
|
|
332
375
|
args.push("-n", options.namespace);
|
|
333
376
|
}
|
|
334
377
|
|
|
335
|
-
// pod/name is the canonical exec target; other workload kinds are also accepted by kubectl exec
|
|
336
378
|
const kind = normalizeKind(options.resourceRef.kind);
|
|
337
379
|
const target = kind === "pod" || kind === "pods" ? `pod/${options.resourceRef.name}` : normalizeResourceTarget(options.resourceRef);
|
|
338
380
|
|
|
339
|
-
// sh -lc lets $SHELL expand inside the child shell, not this process
|
|
340
381
|
args.push("exec", "-it", target, "--", "sh", "-lc", "exec ${SHELL:-/bin/sh}");
|
|
341
382
|
|
|
342
383
|
return startPersistentProcess({ command: "kubectl", args, stdio: "inherit" });
|
|
@@ -408,7 +449,7 @@ export class KubectlService {
|
|
|
408
449
|
const metricsData = JSON.parse(metricsRaw) as { items?: NodeMetricsItem[] };
|
|
409
450
|
const nodesData = JSON.parse(nodesRaw) as { items?: Array<{ metadata?: { name?: string }; status?: { allocatable?: { cpu?: string; memory?: string } } }> };
|
|
410
451
|
|
|
411
|
-
const allocatable = new Map<string,
|
|
452
|
+
const allocatable = new Map<string, AllocatableCapacity>();
|
|
412
453
|
for (const node of nodesData.items ?? []) {
|
|
413
454
|
const name = node.metadata?.name;
|
|
414
455
|
if (!name) continue;
|
|
@@ -475,17 +516,14 @@ export class KubectlService {
|
|
|
475
516
|
return startPersistentProcess({ command: "kubectl", args, stdio: ["ignore", "pipe", "pipe"] });
|
|
476
517
|
}
|
|
477
518
|
|
|
478
|
-
/** Returns true if kubectl scale is supported for the given resource kind. */
|
|
479
519
|
canScale(ref: ResourceRef): boolean {
|
|
480
520
|
return supportsScale(ref);
|
|
481
521
|
}
|
|
482
522
|
|
|
483
|
-
/** Returns true if kubectl rollout restart is supported for the given resource kind. */
|
|
484
523
|
canRolloutRestart(ref: ResourceRef): boolean {
|
|
485
524
|
return supportsRolloutRestart(ref);
|
|
486
525
|
}
|
|
487
526
|
|
|
488
|
-
/** Fetches and base64-decodes a single key from a Kubernetes Secret. */
|
|
489
527
|
async getSecretValue(options: GetSecretValueOptions): Promise<string> {
|
|
490
528
|
const args = ["--context", options.context, "-n", options.namespace, "get", "secret", options.name, "-o", "json"];
|
|
491
529
|
const parsed = await runJson<SecretResponse>({ command: "kubectl", args, timeoutMs: KUBECTL_TIMEOUT_MS });
|
|
@@ -498,7 +536,6 @@ export class KubectlService {
|
|
|
498
536
|
return Buffer.from(encoded, "base64").toString("utf8");
|
|
499
537
|
}
|
|
500
538
|
|
|
501
|
-
/** Fetches and base64-decodes all keys from a Kubernetes Secret. */
|
|
502
539
|
async getAllSecretValues(options: GetAllSecretValuesOptions): Promise<Record<string, string>> {
|
|
503
540
|
const args = ["--context", options.context, "-n", options.namespace, "get", "secret", options.name, "-o", "json"];
|
|
504
541
|
const parsed = await runJson<SecretResponse>({ command: "kubectl", args, timeoutMs: KUBECTL_TIMEOUT_MS });
|
|
@@ -588,529 +625,4 @@ export class KubectlService {
|
|
|
588
625
|
timeoutMs: KUBECTL_TIMEOUT_MS,
|
|
589
626
|
});
|
|
590
627
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
export interface ListNamespacesOptions {
|
|
596
|
-
context: string;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export interface ListResourceKindsOptions {
|
|
600
|
-
context: string;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export interface ListResourcesOptions {
|
|
604
|
-
context: string;
|
|
605
|
-
namespace: string;
|
|
606
|
-
resourceKind: ResourceKind;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export interface GetResourceDetailOptions {
|
|
610
|
-
context: string;
|
|
611
|
-
namespace: string;
|
|
612
|
-
resourceKind: ResourceKind;
|
|
613
|
-
name: string;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
export interface ListResourceEventsOptions {
|
|
617
|
-
context: string;
|
|
618
|
-
namespace: string;
|
|
619
|
-
resourceRef: ResourceRef;
|
|
620
|
-
namespaced: boolean;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
export interface EditResourceOptions {
|
|
624
|
-
context: string;
|
|
625
|
-
namespace: string;
|
|
626
|
-
resourceRef: ResourceRef;
|
|
627
|
-
namespaced: boolean;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
export interface HelmUpgradeValuesOptions {
|
|
631
|
-
context: string;
|
|
632
|
-
namespace: string;
|
|
633
|
-
name: string;
|
|
634
|
-
chart: string;
|
|
635
|
-
valuesFile: string;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
export interface HelmRollbackOptions {
|
|
639
|
-
context: string;
|
|
640
|
-
namespace: string;
|
|
641
|
-
name: string;
|
|
642
|
-
revision?: number | undefined;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
export interface HelmUpgradeOptions {
|
|
646
|
-
context: string;
|
|
647
|
-
namespace: string;
|
|
648
|
-
name: string;
|
|
649
|
-
chart: string;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
export type PodMetricsMap = Record<string, { cpu: string; memory: string }>;
|
|
653
|
-
export type NodeMetricsMap = Record<string, { cpu: string; memory: string; cpuPct: number; memPct: number }>;
|
|
654
|
-
|
|
655
|
-
export interface StreamLogsOptions {
|
|
656
|
-
context: string;
|
|
657
|
-
namespace: string;
|
|
658
|
-
resourceRef: ResourceRef;
|
|
659
|
-
namespaced: boolean;
|
|
660
|
-
container?: string | undefined;
|
|
661
|
-
logOptions?: LogOptions | undefined;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export interface StreamLogsResult {
|
|
665
|
-
target: string;
|
|
666
|
-
child: ChildProcess;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
export interface ScaleResourceOptions {
|
|
670
|
-
context: string;
|
|
671
|
-
namespace: string;
|
|
672
|
-
resourceRef: ResourceRef;
|
|
673
|
-
namespaced: boolean;
|
|
674
|
-
replicas: number;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
export interface RolloutRestartOptions {
|
|
678
|
-
context: string;
|
|
679
|
-
namespace: string;
|
|
680
|
-
resourceRef: ResourceRef;
|
|
681
|
-
namespaced: boolean;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
export interface DeleteResourceOptions {
|
|
685
|
-
context: string;
|
|
686
|
-
namespace: string;
|
|
687
|
-
resourceRef: ResourceRef;
|
|
688
|
-
namespaced: boolean;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
export interface StartShellOptions {
|
|
692
|
-
context: string;
|
|
693
|
-
namespace: string;
|
|
694
|
-
resourceRef: ResourceRef;
|
|
695
|
-
namespaced: boolean;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
export interface StartPortForwardOptions {
|
|
699
|
-
context: string;
|
|
700
|
-
namespace: string;
|
|
701
|
-
resourceRef: ResourceRef;
|
|
702
|
-
namespaced: boolean;
|
|
703
|
-
localPort: number;
|
|
704
|
-
remotePort: number;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
export interface GetSecretValueOptions {
|
|
708
|
-
context: string;
|
|
709
|
-
namespace: string;
|
|
710
|
-
name: string;
|
|
711
|
-
key: string;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
export interface GetAllSecretValuesOptions {
|
|
715
|
-
context: string;
|
|
716
|
-
namespace: string;
|
|
717
|
-
name: string;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
interface KubeConfigContextResponse {
|
|
721
|
-
contexts?: Array<{ name?: string | undefined }> | undefined;
|
|
722
|
-
"current-context"?: string | undefined;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
interface NamespaceListResponse {
|
|
726
|
-
items?: Array<{ metadata?: { name?: string | undefined } | undefined }> | undefined;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
interface ApiResourcesLine {
|
|
730
|
-
name: string;
|
|
731
|
-
shortNames: string[];
|
|
732
|
-
group: string;
|
|
733
|
-
namespaced: boolean;
|
|
734
|
-
kind: string;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
interface GenericListResponse {
|
|
738
|
-
apiVersion?: string | undefined;
|
|
739
|
-
kind?: string | undefined;
|
|
740
|
-
items?: GenericListItem[] | undefined;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
interface GenericListItem {
|
|
744
|
-
kind?: string | undefined;
|
|
745
|
-
metadata?: {
|
|
746
|
-
name?: string | undefined;
|
|
747
|
-
namespace?: string | undefined;
|
|
748
|
-
creationTimestamp?: string | undefined;
|
|
749
|
-
} | undefined;
|
|
750
|
-
spec?: Record<string, unknown> | undefined;
|
|
751
|
-
status?: Record<string, unknown> | undefined;
|
|
752
|
-
// Event-specific top-level fields
|
|
753
|
-
type?: string | undefined;
|
|
754
|
-
reason?: string | undefined;
|
|
755
|
-
message?: string | undefined;
|
|
756
|
-
lastTimestamp?: string | undefined;
|
|
757
|
-
firstTimestamp?: string | undefined;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
interface EventListResponse {
|
|
761
|
-
items?: EventResponseItem[] | undefined;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
interface SecretResponse {
|
|
765
|
-
data?: Record<string, string> | undefined;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
interface EventResponseItem {
|
|
769
|
-
metadata?: {
|
|
770
|
-
creationTimestamp?: string | undefined;
|
|
771
|
-
} | undefined;
|
|
772
|
-
involvedObject?: {
|
|
773
|
-
kind?: string | undefined;
|
|
774
|
-
name?: string | undefined;
|
|
775
|
-
namespace?: string | undefined;
|
|
776
|
-
} | undefined;
|
|
777
|
-
reason?: string | undefined;
|
|
778
|
-
message?: string | undefined;
|
|
779
|
-
type?: string | undefined;
|
|
780
|
-
source?: {
|
|
781
|
-
component?: string | undefined;
|
|
782
|
-
host?: string | undefined;
|
|
783
|
-
} | undefined;
|
|
784
|
-
reportingComponent?: string | undefined;
|
|
785
|
-
firstTimestamp?: string | undefined;
|
|
786
|
-
lastTimestamp?: string | undefined;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
interface HelmListItem {
|
|
790
|
-
name: string;
|
|
791
|
-
namespace: string;
|
|
792
|
-
revision: string;
|
|
793
|
-
updated: string;
|
|
794
|
-
status: string;
|
|
795
|
-
chart: string;
|
|
796
|
-
app_version: string;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
interface HelmStatusResponse {
|
|
800
|
-
name: string;
|
|
801
|
-
namespace: string;
|
|
802
|
-
version: number;
|
|
803
|
-
info: {
|
|
804
|
-
first_deployed: string;
|
|
805
|
-
last_deployed: string;
|
|
806
|
-
status: string;
|
|
807
|
-
notes?: string | undefined;
|
|
808
|
-
description?: string | undefined;
|
|
809
|
-
};
|
|
810
|
-
chart: {
|
|
811
|
-
metadata: {
|
|
812
|
-
name: string;
|
|
813
|
-
version: string;
|
|
814
|
-
appVersion?: string | undefined;
|
|
815
|
-
};
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
interface PodMetricsItem {
|
|
820
|
-
metadata?: { name?: string };
|
|
821
|
-
containers?: Array<{ usage?: { cpu?: string; memory?: string } }>;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
interface NodeMetricsItem {
|
|
825
|
-
metadata?: { name?: string };
|
|
826
|
-
usage?: { cpu?: string; memory?: string };
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function parseCpuNano(value: string): number {
|
|
830
|
-
if (!value) return 0;
|
|
831
|
-
if (value.endsWith("n")) return Number.parseInt(value, 10);
|
|
832
|
-
if (value.endsWith("m")) return Number.parseInt(value, 10) * 1_000_000;
|
|
833
|
-
// plain integer = cores
|
|
834
|
-
const n = Number.parseFloat(value);
|
|
835
|
-
return Number.isFinite(n) ? n * 1_000_000_000 : 0;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function parseMemBytes(value: string): number {
|
|
839
|
-
if (!value) return 0;
|
|
840
|
-
const units: Record<string, number> = {
|
|
841
|
-
Ki: 1024, Mi: 1024 ** 2, Gi: 1024 ** 3, Ti: 1024 ** 4,
|
|
842
|
-
K: 1000, M: 1000 ** 2, G: 1000 ** 3, T: 1000 ** 4,
|
|
843
|
-
};
|
|
844
|
-
for (const [suffix, mult] of Object.entries(units)) {
|
|
845
|
-
if (value.endsWith(suffix)) {
|
|
846
|
-
return Number.parseFloat(value) * mult;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
return Number.parseFloat(value) || 0;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function formatCpu(nano: number): string {
|
|
853
|
-
if (nano >= 1_000_000_000) return `${(nano / 1_000_000_000).toFixed(2)}`;
|
|
854
|
-
return `${Math.round(nano / 1_000_000)}m`;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function formatMem(bytes: number): string {
|
|
858
|
-
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}Gi`;
|
|
859
|
-
if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)}Mi`;
|
|
860
|
-
if (bytes >= 1024) return `${Math.round(bytes / 1024)}Ki`;
|
|
861
|
-
return `${bytes}B`;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function parseKubectlError(stderr: string, args: string[]): Error {
|
|
865
|
-
const message = stderr.trim() || `kubectl ${args.join(" ")} failed`;
|
|
866
|
-
return new Error(message);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function resourceStatus(resource: GenericListItem): string {
|
|
870
|
-
const kind = (resource.kind ?? "").toLowerCase();
|
|
871
|
-
const status = resource.status ?? {};
|
|
872
|
-
const spec = resource.spec ?? {};
|
|
873
|
-
|
|
874
|
-
// Events
|
|
875
|
-
if (kind === "event" || kind === "events") {
|
|
876
|
-
return resource.type ?? "Normal";
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// HorizontalPodAutoscalers
|
|
880
|
-
if (kind === "horizontalpodautoscaler") {
|
|
881
|
-
const current = typeof status.currentReplicas === "number" ? status.currentReplicas : "-";
|
|
882
|
-
const max = typeof spec.maxReplicas === "number" ? spec.maxReplicas : "-";
|
|
883
|
-
return `${current}/${max} replicas`;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// CronJobs
|
|
887
|
-
if (kind === "cronjob") {
|
|
888
|
-
const active = Array.isArray(status.active) ? status.active : [];
|
|
889
|
-
return active.length > 0 ? "active" : "idle";
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Nodes: summarize conditions
|
|
893
|
-
if (kind === "node") {
|
|
894
|
-
const conditions = Array.isArray(status.conditions) ? (status.conditions as Record<string, unknown>[]) : [];
|
|
895
|
-
const ready = conditions.find((c) => c.type === "Ready");
|
|
896
|
-
const memPressure = conditions.find((c) => c.type === "MemoryPressure" && c.status === "True");
|
|
897
|
-
const diskPressure = conditions.find((c) => c.type === "DiskPressure" && c.status === "True");
|
|
898
|
-
|
|
899
|
-
if (memPressure) return "MemoryPressure";
|
|
900
|
-
if (diskPressure) return "DiskPressure";
|
|
901
|
-
return ready?.status === "True" ? "Ready" : "NotReady";
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// RBAC subjects count
|
|
905
|
-
if (kind === "rolebinding" || kind === "clusterrolebinding") {
|
|
906
|
-
const subjects = Array.isArray((resource as unknown as Record<string, unknown>).subjects)
|
|
907
|
-
? ((resource as unknown as Record<string, unknown>).subjects as unknown[])
|
|
908
|
-
: [];
|
|
909
|
-
return `${subjects.length} subjects`;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (typeof status.phase === "string") {
|
|
913
|
-
return status.phase;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
if (typeof status.readyReplicas === "number" && typeof spec.replicas === "number") {
|
|
917
|
-
return `${status.readyReplicas}/${spec.replicas} ready`;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (typeof status.availableReplicas === "number" && typeof spec.replicas === "number") {
|
|
921
|
-
return `${status.availableReplicas}/${spec.replicas} available`;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (typeof status.capacity === "object") {
|
|
925
|
-
return "capacity";
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
return "-";
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function resourceSummary(resource: GenericListItem): string {
|
|
932
|
-
const kind = (resource.kind ?? "").toLowerCase();
|
|
933
|
-
const spec = resource.spec ?? {};
|
|
934
|
-
const status = resource.status ?? {};
|
|
935
|
-
|
|
936
|
-
// Events
|
|
937
|
-
if (kind === "event" || kind === "events") {
|
|
938
|
-
const reason = resource.reason ?? "";
|
|
939
|
-
const message = (resource.message ?? "").slice(0, 60);
|
|
940
|
-
return reason ? `${reason}: ${message}` : message;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// HorizontalPodAutoscalers
|
|
944
|
-
if (kind === "horizontalpodautoscaler") {
|
|
945
|
-
const min = spec.minReplicas ?? "-";
|
|
946
|
-
const max = spec.maxReplicas ?? "-";
|
|
947
|
-
const ref = (spec.scaleTargetRef as Record<string, unknown> | undefined)?.name ?? "-";
|
|
948
|
-
return `min:${min} max:${max} ref:${ref}`;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// CronJobs
|
|
952
|
-
if (kind === "cronjob") {
|
|
953
|
-
const schedule = typeof spec.schedule === "string" ? spec.schedule : "-";
|
|
954
|
-
const lastRun = typeof status.lastScheduleTime === "string" ? formatAge(status.lastScheduleTime) : "-";
|
|
955
|
-
return `${schedule} last: ${lastRun}`;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Nodes
|
|
959
|
-
if (kind === "node") {
|
|
960
|
-
const nodeInfo = (status.nodeInfo ?? {}) as Record<string, unknown>;
|
|
961
|
-
const kubeletVersion = typeof nodeInfo.kubeletVersion === "string" ? nodeInfo.kubeletVersion : "";
|
|
962
|
-
const labels = (resource as unknown as { metadata?: { labels?: Record<string, string> } }).metadata?.labels ?? {};
|
|
963
|
-
const roles = Object.keys(labels)
|
|
964
|
-
.filter((k) => k.startsWith("node-role.kubernetes.io/"))
|
|
965
|
-
.map((k) => k.replace("node-role.kubernetes.io/", ""))
|
|
966
|
-
.join(",") || "worker";
|
|
967
|
-
return kubeletVersion ? `${kubeletVersion} roles: ${roles}` : `roles: ${roles}`;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// Roles/ClusterRoles: show rule count
|
|
971
|
-
if (kind === "role" || kind === "clusterrole") {
|
|
972
|
-
const rules = (resource as unknown as { rules?: unknown[] }).rules ?? [];
|
|
973
|
-
return `${rules.length} rules`;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// RoleBindings/ClusterRoleBindings: show roleRef
|
|
977
|
-
if (kind === "rolebinding" || kind === "clusterrolebinding") {
|
|
978
|
-
const roleRef = (resource as unknown as { roleRef?: Record<string, unknown> }).roleRef;
|
|
979
|
-
if (roleRef) {
|
|
980
|
-
return `\u2192 ${roleRef.kind ?? ""}/${roleRef.name ?? ""}`;
|
|
981
|
-
}
|
|
982
|
-
return "";
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
if (typeof spec.clusterIP === "string") {
|
|
986
|
-
return `clusterIP ${spec.clusterIP}`;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
if (typeof spec.nodeName === "string") {
|
|
990
|
-
return `node ${spec.nodeName}`;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (typeof status.hostIP === "string") {
|
|
994
|
-
return `host ${status.hostIP}`;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
if (typeof status.podIP === "string") {
|
|
998
|
-
return `podIP ${status.podIP}`;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
return "";
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function curatedKinds(kinds: ResourceKind[]): ResourceKind[] {
|
|
1005
|
-
const preferredOrder = [
|
|
1006
|
-
"pods",
|
|
1007
|
-
"deployments",
|
|
1008
|
-
"statefulsets",
|
|
1009
|
-
"daemonsets",
|
|
1010
|
-
"services",
|
|
1011
|
-
"ingresses",
|
|
1012
|
-
"jobs",
|
|
1013
|
-
"cronjobs",
|
|
1014
|
-
"horizontalpodautoscalers",
|
|
1015
|
-
"configmaps",
|
|
1016
|
-
"secrets",
|
|
1017
|
-
"nodes",
|
|
1018
|
-
"namespaces",
|
|
1019
|
-
"events",
|
|
1020
|
-
"persistentvolumeclaims",
|
|
1021
|
-
"persistentvolumes",
|
|
1022
|
-
"serviceaccounts",
|
|
1023
|
-
"roles",
|
|
1024
|
-
"clusterroles",
|
|
1025
|
-
"rolebindings",
|
|
1026
|
-
"clusterrolebindings",
|
|
1027
|
-
"networkpolicies",
|
|
1028
|
-
"poddisruptionbudgets",
|
|
1029
|
-
];
|
|
1030
|
-
|
|
1031
|
-
const kindMap = new Map(kinds.map((kind) => [kind.name, kind]));
|
|
1032
|
-
const curated = preferredOrder
|
|
1033
|
-
.map((name) => kindMap.get(name))
|
|
1034
|
-
.filter((kind): kind is ResourceKind => kind !== undefined);
|
|
1035
|
-
|
|
1036
|
-
if (curated.length > 0) {
|
|
1037
|
-
return curated;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
return kinds.slice(0, 20);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function normalizeKind(kind: string): string {
|
|
1044
|
-
return kind.trim().toLowerCase();
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function normalizeLogTarget(ref: ResourceRef): { target: string; includeAllPods: boolean } | undefined {
|
|
1048
|
-
const kind = normalizeKind(ref.kind);
|
|
1049
|
-
|
|
1050
|
-
if (kind === "pod" || kind === "pods") {
|
|
1051
|
-
return { target: `pod/${ref.name}`, includeAllPods: false };
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
if (kind === "deployment" || kind === "deployments") {
|
|
1055
|
-
return { target: `deployment/${ref.name}`, includeAllPods: true };
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (kind === "job" || kind === "jobs") {
|
|
1059
|
-
return { target: `job/${ref.name}`, includeAllPods: true };
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (kind === "replicaset" || kind === "replicasets") {
|
|
1063
|
-
return { target: `replicaset/${ref.name}`, includeAllPods: true };
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
if (kind === "statefulset" || kind === "statefulsets") {
|
|
1067
|
-
return { target: `statefulset/${ref.name}`, includeAllPods: true };
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
if (kind === "daemonset" || kind === "daemonsets") {
|
|
1071
|
-
return { target: `daemonset/${ref.name}`, includeAllPods: true };
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
return undefined;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function normalizeResourceTarget(ref: ResourceRef): string {
|
|
1078
|
-
const kind = normalizeKind(ref.kind);
|
|
1079
|
-
return `${kind}/${ref.name}`;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function normalizePortForwardTarget(ref: ResourceRef): string | undefined {
|
|
1083
|
-
const kind = normalizeKind(ref.kind);
|
|
1084
|
-
|
|
1085
|
-
if (kind === "pod" || kind === "pods") {
|
|
1086
|
-
return `pod/${ref.name}`;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
if (kind === "service" || kind === "services") {
|
|
1090
|
-
return `service/${ref.name}`;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
if (kind === "deployment" || kind === "deployments") {
|
|
1094
|
-
return `deployment/${ref.name}`;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
if (kind === "replicaset" || kind === "replicasets") {
|
|
1098
|
-
return `replicaset/${ref.name}`;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
if (kind === "statefulset" || kind === "statefulsets") {
|
|
1102
|
-
return `statefulset/${ref.name}`;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
return undefined;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
function supportsRolloutRestart(ref: ResourceRef): boolean {
|
|
1109
|
-
const kind = normalizeKind(ref.kind);
|
|
1110
|
-
return ["deployment", "deployments", "statefulset", "statefulsets", "daemonset", "daemonsets"].includes(kind);
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
function supportsScale(ref: ResourceRef): boolean {
|
|
1114
|
-
const kind = normalizeKind(ref.kind);
|
|
1115
|
-
return ["deployment", "deployments", "statefulset", "statefulsets", "replicaset", "replicasets"].includes(kind);
|
|
1116
628
|
}
|