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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asArray, asBoolean, asNumber, asRecord, asString,
|
|
3
|
+
buildEnvLine, formatContainerState, formatEnvFromSource,
|
|
4
|
+
formatProbe, formatValueFrom, formatVolumeSource,
|
|
5
|
+
} from "../resource-parser";
|
|
6
|
+
import type { ResourceDetailSection } from "../types";
|
|
7
|
+
import type { KubernetesResource } from "../resource-parser";
|
|
8
|
+
|
|
9
|
+
export function buildPodSections(resource: KubernetesResource): ResourceDetailSection[] {
|
|
10
|
+
const spec = resource.spec ?? {};
|
|
11
|
+
const status = resource.status ?? {};
|
|
12
|
+
|
|
13
|
+
const containerStatuses = new Map(
|
|
14
|
+
asArray(status.containerStatuses)
|
|
15
|
+
.map((entry) => asRecord(entry))
|
|
16
|
+
.filter((entry): entry is Record<string, unknown> => Boolean(entry && asString(entry.name)))
|
|
17
|
+
.map((entry) => [asString(entry.name) ?? "", entry]),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const containers = asArray(spec.containers)
|
|
21
|
+
.map((entry) => asRecord(entry))
|
|
22
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined);
|
|
23
|
+
|
|
24
|
+
const containerLines = containers.map((container) => {
|
|
25
|
+
const name = asString(container.name) ?? "container";
|
|
26
|
+
const statusEntry = containerStatuses.get(name);
|
|
27
|
+
const ready = asBoolean(statusEntry?.ready);
|
|
28
|
+
const restartCount = asNumber(statusEntry?.restartCount);
|
|
29
|
+
|
|
30
|
+
return `${name} image ${asString(container.image) ?? "-"} ready ${ready === undefined ? "-" : ready ? "yes" : "no"} restarts ${restartCount ?? 0} state ${formatContainerState(statusEntry?.state)}`;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const environmentLines = containers.flatMap((container) => {
|
|
34
|
+
const containerName = asString(container.name) ?? "container";
|
|
35
|
+
const envLines = asArray(container.env)
|
|
36
|
+
.map((entry) => asRecord(entry))
|
|
37
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined)
|
|
38
|
+
.map((entry, index) => {
|
|
39
|
+
const name = asString(entry.name) ?? "ENV";
|
|
40
|
+
const value = asString(entry.value);
|
|
41
|
+
const valueFrom = asRecord(entry.valueFrom);
|
|
42
|
+
const secretKeyRef = asRecord(valueFrom?.secretKeyRef);
|
|
43
|
+
|
|
44
|
+
if (secretKeyRef) {
|
|
45
|
+
const secretName = asString(secretKeyRef.name) ?? "unknown";
|
|
46
|
+
const secretKey = asString(secretKeyRef.key) ?? "key";
|
|
47
|
+
const secretRefText = `secret:${secretName}/${secretKey}`;
|
|
48
|
+
return buildEnvLine(
|
|
49
|
+
`${containerName}:env:${name}:${index}`,
|
|
50
|
+
`${name}=*****`,
|
|
51
|
+
`${name}=${secretRefText}`,
|
|
52
|
+
{ name: secretName, key: secretKey },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return buildEnvLine(`${containerName}:env:${name}:${index}`, `${name}=${value ?? formatValueFrom(entry.valueFrom)}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const envFromLines = asArray(container.envFrom)
|
|
60
|
+
.map((entry) => asRecord(entry))
|
|
61
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined)
|
|
62
|
+
.map((entry, index) => {
|
|
63
|
+
const secretRef = asRecord(entry.secretRef);
|
|
64
|
+
|
|
65
|
+
if (secretRef) {
|
|
66
|
+
const secretName = asString(secretRef.name) ?? "unknown";
|
|
67
|
+
return buildEnvLine(
|
|
68
|
+
`${containerName}:envfrom:secret:${index}`,
|
|
69
|
+
`envFrom: secret/${secretName} → *****`,
|
|
70
|
+
`envFrom: secret/${secretName} (all keys imported)`,
|
|
71
|
+
{ name: secretName },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return buildEnvLine(`${containerName}:envfrom:${index}`, `envFrom: ${formatEnvFromSource(entry)}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return [...envLines, ...envFromLines];
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const portLines = containers.flatMap((container) => {
|
|
82
|
+
const containerName = asString(container.name) ?? "container";
|
|
83
|
+
|
|
84
|
+
return asArray(container.ports)
|
|
85
|
+
.map((entry) => asRecord(entry))
|
|
86
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined)
|
|
87
|
+
.map((entry) => {
|
|
88
|
+
const port = entry.containerPort ?? "?";
|
|
89
|
+
const protocol = asString(entry.protocol) ?? "TCP";
|
|
90
|
+
const hostPort = entry.hostPort ? ` host ${entry.hostPort}` : "";
|
|
91
|
+
return `${containerName}: ${String(port)}/${protocol}${hostPort}`;
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const mountLines = containers.flatMap((container) => {
|
|
96
|
+
const containerName = asString(container.name) ?? "container";
|
|
97
|
+
|
|
98
|
+
return asArray(container.volumeMounts)
|
|
99
|
+
.map((entry) => asRecord(entry))
|
|
100
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined)
|
|
101
|
+
.map((entry) => {
|
|
102
|
+
const mountPath = asString(entry.mountPath) ?? "?";
|
|
103
|
+
const mountName = asString(entry.name) ?? "volume";
|
|
104
|
+
return `${containerName}: ${mountPath} <- ${mountName}${asBoolean(entry.readOnly) ? " (ro)" : ""}`;
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const probeLines = containers.map((container) => {
|
|
109
|
+
const containerName = asString(container.name) ?? "container";
|
|
110
|
+
return `${containerName}: readiness ${formatProbe(container.readinessProbe)} | liveness ${formatProbe(container.livenessProbe)} | startup ${formatProbe(container.startupProbe)}`;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const conditionLines = asArray(status.conditions)
|
|
114
|
+
.map((entry) => asRecord(entry))
|
|
115
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined)
|
|
116
|
+
.map((entry) => {
|
|
117
|
+
const type = asString(entry.type) ?? "Condition";
|
|
118
|
+
const conditionStatus = asString(entry.status) ?? "Unknown";
|
|
119
|
+
const transition = asString(entry.lastTransitionTime) ?? "unknown";
|
|
120
|
+
return `${type}: ${conditionStatus} transitioned ${transition}`;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const volumeLines = asArray(spec.volumes)
|
|
124
|
+
.map((entry) => asRecord(entry))
|
|
125
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined)
|
|
126
|
+
.map((entry) => `${asString(entry.name) ?? "volume"}: ${formatVolumeSource(entry)}`);
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
{ title: "Containers", lines: containerLines.length > 0 ? containerLines : ["No containers"] },
|
|
130
|
+
{
|
|
131
|
+
title: "Environment",
|
|
132
|
+
lines: environmentLines.length > 0 ? environmentLines : ["No environment variables or imports"],
|
|
133
|
+
},
|
|
134
|
+
{ title: "Ports", lines: portLines.length > 0 ? portLines : ["No declared ports"] },
|
|
135
|
+
{ title: "Volume Mounts", lines: mountLines.length > 0 ? mountLines : ["No volume mounts"] },
|
|
136
|
+
{ title: "Probes", lines: probeLines.length > 0 ? probeLines : ["No probes configured"] },
|
|
137
|
+
{ title: "Conditions", lines: conditionLines.length > 0 ? conditionLines : ["No conditions reported"] },
|
|
138
|
+
{ title: "Volumes", lines: volumeLines.length > 0 ? volumeLines : ["No volumes"] },
|
|
139
|
+
];
|
|
140
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { asArray, asRecord, asString } from "../resource-parser";
|
|
2
|
+
import type { ResourceDetailSection } from "../types";
|
|
3
|
+
import type { KubernetesResource } from "../resource-parser";
|
|
4
|
+
|
|
5
|
+
export function buildRoleSections(resource: KubernetesResource): ResourceDetailSection[] {
|
|
6
|
+
const raw = resource as unknown as Record<string, unknown>;
|
|
7
|
+
const rules = asArray(raw.rules)
|
|
8
|
+
.map((r) => asRecord(r))
|
|
9
|
+
.filter((r): r is Record<string, unknown> => r !== undefined)
|
|
10
|
+
.map((r) => {
|
|
11
|
+
const verbs = asArray(r.verbs).map((v) => String(v)).join(",");
|
|
12
|
+
const resources = asArray(r.resources).map((res) => String(res)).join(",");
|
|
13
|
+
const apiGroups = asArray(r.apiGroups).map((g) => String(g) || '""').join(",");
|
|
14
|
+
return `[${apiGroups}] ${resources}: ${verbs}`;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return [{ title: "Rules", lines: rules.length > 0 ? rules : ["No rules"] }];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildRoleBindingSections(resource: KubernetesResource): ResourceDetailSection[] {
|
|
21
|
+
const raw = resource as unknown as Record<string, unknown>;
|
|
22
|
+
const roleRef = asRecord(raw.roleRef);
|
|
23
|
+
const refLines = roleRef
|
|
24
|
+
? [`${asString(roleRef.kind) ?? "-"} / ${asString(roleRef.name) ?? "-"}`]
|
|
25
|
+
: ["No roleRef"];
|
|
26
|
+
|
|
27
|
+
const subjects = asArray(raw.subjects)
|
|
28
|
+
.map((s) => asRecord(s))
|
|
29
|
+
.filter((s): s is Record<string, unknown> => s !== undefined)
|
|
30
|
+
.map((s) => {
|
|
31
|
+
const ns = asString(s.namespace);
|
|
32
|
+
return `${asString(s.kind) ?? "-"} ${ns ? `${ns}/` : ""}${asString(s.name) ?? "-"}`;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{ title: "RoleRef", lines: refLines },
|
|
37
|
+
{ title: "Subjects", lines: subjects.length > 0 ? subjects : ["No subjects"] },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildServiceAccountSections(resource: KubernetesResource): ResourceDetailSection[] {
|
|
42
|
+
const raw = resource as unknown as Record<string, unknown>;
|
|
43
|
+
const secrets = asArray(raw.secrets)
|
|
44
|
+
.map((s) => asRecord(s))
|
|
45
|
+
.filter((s): s is Record<string, unknown> => s !== undefined)
|
|
46
|
+
.map((s) => asString(s.name) ?? "-");
|
|
47
|
+
|
|
48
|
+
const imagePullSecrets = asArray(raw.imagePullSecrets)
|
|
49
|
+
.map((s) => asRecord(s))
|
|
50
|
+
.filter((s): s is Record<string, unknown> => s !== undefined)
|
|
51
|
+
.map((s) => asString(s.name) ?? "-");
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
{ title: "Secrets", lines: secrets.length > 0 ? secrets : ["None"] },
|
|
55
|
+
...(imagePullSecrets.length > 0 ? [{ title: "Image Pull Secrets", lines: imagePullSecrets }] : []),
|
|
56
|
+
];
|
|
57
|
+
}
|