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
@@ -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
- runCommand({ command: "kubectl", args: ["--context", options.context, "api-resources", "--cached=false", "-o", "wide"], timeoutMs: KUBECTL_TIMEOUT_MS }),
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 lines = result.stdout
72
- .split("\n")
73
- .map((line) => line.trimEnd())
74
- .filter(Boolean);
75
-
76
- const dataLines = lines.slice(1);
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, { cpu: number; memory: number }>();
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
  }