openk8s 0.0.1 → 1.0.1
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 +187 -36
- package/bin/openk8s.js +2 -0
- package/package.json +52 -6
- package/src/app/app-state.ts +461 -0
- package/src/app/app.tsx +708 -0
- package/src/app/components/inspector.tsx +449 -0
- package/src/app/components/kind-rows.tsx +66 -0
- package/src/app/components/notification-tray.tsx +59 -0
- package/src/app/components/overlays/delete-confirm-overlay.tsx +79 -0
- package/src/app/components/overlays/helm-rollback-overlay.tsx +86 -0
- package/src/app/components/overlays/index.ts +12 -0
- package/src/app/components/overlays/logs-dialog.tsx +303 -0
- package/src/app/components/overlays/port-forward-overlay.tsx +184 -0
- package/src/app/components/overlays/scale-dialog.tsx +96 -0
- package/src/app/components/overlays/select-overlay.tsx +68 -0
- package/src/app/components/overlays/shared.tsx +18 -0
- package/src/app/components/port-forwards-tray.tsx +57 -0
- package/src/app/components/resource-rows.tsx +120 -0
- package/src/app/hooks/use-app-keyboard.ts +723 -0
- package/src/app/hooks/use-app-side-effects.ts +39 -0
- package/src/app/hooks/use-clipboard.ts +54 -0
- package/src/app/hooks/use-data-fetching.ts +366 -0
- package/src/app/hooks/use-log-stream.ts +113 -0
- package/src/app/hooks/use-port-forward.ts +149 -0
- package/src/app/persistence.ts +44 -0
- package/src/app/theme.ts +95 -0
- package/src/app/use-polling-tick.ts +27 -0
- package/src/app/utils.ts +274 -0
- package/src/index.tsx +8 -0
- package/src/lib/k8s/k8s-format.ts +42 -0
- package/src/lib/k8s/resource-detail-builder.ts +545 -0
- package/src/lib/k8s/resource-parser.ts +308 -0
- package/src/lib/k8s/types.ts +164 -0
- package/src/lib/kubectl/kubectl-service.ts +1116 -0
- package/src/lib/kubectl/spawn-utils.ts +81 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { ResourceDetailLine, ResourceDetailSection } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface KubernetesMetadata {
|
|
4
|
+
name?: string | undefined;
|
|
5
|
+
namespace?: string | undefined;
|
|
6
|
+
creationTimestamp?: string | undefined;
|
|
7
|
+
labels?: Record<string, string> | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface KubernetesResource {
|
|
11
|
+
apiVersion?: string | undefined;
|
|
12
|
+
kind?: string | undefined;
|
|
13
|
+
metadata?: KubernetesMetadata | undefined;
|
|
14
|
+
spec?: Record<string, unknown> | undefined;
|
|
15
|
+
status?: Record<string, unknown> | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function asString(value: unknown): string | undefined {
|
|
23
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function asNumber(value: unknown): number | undefined {
|
|
27
|
+
return typeof value === "number" ? value : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function asBoolean(value: unknown): boolean | undefined {
|
|
31
|
+
return typeof value === "boolean" ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function asArray(value: unknown): unknown[] {
|
|
35
|
+
return Array.isArray(value) ? value : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function summarizeLabels(labels?: Record<string, string> | undefined): string {
|
|
39
|
+
const pairs = Object.entries(labels ?? {});
|
|
40
|
+
|
|
41
|
+
if (pairs.length === 0) {
|
|
42
|
+
return "none";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return pairs
|
|
46
|
+
.slice(0, 6)
|
|
47
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
48
|
+
.join(", ");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function summarizeIngressHosts(spec: Record<string, unknown>): string | undefined {
|
|
52
|
+
const rules = asArray(spec.rules);
|
|
53
|
+
const hosts = rules
|
|
54
|
+
.map((rule) => asString(asRecord(rule)?.host))
|
|
55
|
+
.filter((host): host is string => host !== undefined);
|
|
56
|
+
|
|
57
|
+
return hosts.length > 0 ? hosts.join(", ") : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isPodKind(kind: string): boolean {
|
|
61
|
+
const normalized = kind.trim().toLowerCase();
|
|
62
|
+
return normalized === "pod" || normalized === "pods";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function normalizeKind(kind: string): string {
|
|
66
|
+
return kind.trim().toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function supportsDirectPortForward(kind: string): boolean {
|
|
70
|
+
const normalized = normalizeKind(kind);
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
"pod",
|
|
74
|
+
"pods",
|
|
75
|
+
"service",
|
|
76
|
+
"services",
|
|
77
|
+
"deployment",
|
|
78
|
+
"deployments",
|
|
79
|
+
"replicaset",
|
|
80
|
+
"replicasets",
|
|
81
|
+
"statefulset",
|
|
82
|
+
"statefulsets",
|
|
83
|
+
].includes(normalized);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function flattenSections(sections: ResourceDetailSection[]): string[] {
|
|
87
|
+
return sections.flatMap((section) => [
|
|
88
|
+
section.title,
|
|
89
|
+
...section.lines.map((line) => ` ${typeof line === "string" ? line : line.text}`),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function asPort(value: unknown): number | undefined {
|
|
94
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65_535) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof value === "string" && /^\d+$/.test(value)) {
|
|
99
|
+
const parsed = Number.parseInt(value, 10);
|
|
100
|
+
return parsed > 0 && parsed <= 65_535 ? parsed : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function suggestedLocalPort(remotePort: number): number {
|
|
107
|
+
if (remotePort === 80) {
|
|
108
|
+
return 8080;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (remotePort === 443) {
|
|
112
|
+
return 8443;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return remotePort;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function extractContainerPorts(specValue: unknown): number[] {
|
|
119
|
+
const spec = asRecord(specValue);
|
|
120
|
+
|
|
121
|
+
if (!spec) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const ports = asArray(spec.containers)
|
|
126
|
+
.map((container) => asRecord(container))
|
|
127
|
+
.filter((container): container is Record<string, unknown> => container !== undefined)
|
|
128
|
+
.flatMap((container) =>
|
|
129
|
+
asArray(container.ports)
|
|
130
|
+
.map((port) => asPort(asRecord(port)?.containerPort))
|
|
131
|
+
.filter((port): port is number => port !== undefined),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return [...new Set(ports)];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function extractContainerNames(specValue: unknown): string[] {
|
|
138
|
+
const spec = asRecord(specValue);
|
|
139
|
+
|
|
140
|
+
if (!spec) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return asArray(spec.containers)
|
|
145
|
+
.map((container) => asString(asRecord(container)?.name))
|
|
146
|
+
.filter((name): name is string => name !== undefined);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatContainerState(value: unknown): string {
|
|
150
|
+
const state = asRecord(value);
|
|
151
|
+
|
|
152
|
+
if (!state) {
|
|
153
|
+
return "waiting";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (state.running) {
|
|
157
|
+
return "running";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (state.waiting) {
|
|
161
|
+
return `waiting (${asString(asRecord(state.waiting)?.reason) ?? "unknown"})`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (state.terminated) {
|
|
165
|
+
return `terminated (${asString(asRecord(state.terminated)?.reason) ?? "unknown"})`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return "unknown";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function formatValueFrom(value: unknown): string {
|
|
172
|
+
const valueFrom = asRecord(value);
|
|
173
|
+
|
|
174
|
+
if (!valueFrom) {
|
|
175
|
+
return "-";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fieldRef = asRecord(valueFrom.fieldRef);
|
|
179
|
+
if (fieldRef) {
|
|
180
|
+
return `field:${asString(fieldRef.fieldPath) ?? "unknown"}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const resourceFieldRef = asRecord(valueFrom.resourceFieldRef);
|
|
184
|
+
if (resourceFieldRef) {
|
|
185
|
+
return `resource:${asString(resourceFieldRef.resource) ?? "unknown"}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const configMapKeyRef = asRecord(valueFrom.configMapKeyRef);
|
|
189
|
+
if (configMapKeyRef) {
|
|
190
|
+
return `configmap:${asString(configMapKeyRef.name) ?? "unknown"}/${asString(configMapKeyRef.key) ?? "key"}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const secretKeyRef = asRecord(valueFrom.secretKeyRef);
|
|
194
|
+
if (secretKeyRef) {
|
|
195
|
+
return `secret:${asString(secretKeyRef.name) ?? "unknown"}/${asString(secretKeyRef.key) ?? "key"}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return "-";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function buildEnvLine(
|
|
202
|
+
id: string,
|
|
203
|
+
text: string,
|
|
204
|
+
revealedText?: string | undefined,
|
|
205
|
+
secretRef?: ResourceDetailLine["secretRef"],
|
|
206
|
+
): ResourceDetailLine {
|
|
207
|
+
return revealedText
|
|
208
|
+
? {
|
|
209
|
+
id,
|
|
210
|
+
text,
|
|
211
|
+
revealedText,
|
|
212
|
+
revealable: true,
|
|
213
|
+
secretRef,
|
|
214
|
+
}
|
|
215
|
+
: {
|
|
216
|
+
id,
|
|
217
|
+
text,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatEnvFromSource(value: unknown): string {
|
|
222
|
+
const envFrom = asRecord(value);
|
|
223
|
+
|
|
224
|
+
if (!envFrom) {
|
|
225
|
+
return "-";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const prefix = asString(envFrom.prefix);
|
|
229
|
+
const configMapRef = asRecord(envFrom.configMapRef);
|
|
230
|
+
if (configMapRef) {
|
|
231
|
+
return `${prefix ? `${prefix} <= ` : ""}configmap:${asString(configMapRef.name) ?? "unknown"}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const secretRef = asRecord(envFrom.secretRef);
|
|
235
|
+
if (secretRef) {
|
|
236
|
+
return `${prefix ? `${prefix} <= ` : ""}secret:${asString(secretRef.name) ?? "unknown"}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return "-";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function formatProbe(value: unknown): string {
|
|
243
|
+
const probe = asRecord(value);
|
|
244
|
+
|
|
245
|
+
if (!probe) {
|
|
246
|
+
return "disabled";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const httpGet = asRecord(probe.httpGet);
|
|
250
|
+
if (httpGet) {
|
|
251
|
+
return `http ${asString(httpGet.path) ?? "/"} port ${String(httpGet.port ?? "?")}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const tcpSocket = asRecord(probe.tcpSocket);
|
|
255
|
+
if (tcpSocket) {
|
|
256
|
+
return `tcp port ${String(tcpSocket.port ?? "?")}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const exec = asRecord(probe.exec);
|
|
260
|
+
if (exec) {
|
|
261
|
+
const command = asArray(exec.command)
|
|
262
|
+
.map((part) => asString(part))
|
|
263
|
+
.filter((part): part is string => part !== undefined)
|
|
264
|
+
.join(" ");
|
|
265
|
+
return `exec ${command || "command"}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const grpc = asRecord(probe.grpc);
|
|
269
|
+
if (grpc) {
|
|
270
|
+
return `grpc port ${String(grpc.port ?? "?")}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return "configured";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function formatVolumeSource(value: unknown): string {
|
|
277
|
+
const volume = asRecord(value);
|
|
278
|
+
|
|
279
|
+
if (!volume) {
|
|
280
|
+
return "unknown";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (volume.configMap) {
|
|
284
|
+
return `configmap:${asString(asRecord(volume.configMap)?.name) ?? "unknown"}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (volume.secret) {
|
|
288
|
+
return `secret:${asString(asRecord(volume.secret)?.secretName) ?? "unknown"}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (volume.persistentVolumeClaim) {
|
|
292
|
+
return `pvc:${asString(asRecord(volume.persistentVolumeClaim)?.claimName) ?? "unknown"}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (volume.projected) {
|
|
296
|
+
return "projected";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (volume.emptyDir) {
|
|
300
|
+
return "emptyDir";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (volume.downwardAPI) {
|
|
304
|
+
return "downwardAPI";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return "other";
|
|
308
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
export type PaneId = "clusters" | "resources" | "inspector";
|
|
2
|
+
|
|
3
|
+
export type OverlayId = "cluster-switcher" | "namespace-switcher" | "delete-confirm" | "port-forward" | "logs" | "scale" | "container-picker" | "helm-rollback";
|
|
4
|
+
|
|
5
|
+
export type InspectorTab = "summary" | "yaml" | "events" | "describe";
|
|
6
|
+
|
|
7
|
+
export type LoadStatus = "idle" | "loading" | "ready" | "error";
|
|
8
|
+
|
|
9
|
+
export type PortForwardStatus = "starting" | "ready";
|
|
10
|
+
|
|
11
|
+
export interface LogOptions {
|
|
12
|
+
tail: number;
|
|
13
|
+
since?: string | undefined;
|
|
14
|
+
previous: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ClusterContext {
|
|
18
|
+
name: string;
|
|
19
|
+
isCurrent: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NamespaceItem {
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResourceKind {
|
|
27
|
+
name: string;
|
|
28
|
+
namespaced: boolean;
|
|
29
|
+
group?: string | undefined;
|
|
30
|
+
shortNames: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResourceRef {
|
|
34
|
+
kind: string;
|
|
35
|
+
name: string;
|
|
36
|
+
namespace?: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ResourceListItem {
|
|
40
|
+
ref: ResourceRef;
|
|
41
|
+
status: string;
|
|
42
|
+
age: string;
|
|
43
|
+
summary: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResourcePortForward {
|
|
47
|
+
localPort: number;
|
|
48
|
+
remotePort: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResourceDetailLine {
|
|
52
|
+
id: string;
|
|
53
|
+
text: string;
|
|
54
|
+
/** The actual value shown when the user reveals a concealed line (e.g. a secret reference). */
|
|
55
|
+
revealedText?: string | undefined;
|
|
56
|
+
revealable?: boolean | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Present when this line is backed by a Kubernetes Secret.
|
|
59
|
+
* key is set for a specific key ref; omitted for envFrom (all keys).
|
|
60
|
+
*/
|
|
61
|
+
secretRef?: { name: string; key?: string | undefined } | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ResourceDetailSection {
|
|
65
|
+
title: string;
|
|
66
|
+
lines: Array<string | ResourceDetailLine>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ResourceDetail {
|
|
70
|
+
ref: ResourceRef;
|
|
71
|
+
summaryLines: string[];
|
|
72
|
+
summarySections: ResourceDetailSection[];
|
|
73
|
+
yaml: string;
|
|
74
|
+
describe?: string | undefined;
|
|
75
|
+
replicas?: number | undefined;
|
|
76
|
+
portForwards?: ResourcePortForward[] | undefined;
|
|
77
|
+
containers?: string[] | undefined;
|
|
78
|
+
helmChart?: string | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface EventItem {
|
|
82
|
+
type: string;
|
|
83
|
+
reason: string;
|
|
84
|
+
message: string;
|
|
85
|
+
age: string;
|
|
86
|
+
source: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ActivePortForward {
|
|
90
|
+
id: string;
|
|
91
|
+
context: string;
|
|
92
|
+
namespace: string;
|
|
93
|
+
namespaced: boolean;
|
|
94
|
+
ref: ResourceRef;
|
|
95
|
+
localPort: number;
|
|
96
|
+
remotePort: number;
|
|
97
|
+
status: PortForwardStatus;
|
|
98
|
+
message?: string | undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AppError {
|
|
102
|
+
message: string;
|
|
103
|
+
detail?: string | undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type NotificationTone = "info" | "success" | "warning" | "danger";
|
|
107
|
+
|
|
108
|
+
export interface Notification {
|
|
109
|
+
id: string;
|
|
110
|
+
tone: NotificationTone;
|
|
111
|
+
message: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface PodMetricEntry {
|
|
115
|
+
cpu: string;
|
|
116
|
+
memory: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface NodeMetricEntry {
|
|
120
|
+
cpu: string;
|
|
121
|
+
memory: string;
|
|
122
|
+
cpuPct: number;
|
|
123
|
+
memPct: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface AppState {
|
|
127
|
+
activePane: PaneId;
|
|
128
|
+
overlay?: OverlayId | undefined;
|
|
129
|
+
inspectorTab: InspectorTab;
|
|
130
|
+
kubectlAvailable: boolean;
|
|
131
|
+
statusMessage: string;
|
|
132
|
+
error?: AppError | undefined;
|
|
133
|
+
contexts: ClusterContext[];
|
|
134
|
+
contextsStatus: LoadStatus;
|
|
135
|
+
activeContext?: string | undefined;
|
|
136
|
+
namespaces: NamespaceItem[];
|
|
137
|
+
namespacesStatus: LoadStatus;
|
|
138
|
+
activeNamespace: string;
|
|
139
|
+
resourceKinds: ResourceKind[];
|
|
140
|
+
resourceKindsStatus: LoadStatus;
|
|
141
|
+
selectedKind: string;
|
|
142
|
+
resources: ResourceListItem[];
|
|
143
|
+
resourcesStatus: LoadStatus;
|
|
144
|
+
resourceFilter: string;
|
|
145
|
+
selectedResourceName?: string | undefined;
|
|
146
|
+
selectedResourceNames: string[];
|
|
147
|
+
selectedResourceDetail?: ResourceDetail | undefined;
|
|
148
|
+
selectedResourceDetailStatus: LoadStatus;
|
|
149
|
+
events: EventItem[];
|
|
150
|
+
eventsStatus: LoadStatus;
|
|
151
|
+
logsTarget?: string | undefined;
|
|
152
|
+
logsText: string;
|
|
153
|
+
logsStatus: LoadStatus;
|
|
154
|
+
logsContainer?: string | undefined;
|
|
155
|
+
logsOptions: LogOptions;
|
|
156
|
+
revealedDetailLineIds: string[];
|
|
157
|
+
/** Maps line ID → fetched plaintext secret value for lines that have been revealed */
|
|
158
|
+
revealedSecretValues: Record<string, string>;
|
|
159
|
+
activePortForwards: ActivePortForward[];
|
|
160
|
+
notifications: Notification[];
|
|
161
|
+
podMetrics: Record<string, { cpu: string; memory: string }>;
|
|
162
|
+
nodeMetrics: Record<string, { cpu: string; memory: string; cpuPct: number; memPct: number }>;
|
|
163
|
+
helmRollbackRevision: string;
|
|
164
|
+
}
|