feat(kubernetes): add support for Persistent Volume Claims (PVCs) and Persistent Volumes (PVs)

- Implemented KubernetesClusterPVCDetail and KubernetesClusterPVCs components to display PVC details and list.
- Created KubernetesClusterPVs component to list all Persistent Volumes in the cluster.
- Updated routing and page mapping for PVCs and PVs.
- Enhanced existing Kubernetes components to utilize new utility functions for better data handling.
- Removed unused InfoCard components for a cleaner UI.
- Added yAxisValueFormatter to metric queries for better chart representation.
- Updated Helm chart to include PVC and PV data collection.
This commit is contained in:
Nawaz Dhandala
2026-03-19 19:27:57 +00:00
parent db1ce405f5
commit ff0a2e9c91
35 changed files with 2269 additions and 557 deletions

View File

@@ -1,15 +1,7 @@
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import {
fetchPodLogs,
KubernetesLogEntry,
} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, { FunctionComponent, ReactElement, useMemo } from "react";
import DashboardLogsViewer from "../Logs/LogsViewer";
import Query from "Common/Types/BaseDatabase/Query";
import Log from "Common/Models/AnalyticsModels/Log";
export interface ComponentProps {
clusterIdentifier: string;
@@ -21,29 +13,23 @@ export interface ComponentProps {
const KubernetesLogsTab: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [logs, setLogs] = useState<Array<KubernetesLogEntry>>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
useEffect(() => {
const fetchLogs: () => Promise<void> = async (): Promise<void> => {
setIsLoading(true);
try {
const result: Array<KubernetesLogEntry> = await fetchPodLogs({
clusterIdentifier: props.clusterIdentifier,
podName: props.podName,
containerName: props.containerName,
namespace: props.namespace,
limit: 500,
});
setLogs(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch logs");
}
setIsLoading(false);
const logQuery: Query<Log> = useMemo(() => {
const attributeFilters: Record<string, string> = {
"resource.k8s.cluster.name": props.clusterIdentifier,
"resource.k8s.pod.name": props.podName,
};
fetchLogs().catch(() => {});
if (props.containerName) {
attributeFilters["resource.k8s.container.name"] = props.containerName;
}
if (props.namespace) {
attributeFilters["resource.k8s.namespace.name"] = props.namespace;
}
return {
attributes: attributeFilters,
} as Query<Log>;
}, [
props.clusterIdentifier,
props.podName,
@@ -51,65 +37,13 @@ const KubernetesLogsTab: FunctionComponent<ComponentProps> = (
props.namespace,
]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (logs.length === 0) {
return (
<div className="text-gray-500 text-sm p-4">
No application logs found for this pod in the last 6 hours. Logs will
appear here once the kubernetes-agent&apos;s filelog receiver is
collecting data.
</div>
);
}
const getSeverityColor: (severity: string) => string = (
severity: string,
): string => {
const s: string = severity.toUpperCase();
if (s === "ERROR" || s === "FATAL" || s === "CRITICAL") {
return "text-red-600";
}
if (s === "WARN" || s === "WARNING") {
return "text-yellow-600";
}
if (s === "DEBUG" || s === "TRACE") {
return "text-gray-400";
}
return "text-gray-700";
};
return (
<div className="bg-gray-900 rounded-lg p-4 overflow-auto max-h-[600px] font-mono text-xs">
{logs.map((log: KubernetesLogEntry, index: number) => {
return (
<div key={index} className="flex gap-2 py-0.5 hover:bg-gray-800">
<span className="text-gray-500 whitespace-nowrap flex-shrink-0">
{log.timestamp}
</span>
{log.containerName && (
<span className="text-blue-400 whitespace-nowrap flex-shrink-0">
[{log.containerName}]
</span>
)}
<span
className={`whitespace-nowrap flex-shrink-0 w-12 ${getSeverityColor(log.severity)}`}
>
{log.severity}
</span>
<span className="text-gray-200 whitespace-pre-wrap break-all">
{log.body}
</span>
</div>
);
})}
</div>
<DashboardLogsViewer
id={`k8s-logs-${props.podName}`}
logQuery={logQuery}
showFilters={true}
noLogsMessage="No application logs found for this pod. Logs will appear here once the kubernetes-agent's filelog receiver is collecting data."
/>
);
};

View File

@@ -1,4 +1,9 @@
import React, { FunctionComponent, ReactElement } from "react";
import React, {
FunctionComponent,
ReactElement,
useMemo,
useState,
} from "react";
import KubernetesResourceUtils, {
KubernetesResource,
} from "../../Pages/Kubernetes/Utils/KubernetesResourceUtils";
@@ -9,6 +14,7 @@ import Link from "Common/UI/Components/Link/Link";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import Route from "Common/Types/API/Route";
import Column from "Common/UI/Components/Table/Types/Column";
import Input from "Common/UI/Components/Input/Input";
export interface ResourceColumn {
title: string;
@@ -22,21 +28,132 @@ export interface ComponentProps {
description: string;
columns?: Array<ResourceColumn>;
showNamespace?: boolean;
showStatus?: boolean;
showResourceMetrics?: boolean;
getViewRoute?: (resource: KubernetesResource) => Route;
emptyMessage?: string;
isLoading?: boolean;
}
const PAGE_SIZE: number = 25;
function getStatusBadgeClass(status: string): string {
const s: string = status.toLowerCase();
if (
s === "running" ||
s === "ready" ||
s === "active" ||
s === "bound" ||
s === "succeeded" ||
s === "available" ||
s === "true"
) {
return "bg-green-50 text-green-700";
}
if (
s === "pending" ||
s === "unknown" ||
s === "waiting" ||
s === "terminating"
) {
return "bg-yellow-50 text-yellow-700";
}
if (
s === "failed" ||
s === "crashloopbackoff" ||
s === "error" ||
s === "lost" ||
s === "notready" ||
s === "imagepullbackoff" ||
s === "false"
) {
return "bg-red-50 text-red-700";
}
return "bg-gray-50 text-gray-700";
}
function getCpuBarColor(pct: number): string {
if (pct > 80) {
return "bg-red-500";
}
if (pct > 60) {
return "bg-yellow-500";
}
return "bg-green-500";
}
function getMemoryBarColor(pct: number): string {
if (pct > 85) {
return "bg-red-500";
}
if (pct > 70) {
return "bg-yellow-500";
}
return "bg-blue-500";
}
const KubernetesResourceTable: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const showNamespace: boolean = props.showNamespace !== false;
const showStatus: boolean = props.showStatus !== false;
const showResourceMetrics: boolean = props.showResourceMetrics !== false;
const [currentPage, setCurrentPage] = useState<number>(1);
const [sortBy, setSortBy] = useState<string | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(SortOrder.Ascending);
const [filterText, setFilterText] = useState<string>("");
// Filter and sort data client-side
const processedData: Array<KubernetesResource> = useMemo(() => {
let data: Array<KubernetesResource> = [...props.resources];
// Filter by search text
if (filterText.trim()) {
const search: string = filterText.toLowerCase().trim();
data = data.filter((r: KubernetesResource) => {
return (
r.name.toLowerCase().includes(search) ||
r.namespace.toLowerCase().includes(search) ||
r.status.toLowerCase().includes(search)
);
});
}
// Sort
if (sortBy) {
data.sort((a: KubernetesResource, b: KubernetesResource) => {
let cmp: number = 0;
if (sortBy === "name") {
cmp = a.name.localeCompare(b.name);
} else if (sortBy === "namespace") {
cmp = a.namespace.localeCompare(b.namespace);
} else if (sortBy === "status") {
cmp = a.status.localeCompare(b.status);
} else if (sortBy === "cpuUtilization") {
cmp = (a.cpuUtilization ?? -1) - (b.cpuUtilization ?? -1);
} else if (sortBy === "memoryUsageBytes") {
cmp = (a.memoryUsageBytes ?? -1) - (b.memoryUsageBytes ?? -1);
} else if (sortBy === "age") {
cmp = a.age.localeCompare(b.age);
}
return sortOrder === SortOrder.Descending ? -cmp : cmp;
});
}
return data;
}, [props.resources, filterText, sortBy, sortOrder]);
// Paginate
const paginatedData: Array<KubernetesResource> = useMemo(() => {
const start: number = (currentPage - 1) * PAGE_SIZE;
return processedData.slice(start, start + PAGE_SIZE);
}, [processedData, currentPage]);
const tableColumns: Array<Column<KubernetesResource>> = [
{
title: "Name",
type: FieldType.Element,
key: "name",
disableSort: true,
getElement: (resource: KubernetesResource): ReactElement => {
return (
<span className="font-medium text-gray-900">{resource.name}</span>
@@ -50,7 +167,6 @@ const KubernetesResourceTable: FunctionComponent<ComponentProps> = (
title: "Namespace",
type: FieldType.Element,
key: "namespace",
disableSort: true,
getElement: (resource: KubernetesResource): ReactElement => {
return (
<span className="inline-flex px-2 py-0.5 text-xs font-medium rounded bg-blue-50 text-blue-700">
@@ -61,6 +177,26 @@ const KubernetesResourceTable: FunctionComponent<ComponentProps> = (
});
}
if (showStatus) {
tableColumns.push({
title: "Status",
type: FieldType.Element,
key: "status",
getElement: (resource: KubernetesResource): ReactElement => {
if (!resource.status) {
return <span className="text-gray-400">-</span>;
}
return (
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${getStatusBadgeClass(resource.status)}`}
>
{resource.status}
</span>
);
},
});
}
if (props.columns) {
for (const col of props.columns) {
tableColumns.push({
@@ -78,49 +214,113 @@ const KubernetesResourceTable: FunctionComponent<ComponentProps> = (
}
}
tableColumns.push(
{
title: "CPU",
type: FieldType.Element,
key: "cpuUtilization",
disableSort: true,
getElement: (resource: KubernetesResource): ReactElement => {
return (
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
resource.cpuUtilization !== null && resource.cpuUtilization > 80
? "bg-red-50 text-red-700"
: resource.cpuUtilization !== null &&
resource.cpuUtilization > 60
? "bg-yellow-50 text-yellow-700"
: "bg-green-50 text-green-700"
}`}
>
{KubernetesResourceUtils.formatCpuValue(resource.cpuUtilization)}
</span>
);
if (showResourceMetrics) {
tableColumns.push(
{
title: "CPU",
type: FieldType.Element,
key: "cpuUtilization",
getElement: (resource: KubernetesResource): ReactElement => {
if (
resource.cpuUtilization === null ||
resource.cpuUtilization === undefined
) {
return <span className="text-gray-400">N/A</span>;
}
const pct: number = Math.min(resource.cpuUtilization, 100);
return (
<div className="flex items-center gap-2 min-w-[120px]">
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${getCpuBarColor(pct)}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-xs text-gray-600 whitespace-nowrap w-10 text-right">
{KubernetesResourceUtils.formatCpuValue(
resource.cpuUtilization,
)}
</span>
</div>
);
},
},
},
{
title: "Memory",
type: FieldType.Element,
key: "memoryUsageBytes",
disableSort: true,
getElement: (resource: KubernetesResource): ReactElement => {
return (
<span>
{KubernetesResourceUtils.formatMemoryValue(
resource.memoryUsageBytes,
)}
</span>
);
{
title: "Memory",
type: FieldType.Element,
key: "memoryUsageBytes",
getElement: (resource: KubernetesResource): ReactElement => {
if (
resource.memoryUsageBytes === null ||
resource.memoryUsageBytes === undefined
) {
return <span className="text-gray-400">N/A</span>;
}
if (
resource.memoryLimitBytes !== null &&
resource.memoryLimitBytes !== undefined &&
resource.memoryLimitBytes > 0
) {
const pct: number = Math.min(
(resource.memoryUsageBytes / resource.memoryLimitBytes) * 100,
100,
);
return (
<div className="min-w-[140px]">
<div className="flex items-center gap-2">
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${getMemoryBarColor(pct)}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-xs text-gray-600 whitespace-nowrap">
{Math.round(pct)}%
</span>
</div>
<div className="text-xs text-gray-500 mt-0.5">
{KubernetesResourceUtils.formatMemoryValue(
resource.memoryUsageBytes,
)}{" "}
/{" "}
{KubernetesResourceUtils.formatMemoryValue(
resource.memoryLimitBytes,
)}
</div>
</div>
);
}
return (
<span className="text-sm text-gray-700">
{KubernetesResourceUtils.formatMemoryValue(
resource.memoryUsageBytes,
)}
</span>
);
},
},
},
);
);
}
if (showStatus) {
tableColumns.push({
title: "Age",
type: FieldType.Element,
key: "age",
getElement: (resource: KubernetesResource): ReactElement => {
if (!resource.age) {
return <span className="text-gray-400">-</span>;
}
return <span className="text-sm text-gray-600">{resource.age}</span>;
},
});
}
if (props.getViewRoute) {
tableColumns.push({
title: "Actions",
title: "",
type: FieldType.Element,
key: "name",
disableSort: true,
@@ -139,25 +339,44 @@ const KubernetesResourceTable: FunctionComponent<ComponentProps> = (
return (
<Card title={props.title} description={props.description}>
<div className="px-4 pt-3 pb-2">
<Input
placeholder="Search by name, namespace, or status..."
onChange={(value: string) => {
setFilterText(value);
setCurrentPage(1);
}}
value={filterText}
/>
</div>
<Table<KubernetesResource>
id={`kubernetes-${props.title.toLowerCase().replace(/\s+/g, "-")}-table`}
columns={tableColumns}
data={props.resources}
data={paginatedData}
singularLabel={props.title}
pluralLabel={props.title}
isLoading={false}
isLoading={props.isLoading || false}
error=""
disablePagination={true}
currentPageNumber={1}
totalItemsCount={props.resources.length}
itemsOnPage={props.resources.length}
onNavigateToPage={() => {}}
sortBy={null}
sortOrder={SortOrder.Ascending}
onSortChanged={() => {}}
currentPageNumber={currentPage}
totalItemsCount={processedData.length}
itemsOnPage={paginatedData.length}
onNavigateToPage={(page: number) => {
setCurrentPage(page);
}}
sortBy={sortBy as keyof KubernetesResource | null}
sortOrder={sortOrder}
onSortChanged={(
newSortBy: keyof KubernetesResource | null,
newSortOrder: SortOrder,
) => {
setSortBy(newSortBy as string | null);
setSortOrder(newSortOrder);
}}
noItemsMessage={
props.emptyMessage ||
"No resources found. Resources will appear here once the kubernetes-agent is sending data."
filterText
? `No resources match "${filterText}".`
: props.emptyMessage ||
"No resources found. Resources will appear here once the kubernetes-agent is sending data."
}
/>
</Card>

View File

@@ -197,6 +197,10 @@ const MetricCharts: FunctionComponent<ComponentProps> = (
options: {
type: YAxisType.Number,
formatter: (value: number) => {
if (queryConfig.yAxisValueFormatter) {
return queryConfig.yAxisValueFormatter(value);
}
const metricType: MetricType | undefined =
props.metricTypes.find((m: MetricType) => {
return (

View File

@@ -19,6 +19,8 @@ import {
KubernetesJobObject,
KubernetesCronJobObject,
KubernetesNamespaceObject,
KubernetesPVCObject,
KubernetesPVObject,
parsePodObject,
parseNodeObject,
parseDeploymentObject,
@@ -27,6 +29,8 @@ import {
parseJobObject,
parseCronJobObject,
parseNamespaceObject,
parsePVCObject,
parsePVObject,
} from "./KubernetesObjectParser";
export type KubernetesObjectType =
@@ -37,7 +41,9 @@ export type KubernetesObjectType =
| KubernetesDaemonSetObject
| KubernetesJobObject
| KubernetesCronJobObject
| KubernetesNamespaceObject;
| KubernetesNamespaceObject
| KubernetesPVCObject
| KubernetesPVObject;
export interface FetchK8sObjectOptions {
clusterIdentifier: string;
@@ -58,6 +64,8 @@ function getParser(resourceType: string): ParserFunction | null {
jobs: parseJobObject,
cronjobs: parseCronJobObject,
namespaces: parseNamespaceObject,
persistentvolumeclaims: parsePVCObject,
persistentvolumes: parsePVObject,
};
return parsers[resourceType] || null;
}
@@ -165,6 +173,106 @@ export async function fetchLatestK8sObject<T extends KubernetesObjectType>(
}
}
/**
* Batch fetch all K8s objects of a given type for a cluster.
* Returns a Map keyed by "namespace/name" (or just "name" for cluster-scoped resources).
*/
export async function fetchK8sObjectsBatch(options: {
clusterIdentifier: string;
resourceType: string;
}): Promise<Map<string, KubernetesObjectType>> {
const parser: ParserFunction | null = getParser(options.resourceType);
if (!parser) {
return new Map();
}
const projectId: string | undefined =
ProjectUtil.getCurrentProjectId()?.toString();
if (!projectId) {
return new Map();
}
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryOptions: any = {
modelType: Log,
query: {
projectId: projectId,
time: new InBetween<Date>(startDate, endDate),
attributes: {
"logAttributes.k8s.resource.name": options.resourceType,
},
},
limit: 2000,
skip: 0,
select: {
time: true,
body: true,
attributes: true,
},
sort: {
time: SortOrder.Descending,
},
requestOptions: {},
};
const listResult: ListResult<Log> =
await AnalyticsModelAPI.getList<Log>(queryOptions);
const resultMap: Map<string, KubernetesObjectType> = new Map();
for (const log of listResult.data) {
const attrs: JSONObject = log.attributes || {};
if (
attrs["resource.k8s.cluster.name"] !== options.clusterIdentifier &&
attrs["k8s.cluster.name"] !== options.clusterIdentifier
) {
continue;
}
if (typeof log.body !== "string") {
continue;
}
const objectKvList: JSONObject | null = extractObjectFromLogBody(
log.body,
);
if (!objectKvList) {
continue;
}
const metadataKv: string | JSONObject | null = getKvValue(
objectKvList,
"metadata",
);
if (!metadataKv || typeof metadataKv === "string") {
continue;
}
const name: string = getKvStringValue(metadataKv, "name");
const namespace: string = getKvStringValue(metadataKv, "namespace");
const key: string = namespace ? `${namespace}/${name}` : name;
// Only keep the latest (first encountered since sorted desc)
if (resultMap.has(key)) {
continue;
}
const parsed: KubernetesObjectType | null = parser(objectKvList);
if (parsed) {
resultMap.set(key, parsed);
}
}
return resultMap;
} catch {
return new Map();
}
}
/**
* Fetch K8s events related to a specific resource.
*/

View File

@@ -340,6 +340,45 @@ export interface KubernetesNamespaceObject {
};
}
export interface KubernetesPVCObject {
metadata: KubernetesObjectMetadata;
spec: {
accessModes: Array<string>;
storageClassName: string;
volumeName: string;
resources: {
requests: {
storage: string;
};
};
};
status: {
phase: string; // Bound, Pending, Lost
capacity: {
storage: string;
};
};
}
export interface KubernetesPVObject {
metadata: KubernetesObjectMetadata;
spec: {
capacity: {
storage: string;
};
accessModes: Array<string>;
storageClassName: string;
persistentVolumeReclaimPolicy: string;
claimRef: {
name: string;
namespace: string;
};
};
status: {
phase: string; // Available, Bound, Released, Failed
};
}
/*
* ============================================================
* Parsers
@@ -1261,6 +1300,187 @@ export function parseNamespaceObject(
}
}
export function parsePVCObject(
objectKvList: JSONObject,
): KubernetesPVCObject | null {
try {
const metadataKv: string | JSONObject | null = getKvValue(
objectKvList,
"metadata",
);
if (!metadataKv || typeof metadataKv === "string") {
return null;
}
const metadata: KubernetesObjectMetadata = parseMetadata(metadataKv);
if (!metadata.name) {
return null;
}
const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec");
const statusKv: string | JSONObject | null = getKvValue(
objectKvList,
"status",
);
// Parse spec
let accessModes: Array<string> = [];
let storageClassName: string = "";
let volumeName: string = "";
let requestsStorage: string = "";
if (specKv && typeof specKv !== "string") {
storageClassName = getKvStringValue(specKv, "storageClassName");
volumeName = getKvStringValue(specKv, "volumeName");
const accessModesArray: string | JSONObject | null = getKvValue(
specKv,
"accessModes",
);
if (accessModesArray && typeof accessModesArray !== "string") {
const modeValues: Array<JSONObject> =
(accessModesArray["values"] as Array<JSONObject>) || [];
for (const v of modeValues) {
if (v["stringValue"]) {
accessModes.push(v["stringValue"] as string);
}
}
}
const resourcesKv: string | JSONObject | null = getKvValue(
specKv,
"resources",
);
if (resourcesKv && typeof resourcesKv !== "string") {
requestsStorage = getNestedKvValue(resourcesKv, "requests", "storage");
}
}
// Parse status
let phase: string = "";
let capacityStorage: string = "";
if (statusKv && typeof statusKv !== "string") {
phase = getKvStringValue(statusKv, "phase");
capacityStorage = getNestedKvValue(statusKv, "capacity", "storage");
}
return {
metadata,
spec: {
accessModes,
storageClassName,
volumeName,
resources: {
requests: {
storage: requestsStorage,
},
},
},
status: {
phase,
capacity: {
storage: capacityStorage,
},
},
};
} catch {
return null;
}
}
export function parsePVObject(
objectKvList: JSONObject,
): KubernetesPVObject | null {
try {
const metadataKv: string | JSONObject | null = getKvValue(
objectKvList,
"metadata",
);
if (!metadataKv || typeof metadataKv === "string") {
return null;
}
const metadata: KubernetesObjectMetadata = parseMetadata(metadataKv);
if (!metadata.name) {
return null;
}
const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec");
const statusKv: string | JSONObject | null = getKvValue(
objectKvList,
"status",
);
// Parse spec
let capacityStorage: string = "";
let accessModes: Array<string> = [];
let storageClassName: string = "";
let persistentVolumeReclaimPolicy: string = "";
let claimRefName: string = "";
let claimRefNamespace: string = "";
if (specKv && typeof specKv !== "string") {
capacityStorage = getNestedKvValue(specKv, "capacity", "storage");
storageClassName = getKvStringValue(specKv, "storageClassName");
persistentVolumeReclaimPolicy = getKvStringValue(
specKv,
"persistentVolumeReclaimPolicy",
);
const accessModesArray: string | JSONObject | null = getKvValue(
specKv,
"accessModes",
);
if (accessModesArray && typeof accessModesArray !== "string") {
const modeValues: Array<JSONObject> =
(accessModesArray["values"] as Array<JSONObject>) || [];
for (const v of modeValues) {
if (v["stringValue"]) {
accessModes.push(v["stringValue"] as string);
}
}
}
const claimRefKv: string | JSONObject | null = getKvValue(
specKv,
"claimRef",
);
if (claimRefKv && typeof claimRefKv !== "string") {
claimRefName = getKvStringValue(claimRefKv, "name");
claimRefNamespace = getKvStringValue(claimRefKv, "namespace");
}
}
// Parse status
let phase: string = "";
if (statusKv && typeof statusKv !== "string") {
phase = getKvStringValue(statusKv, "phase");
}
return {
metadata,
spec: {
capacity: {
storage: capacityStorage,
},
accessModes,
storageClassName,
persistentVolumeReclaimPolicy,
claimRef: {
name: claimRefName,
namespace: claimRefNamespace,
},
},
status: {
phase,
},
};
} catch {
return null;
}
}
/**
* Extract the K8s object from a raw OTLP log body string.
* For k8sobjects pull mode, the body is:

View File

@@ -13,6 +13,9 @@ export interface KubernetesResource {
namespace: string;
cpuUtilization: number | null;
memoryUsageBytes: number | null;
memoryLimitBytes: number | null;
status: string;
age: string;
additionalAttributes: Record<string, string>;
}
@@ -98,6 +101,9 @@ export default class KubernetesResourceUtils {
namespace: namespace,
cpuUtilization: dataPoint.value ?? null,
memoryUsageBytes: null,
memoryLimitBytes: null,
status: "",
age: "",
additionalAttributes: additionalAttrs,
});
}
@@ -184,6 +190,34 @@ export default class KubernetesResourceUtils {
return resources;
}
public static formatAge(creationTimestamp: string | undefined): string {
if (!creationTimestamp) {
return "N/A";
}
const created: Date = new Date(creationTimestamp);
const now: Date = new Date();
const diffMs: number = now.getTime() - created.getTime();
const diffSec: number = Math.floor(diffMs / 1000);
if (diffSec < 60) {
return `${diffSec}s`;
}
const diffMin: number = Math.floor(diffSec / 60);
if (diffMin < 60) {
return `${diffMin}m`;
}
const diffHours: number = Math.floor(diffMin / 60);
if (diffHours < 24) {
return `${diffHours}h`;
}
const diffDays: number = Math.floor(diffHours / 24);
if (diffDays < 30) {
return `${diffDays}d`;
}
const diffMonths: number = Math.floor(diffDays / 30);
return `${diffMonths}mo`;
}
public static formatCpuValue(value: number | null): string {
if (value === null || value === undefined) {
return "N/A";
@@ -210,4 +244,48 @@ export default class KubernetesResourceUtils {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
public static formatBytesForChart(value: number): string {
if (value === null || value === undefined) {
return "N/A";
}
const absValue: number = Math.abs(value);
if (absValue < 1024) {
return `${value.toFixed(0)} B`;
}
if (absValue < 1024 * 1024) {
return `${(value / 1024).toFixed(1)} KB`;
}
if (absValue < 1024 * 1024 * 1024) {
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
}
return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
public static formatBytesPerSecForChart(value: number): string {
if (value === null || value === undefined) {
return "N/A";
}
const absValue: number = Math.abs(value);
if (absValue < 1024) {
return `${value.toFixed(0)} B/s`;
}
if (absValue < 1024 * 1024) {
return `${(value / 1024).toFixed(1)} KB/s`;
}
if (absValue < 1024 * 1024 * 1024) {
return `${(value / (1024 * 1024)).toFixed(1)} MB/s`;
}
return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB/s`;
}
}

View File

@@ -9,7 +9,6 @@ import MetricQueryConfigData, {
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -25,6 +24,7 @@ import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterContainerDetail: FunctionComponent<
PageComponentProps
@@ -115,7 +115,7 @@ const KubernetesClusterContainerDetail: FunctionComponent<
title: "Container Memory Usage",
description: `Memory usage for container ${containerName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -132,6 +132,7 @@ const KubernetesClusterContainerDetail: FunctionComponent<
},
},
getSeries: getSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
const tabs: Array<Tab> = [
@@ -177,18 +178,7 @@ const KubernetesClusterContainerDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Container" value={containerName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterContainerDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesPodObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterContainers: FunctionComponent<
PageComponentProps
@@ -48,14 +47,58 @@ const KubernetesClusterContainers: FunctionComponent<
return;
}
const containerList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [containerList, podObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "container.cpu.utilization",
memoryMetricName: "container.memory.usage",
resourceNameAttribute: "resource.k8s.container.name",
additionalAttributes: ["resource.k8s.pod.name"],
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "pods",
}),
]);
for (const resource of containerList) {
const podName: string =
resource.additionalAttributes["resource.k8s.pod.name"] || "";
const podKey: string = resource.namespace
? `${resource.namespace}/${podName}`
: podName;
const podObj: KubernetesObjectType | undefined = podObjects.get(podKey);
if (podObj) {
const pod: KubernetesPodObject = podObj as KubernetesPodObject;
// Find the container status matching this container name
const containerStatus = pod.status.containerStatuses.find(
(cs) => cs.name === resource.name,
);
if (containerStatus) {
if (containerStatus.state === "running") {
resource.status = containerStatus.ready ? "Running" : "NotReady";
} else if (containerStatus.state === "waiting") {
resource.status = "Waiting";
} else if (containerStatus.state === "terminated") {
resource.status = "Terminated";
} else {
resource.status = containerStatus.state || "Unknown";
}
resource.additionalAttributes["restarts"] =
`${containerStatus.restartCount}`;
}
resource.age = KubernetesResourceUtils.formatAge(
pod.metadata.creationTimestamp,
);
}
}
setResources(containerList);
} catch (err) {
@@ -79,28 +122,30 @@ const KubernetesClusterContainers: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="Containers"
description="All containers running in this cluster."
resources={resources}
columns={[
<KubernetesResourceTable
title="Containers"
description="All containers running in this cluster."
resources={resources}
columns={[
{
title: "Pod",
key: "resource.k8s.pod.name",
},
{
title: "Restarts",
key: "restarts",
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL] as Route,
{
title: "Pod",
key: "resource.k8s.pod.name",
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
);
}}
/>
);
};

View File

@@ -21,6 +21,7 @@ import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterControlPlane: FunctionComponent<
PageComponentProps
@@ -78,7 +79,7 @@ const KubernetesClusterControlPlane: FunctionComponent<
title: "etcd Database Size",
description: "Total size of the etcd database",
legend: "DB Size",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -93,6 +94,7 @@ const KubernetesClusterControlPlane: FunctionComponent<
attributes: true,
},
},
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
const apiServerRequestRateQuery: MetricQueryConfigData = {

View File

@@ -3,13 +3,12 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -28,6 +27,7 @@ import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEvents
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesCronJobObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterCronJobDetail: FunctionComponent<
PageComponentProps
@@ -145,7 +145,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
title: "Pod Memory Usage",
description: `Memory usage for pods in cronjob ${cronJobName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -162,6 +162,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
},
},
getSeries: getSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
// Build overview summary fields from cronjob object
@@ -253,18 +254,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="CronJob Name" value={cronJobName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterCronJobDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesCronJobObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterCronJobs: FunctionComponent<
PageComponentProps
@@ -48,13 +47,39 @@ const KubernetesClusterCronJobs: FunctionComponent<
return;
}
const cronjobList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [cronjobList, cronjobObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.pod.cpu.utilization",
memoryMetricName: "k8s.pod.memory.usage",
resourceNameAttribute: "resource.k8s.cronjob.name",
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "cronjobs",
}),
]);
for (const resource of cronjobList) {
const key: string = `${resource.namespace}/${resource.name}`;
const cjObj: KubernetesObjectType | undefined =
cronjobObjects.get(key);
if (cjObj) {
const cronJob: KubernetesCronJobObject =
cjObj as KubernetesCronJobObject;
resource.status = cronJob.spec.suspend ? "Suspended" : "Active";
resource.additionalAttributes["schedule"] = cronJob.spec.schedule;
resource.age = KubernetesResourceUtils.formatAge(
cronJob.metadata.creationTimestamp,
);
}
}
setResources(cronjobList);
} catch (err) {
@@ -78,22 +103,26 @@ const KubernetesClusterCronJobs: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="CronJobs"
description="All cron jobs in this cluster."
resources={resources}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOB_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
<KubernetesResourceTable
title="CronJobs"
description="All cron jobs in this cluster."
resources={resources}
columns={[
{
title: "Schedule",
key: "schedule",
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOB_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
);
};

View File

@@ -3,13 +3,12 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -28,6 +27,7 @@ import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEvents
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesDaemonSetObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterDaemonSetDetail: FunctionComponent<
PageComponentProps
@@ -145,7 +145,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
title: "Pod Memory Usage",
description: `Memory usage for pods in daemonset ${daemonSetName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -162,6 +162,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
},
},
getSeries: getSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
// Build overview summary fields from daemonset object
@@ -245,18 +246,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="DaemonSet" value={daemonSetName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterDaemonSetDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesDaemonSetObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterDaemonSets: FunctionComponent<
PageComponentProps
@@ -48,13 +47,44 @@ const KubernetesClusterDaemonSets: FunctionComponent<
return;
}
const daemonsetList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [daemonsetList, daemonsetObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.pod.cpu.utilization",
memoryMetricName: "k8s.pod.memory.usage",
resourceNameAttribute: "resource.k8s.daemonset.name",
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "daemonsets",
}),
]);
for (const resource of daemonsetList) {
const key: string = `${resource.namespace}/${resource.name}`;
const dsObj: KubernetesObjectType | undefined =
daemonsetObjects.get(key);
if (dsObj) {
const ds: KubernetesDaemonSetObject =
dsObj as KubernetesDaemonSetObject;
const numberReady: number = ds.status.numberReady;
const desired: number = ds.status.desiredNumberScheduled;
resource.status =
numberReady === desired && desired > 0 ? "Ready" : "Progressing";
resource.additionalAttributes["ready"] =
`${numberReady}/${desired}`;
resource.age = KubernetesResourceUtils.formatAge(
ds.metadata.creationTimestamp,
);
}
}
setResources(daemonsetList);
} catch (err) {
@@ -78,22 +108,26 @@ const KubernetesClusterDaemonSets: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="DaemonSets"
description="All daemonsets running in this cluster."
resources={resources}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSET_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
<KubernetesResourceTable
title="DaemonSets"
description="All daemonsets running in this cluster."
resources={resources}
columns={[
{
title: "Ready",
key: "ready",
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSET_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
);
};

View File

@@ -3,13 +3,12 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -28,6 +27,7 @@ import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEvents
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesDeploymentObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterDeploymentDetail: FunctionComponent<
PageComponentProps
@@ -145,7 +145,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
title: "Pod Memory Usage",
description: `Memory usage for pods in deployment ${deploymentName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -162,6 +162,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
},
},
getSeries: getSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
// Build overview summary fields from deployment object
@@ -242,18 +243,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Deployment" value={deploymentName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterDeploymentDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesDeploymentObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterDeployments: FunctionComponent<
PageComponentProps
@@ -48,13 +47,53 @@ const KubernetesClusterDeployments: FunctionComponent<
return;
}
const deploymentList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [deploymentList, deploymentObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.pod.cpu.utilization",
memoryMetricName: "k8s.pod.memory.usage",
resourceNameAttribute: "resource.k8s.deployment.name",
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "deployments",
}),
]);
for (const resource of deploymentList) {
const key: string = `${resource.namespace}/${resource.name}`;
const depObj: KubernetesObjectType | undefined =
deploymentObjects.get(key);
if (depObj) {
const deployment: KubernetesDeploymentObject =
depObj as KubernetesDeploymentObject;
const readyReplicas: number = deployment.status.readyReplicas;
const replicas: number = deployment.spec.replicas;
if (readyReplicas === replicas && replicas > 0) {
resource.status = "Ready";
} else if (readyReplicas < replicas) {
// Check conditions for failure
const failedCondition = deployment.status.conditions.find(
(c) => c.type === "Available" && c.status === "False",
);
resource.status = failedCondition ? "Failed" : "Progressing";
} else {
resource.status = "Progressing";
}
resource.additionalAttributes["ready"] =
`${readyReplicas}/${replicas}`;
resource.age = KubernetesResourceUtils.formatAge(
deployment.metadata.creationTimestamp,
);
}
}
setResources(deploymentList);
} catch (err) {
@@ -78,24 +117,28 @@ const KubernetesClusterDeployments: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="Deployments"
description="All deployments running in this cluster."
resources={resources}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[
PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL
] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
<KubernetesResourceTable
title="Deployments"
description="All deployments running in this cluster."
resources={resources}
columns={[
{
title: "Ready",
key: "ready",
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[
PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL
] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
);
};

View File

@@ -25,6 +25,14 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import {
KubernetesPodObject,
KubernetesNodeObject,
} from "../Utils/KubernetesObjectParser";
interface ResourceLink {
title: string;
@@ -43,6 +51,19 @@ const KubernetesClusterOverview: FunctionComponent<
const [nodeCount, setNodeCount] = useState<number>(0);
const [podCount, setPodCount] = useState<number>(0);
const [namespaceCount, setNamespaceCount] = useState<number>(0);
const [podHealthSummary, setPodHealthSummary] = useState<{
running: number;
pending: number;
failed: number;
succeeded: number;
}>({ running: 0, pending: 0, failed: 0, succeeded: 0 });
const [nodeHealthSummary, setNodeHealthSummary] = useState<{
ready: number;
notReady: number;
}>({ ready: 0, notReady: 0 });
const [clusterHealth, setClusterHealth] = useState<
"Healthy" | "Degraded" | "Unhealthy"
>("Healthy");
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
@@ -89,6 +110,75 @@ const KubernetesClusterOverview: FunctionComponent<
setNodeCount(nodes.length);
setPodCount(pods.length);
setNamespaceCount(namespaces.length);
// Fetch pod and node objects for health status
try {
const [podObjects, nodeObjects]: [
Map<string, KubernetesObjectType>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
fetchK8sObjectsBatch({
clusterIdentifier: item.clusterIdentifier,
resourceType: "pods",
}),
fetchK8sObjectsBatch({
clusterIdentifier: item.clusterIdentifier,
resourceType: "nodes",
}),
]);
// Calculate pod health
let running: number = 0;
let pending: number = 0;
let failed: number = 0;
let succeeded: number = 0;
for (const podObj of podObjects.values()) {
const pod: KubernetesPodObject =
podObj as KubernetesPodObject;
const phase: string = pod.status.phase || "Unknown";
if (phase === "Running") {
running++;
} else if (phase === "Pending") {
pending++;
} else if (phase === "Failed") {
failed++;
} else if (phase === "Succeeded") {
succeeded++;
}
}
setPodHealthSummary({ running, pending, failed, succeeded });
// Calculate node health
let ready: number = 0;
let notReady: number = 0;
for (const nodeObj of nodeObjects.values()) {
const node: KubernetesNodeObject =
nodeObj as KubernetesNodeObject;
const readyCondition: boolean = node.status.conditions.some(
(c: { type: string; status: string }) =>
c.type === "Ready" && c.status === "True",
);
if (readyCondition) {
ready++;
} else {
notReady++;
}
}
setNodeHealthSummary({ ready, notReady });
// Determine overall health
if (failed > 0 || notReady > 0) {
setClusterHealth("Unhealthy");
} else if (pending > 0) {
setClusterHealth("Degraded");
} else {
setClusterHealth("Healthy");
}
} catch {
// Health data is supplementary, don't fail
}
}
} catch (err) {
setError(API.getFriendlyMessage(err));
@@ -168,17 +258,116 @@ const KubernetesClusterOverview: FunctionComponent<
description: "View all containers",
pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINERS,
},
{
title: "PVCs",
description: "View persistent volume claims",
pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_PVCS,
},
{
title: "PVs",
description: "View persistent volumes",
pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_PVS,
},
];
return (
<Fragment>
{/* Cluster Health Banner */}
<div
className={`mb-5 rounded-lg border p-4 ${
clusterHealth === "Healthy"
? "bg-green-50 border-green-200"
: clusterHealth === "Degraded"
? "bg-yellow-50 border-yellow-200"
: "bg-red-50 border-red-200"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span
className={`inline-flex h-3 w-3 rounded-full ${
clusterHealth === "Healthy"
? "bg-green-500"
: clusterHealth === "Degraded"
? "bg-yellow-500"
: "bg-red-500"
}`}
/>
<span
className={`text-lg font-semibold ${
clusterHealth === "Healthy"
? "text-green-800"
: clusterHealth === "Degraded"
? "text-yellow-800"
: "text-red-800"
}`}
>
Cluster {clusterHealth}
</span>
</div>
<div className="flex gap-4 text-sm">
<span className="text-gray-600">
<span className="font-medium text-green-700">
{podHealthSummary.running}
</span>{" "}
Running
</span>
{podHealthSummary.pending > 0 && (
<span className="text-gray-600">
<span className="font-medium text-yellow-700">
{podHealthSummary.pending}
</span>{" "}
Pending
</span>
)}
{podHealthSummary.failed > 0 && (
<span className="text-gray-600">
<span className="font-medium text-red-700">
{podHealthSummary.failed}
</span>{" "}
Failed
</span>
)}
{nodeHealthSummary.notReady > 0 && (
<span className="text-gray-600">
<span className="font-medium text-red-700">
{nodeHealthSummary.notReady}
</span>{" "}
Nodes Not Ready
</span>
)}
</div>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mb-5">
<InfoCard
title="Cluster Health"
value={
<span
className={`text-2xl font-semibold ${
clusterHealth === "Healthy"
? "text-green-600"
: clusterHealth === "Degraded"
? "text-yellow-600"
: "text-red-600"
}`}
>
{clusterHealth}
</span>
}
/>
<InfoCard
title="Nodes"
value={
<span className="text-2xl font-semibold">
{nodeCount.toString()}
{nodeHealthSummary.notReady > 0 && (
<span className="text-sm text-red-500 ml-1">
({nodeHealthSummary.notReady} not ready)
</span>
)}
</span>
}
/>
@@ -218,13 +407,17 @@ const KubernetesClusterOverview: FunctionComponent<
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 p-4">
{workloadLinks.map((link: ResourceLink) => {
return (
<a
<div
key={link.title}
href={RouteUtil.populateRouteParams(
RouteMap[link.pageMap] as Route,
{ modelId: modelId },
).toString()}
className="flex items-center p-3 rounded-lg border border-gray-200 hover:border-indigo-300 hover:bg-indigo-50 transition-colors group"
onClick={() => {
Navigation.navigate(
RouteUtil.populateRouteParams(
RouteMap[link.pageMap] as Route,
{ modelId: modelId },
),
);
}}
className="flex items-center p-3 rounded-lg border border-gray-200 hover:border-indigo-300 hover:bg-indigo-50 transition-colors group cursor-pointer"
>
<div>
<div className="font-medium text-gray-900 group-hover:text-indigo-700">
@@ -234,7 +427,7 @@ const KubernetesClusterOverview: FunctionComponent<
{link.description}
</div>
</div>
</a>
</div>
);
})}
</div>
@@ -248,13 +441,17 @@ const KubernetesClusterOverview: FunctionComponent<
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 p-4">
{infraLinks.map((link: ResourceLink) => {
return (
<a
<div
key={link.title}
href={RouteUtil.populateRouteParams(
RouteMap[link.pageMap] as Route,
{ modelId: modelId },
).toString()}
className="flex items-center p-3 rounded-lg border border-gray-200 hover:border-indigo-300 hover:bg-indigo-50 transition-colors group"
onClick={() => {
Navigation.navigate(
RouteUtil.populateRouteParams(
RouteMap[link.pageMap] as Route,
{ modelId: modelId },
),
);
}}
className="flex items-center p-3 rounded-lg border border-gray-200 hover:border-indigo-300 hover:bg-indigo-50 transition-colors group cursor-pointer"
>
<div>
<div className="font-medium text-gray-900 group-hover:text-indigo-700">
@@ -264,7 +461,7 @@ const KubernetesClusterOverview: FunctionComponent<
{link.description}
</div>
</div>
</a>
</div>
);
})}
</div>

View File

@@ -3,13 +3,12 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -28,6 +27,7 @@ import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEvents
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesJobObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterJobDetail: FunctionComponent<
PageComponentProps
@@ -144,7 +144,7 @@ const KubernetesClusterJobDetail: FunctionComponent<
title: "Pod Memory Usage",
description: `Memory usage for pods in job ${jobName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -161,6 +161,7 @@ const KubernetesClusterJobDetail: FunctionComponent<
},
},
getSeries: getSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
// Build overview summary fields from job object
@@ -257,18 +258,7 @@ const KubernetesClusterJobDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Job Name" value={jobName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterJobDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesJobObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterJobs: FunctionComponent<
PageComponentProps
@@ -48,13 +47,43 @@ const KubernetesClusterJobs: FunctionComponent<
return;
}
const jobList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [jobList, jobObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.pod.cpu.utilization",
memoryMetricName: "k8s.pod.memory.usage",
resourceNameAttribute: "resource.k8s.job.name",
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "jobs",
}),
]);
for (const resource of jobList) {
const key: string = `${resource.namespace}/${resource.name}`;
const jobObj: KubernetesObjectType | undefined = jobObjects.get(key);
if (jobObj) {
const job: KubernetesJobObject = jobObj as KubernetesJobObject;
if (job.status.completionTime) {
resource.status = "Complete";
} else if (job.status.failed > 0) {
resource.status = "Failed";
} else if (job.status.active > 0) {
resource.status = "Running";
} else {
resource.status = "Pending";
}
resource.age = KubernetesResourceUtils.formatAge(
job.metadata.creationTimestamp,
);
}
}
setResources(jobList);
} catch (err) {
@@ -78,22 +107,20 @@ const KubernetesClusterJobs: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="Jobs"
description="All jobs in this cluster."
resources={resources}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_JOB_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
<KubernetesResourceTable
title="Jobs"
description="All jobs in this cluster."
resources={resources}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_JOB_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
);
};

View File

@@ -3,13 +3,12 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -28,6 +27,7 @@ import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEvents
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesNamespaceObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterNamespaceDetail: FunctionComponent<
PageComponentProps
@@ -146,7 +146,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
title: "Pod Memory Usage",
description: `Memory usage for pods in namespace ${namespaceName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -163,6 +163,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
},
},
getSeries: getSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
// Build overview summary fields from namespace object
@@ -225,18 +226,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Namespace" value={namespaceName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterNamespaceDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesNamespaceObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterNamespaces: FunctionComponent<
PageComponentProps
@@ -48,14 +47,38 @@ const KubernetesClusterNamespaces: FunctionComponent<
return;
}
const namespaceList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [namespaceList, namespaceObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.pod.cpu.utilization",
memoryMetricName: "k8s.pod.memory.usage",
resourceNameAttribute: "resource.k8s.namespace.name",
namespaceAttribute: "resource.k8s.namespace.name",
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "namespaces",
}),
]);
for (const resource of namespaceList) {
const key: string = resource.name;
const nsObj: KubernetesObjectType | undefined =
namespaceObjects.get(key);
if (nsObj) {
const ns: KubernetesNamespaceObject =
nsObj as KubernetesNamespaceObject;
resource.status = ns.status.phase || "Unknown";
resource.age = KubernetesResourceUtils.formatAge(
ns.metadata.creationTimestamp,
);
}
}
setResources(namespaceList);
} catch (err) {
@@ -79,23 +102,21 @@ const KubernetesClusterNamespaces: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="Namespaces"
description="All namespaces in this cluster."
resources={resources}
showNamespace={false}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACE_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
<KubernetesResourceTable
title="Namespaces"
description="All namespaces in this cluster."
resources={resources}
showNamespace={false}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACE_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
);
};

View File

@@ -3,11 +3,10 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -28,6 +27,7 @@ import {
KubernetesNodeObject,
} from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterNodeDetail: FunctionComponent<
PageComponentProps
@@ -135,7 +135,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
title: "Memory Usage",
description: `Memory usage for node ${nodeName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -151,6 +151,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
attributes: true,
},
},
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
const filesystemQuery: MetricQueryConfigData = {
@@ -159,7 +160,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
title: "Filesystem Usage",
description: `Filesystem usage for node ${nodeName}`,
legend: "Filesystem",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -175,6 +176,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
attributes: true,
},
},
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
const networkRxQuery: MetricQueryConfigData = {
@@ -183,7 +185,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
title: "Network Receive",
description: `Network bytes received for node ${nodeName}`,
legend: "Network RX",
legendUnit: "bytes/s",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -199,6 +201,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
attributes: true,
},
},
yAxisValueFormatter: KubernetesResourceUtils.formatBytesPerSecForChart,
};
const networkTxQuery: MetricQueryConfigData = {
@@ -207,7 +210,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
title: "Network Transmit",
description: `Network bytes transmitted for node ${nodeName}`,
legend: "Network TX",
legendUnit: "bytes/s",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -223,6 +226,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
attributes: true,
},
},
yAxisValueFormatter: KubernetesResourceUtils.formatBytesPerSecForChart,
};
// Determine node status from conditions
@@ -352,18 +356,7 @@ const KubernetesClusterNodeDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Node Name" value={nodeName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterNodeDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesNodeObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterNodes: FunctionComponent<
PageComponentProps
@@ -48,14 +47,43 @@ const KubernetesClusterNodes: FunctionComponent<
return;
}
const nodeList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [nodeList, nodeObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.node.cpu.utilization",
memoryMetricName: "k8s.node.memory.usage",
resourceNameAttribute: "resource.k8s.node.name",
namespaceAttribute: "resource.k8s.node.name",
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "nodes",
}),
]);
for (const resource of nodeList) {
const key: string = resource.name;
const nodeObj: KubernetesObjectType | undefined = nodeObjects.get(key);
if (nodeObj) {
const node: KubernetesNodeObject = nodeObj as KubernetesNodeObject;
// Check conditions for Ready status
const readyCondition = node.status.conditions.find(
(c) => c.type === "Ready",
);
resource.status =
readyCondition && readyCondition.status === "True"
? "Ready"
: "NotReady";
resource.age = KubernetesResourceUtils.formatAge(
node.metadata.creationTimestamp,
);
}
}
setResources(nodeList);
} catch (err) {
@@ -79,23 +107,21 @@ const KubernetesClusterNodes: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="Nodes"
description="All nodes in this cluster with their current resource usage."
resources={resources}
showNamespace={false}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
<KubernetesResourceTable
title="Nodes"
description="All nodes in this cluster with their current resource usage."
resources={resources}
showNamespace={false}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
);
};

View File

@@ -0,0 +1,187 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import { KubernetesPVCObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterPVCDetail: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(2);
const pvcName: string = Navigation.getLastParamAsString();
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [pvcObject, setPvcObject] = useState<KubernetesPVCObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchPvcObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesPVCObject | null =
await fetchLatestK8sObject<KubernetesPVCObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "persistentvolumeclaims",
resourceName: pvcName,
});
setPvcObject(obj);
} catch {
// Graceful degradation
}
setIsLoadingObject(false);
};
fetchPvcObject().catch(() => {});
}, [cluster?.clusterIdentifier, pvcName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster) {
return <ErrorMessage message="Cluster not found." />;
}
const clusterIdentifier: string = cluster.clusterIdentifier || "";
const summaryFields: Array<{
title: string;
value: string | ReactElement;
}> = [
{ title: "PVC Name", value: pvcName },
{ title: "Cluster", value: clusterIdentifier },
];
if (pvcObject) {
summaryFields.push(
{
title: "Namespace",
value: pvcObject.metadata.namespace || "default",
},
{
title: "Status",
value: (
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
pvcObject.status.phase === "Bound"
? "bg-green-50 text-green-700"
: pvcObject.status.phase === "Pending"
? "bg-yellow-50 text-yellow-700"
: "bg-red-50 text-red-700"
}`}
>
{pvcObject.status.phase || "Unknown"}
</span>
),
},
{
title: "Storage Class",
value: pvcObject.spec.storageClassName || "N/A",
},
{
title: "Capacity",
value: pvcObject.status.capacity.storage || "N/A",
},
{
title: "Requested Storage",
value: pvcObject.spec.resources.requests.storage || "N/A",
},
{
title: "Volume Name",
value: pvcObject.spec.volumeName || "N/A",
},
{
title: "Access Modes",
value: pvcObject.spec.accessModes.join(", ") || "N/A",
},
{
title: "Created",
value: pvcObject.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={pvcObject?.metadata.labels || {}}
annotations={pvcObject?.metadata.annotations || {}}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card
title="PVC Events"
description="Kubernetes events for this PVC in the last 24 hours."
>
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="PersistentVolumeClaim"
resourceName={pvcName}
namespace={pvcObject?.metadata.namespace}
/>
</Card>
),
},
];
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterPVCDetail;

View File

@@ -0,0 +1,185 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import Tabs from "Common/UI/Components/Tabs/Tabs";
import { Tab } from "Common/UI/Components/Tabs/Tab";
import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab";
import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab";
import { KubernetesPVObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
const KubernetesClusterPVDetail: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(2);
const pvName: string = Navigation.getLastParamAsString();
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [pvObject, setPvObject] = useState<KubernetesPVObject | null>(null);
const [isLoadingObject, setIsLoadingObject] = useState<boolean>(true);
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const item: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
setCluster(item);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchCluster().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
useEffect(() => {
if (!cluster?.clusterIdentifier) {
return;
}
const fetchPvObject: () => Promise<void> = async (): Promise<void> => {
setIsLoadingObject(true);
try {
const obj: KubernetesPVObject | null =
await fetchLatestK8sObject<KubernetesPVObject>({
clusterIdentifier: cluster.clusterIdentifier || "",
resourceType: "persistentvolumes",
resourceName: pvName,
});
setPvObject(obj);
} catch {
// Graceful degradation
}
setIsLoadingObject(false);
};
fetchPvObject().catch(() => {});
}, [cluster?.clusterIdentifier, pvName]);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!cluster) {
return <ErrorMessage message="Cluster not found." />;
}
const clusterIdentifier: string = cluster.clusterIdentifier || "";
const summaryFields: Array<{
title: string;
value: string | ReactElement;
}> = [
{ title: "PV Name", value: pvName },
{ title: "Cluster", value: clusterIdentifier },
];
if (pvObject) {
summaryFields.push(
{
title: "Status",
value: (
<span
className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
pvObject.status.phase === "Bound" ||
pvObject.status.phase === "Available"
? "bg-green-50 text-green-700"
: pvObject.status.phase === "Released"
? "bg-yellow-50 text-yellow-700"
: "bg-red-50 text-red-700"
}`}
>
{pvObject.status.phase || "Unknown"}
</span>
),
},
{
title: "Capacity",
value: pvObject.spec.capacity.storage || "N/A",
},
{
title: "Storage Class",
value: pvObject.spec.storageClassName || "N/A",
},
{
title: "Reclaim Policy",
value: pvObject.spec.persistentVolumeReclaimPolicy || "N/A",
},
{
title: "Access Modes",
value: pvObject.spec.accessModes.join(", ") || "N/A",
},
{
title: "Claim",
value: pvObject.spec.claimRef.name
? `${pvObject.spec.claimRef.namespace}/${pvObject.spec.claimRef.name}`
: "N/A",
},
{
title: "Created",
value: pvObject.metadata.creationTimestamp || "N/A",
},
);
}
const tabs: Array<Tab> = [
{
name: "Overview",
children: (
<KubernetesOverviewTab
summaryFields={summaryFields}
labels={pvObject?.metadata.labels || {}}
annotations={pvObject?.metadata.annotations || {}}
isLoading={isLoadingObject}
/>
),
},
{
name: "Events",
children: (
<Card
title="PV Events"
description="Kubernetes events for this PV in the last 24 hours."
>
<KubernetesEventsTab
clusterIdentifier={clusterIdentifier}
resourceKind="PersistentVolume"
resourceName={pvName}
/>
</Card>
),
},
];
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterPVDetail;

View File

@@ -0,0 +1,143 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable";
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesPVCObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterPVCs: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [resources, setResources] = useState<Array<KubernetesResource>>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchData: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const cluster: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
if (!cluster?.clusterIdentifier) {
setError("Cluster not found.");
setIsLoading(false);
return;
}
const pvcObjects: Map<string, KubernetesObjectType> =
await fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "persistentvolumeclaims",
});
const pvcResources: Array<KubernetesResource> = [];
for (const pvcObj of pvcObjects.values()) {
const pvc: KubernetesPVCObject = pvcObj as KubernetesPVCObject;
pvcResources.push({
name: pvc.metadata.name,
namespace: pvc.metadata.namespace || "default",
cpuUtilization: null,
memoryUsageBytes: null,
memoryLimitBytes: null,
status: pvc.status.phase || "Unknown",
age: KubernetesResourceUtils.formatAge(
pvc.metadata.creationTimestamp,
),
additionalAttributes: {
storageClass: pvc.spec.storageClassName || "N/A",
capacity: pvc.status.capacity.storage || "N/A",
volumeName: pvc.spec.volumeName || "N/A",
accessModes: pvc.spec.accessModes.join(", ") || "N/A",
},
});
}
setResources(pvcResources);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchData().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
return (
<KubernetesResourceTable
title="Persistent Volume Claims"
description="All PVCs in this cluster with their current status."
resources={resources}
showResourceMetrics={false}
columns={[
{
title: "Storage Class",
key: "storageClass",
},
{
title: "Capacity",
key: "capacity",
},
{
title: "Volume",
key: "volumeName",
},
{
title: "Access Modes",
key: "accessModes",
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_PVC_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
emptyMessage="No PVCs found. PVC data will appear here once the kubernetes-agent Helm chart has resourceSpecs.enabled set to true and includes persistentvolumeclaims."
/>
);
};
export default KubernetesClusterPVCs;

View File

@@ -0,0 +1,134 @@
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable";
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesPVObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterPVs: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [resources, setResources] = useState<Array<KubernetesResource>>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchData: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const cluster: KubernetesCluster | null = await ModelAPI.getItem({
modelType: KubernetesCluster,
id: modelId,
select: {
clusterIdentifier: true,
},
});
if (!cluster?.clusterIdentifier) {
setError("Cluster not found.");
setIsLoading(false);
return;
}
const pvObjects: Map<string, KubernetesObjectType> =
await fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "persistentvolumes",
});
const pvResources: Array<KubernetesResource> = [];
for (const pvObj of pvObjects.values()) {
const pv: KubernetesPVObject = pvObj as KubernetesPVObject;
pvResources.push({
name: pv.metadata.name,
namespace: "",
cpuUtilization: null,
memoryUsageBytes: null,
memoryLimitBytes: null,
status: pv.status.phase || "Unknown",
age: KubernetesResourceUtils.formatAge(
pv.metadata.creationTimestamp,
),
additionalAttributes: {
capacity: pv.spec.capacity.storage || "N/A",
storageClass: pv.spec.storageClassName || "N/A",
reclaimPolicy: pv.spec.persistentVolumeReclaimPolicy || "N/A",
claimRef: pv.spec.claimRef.name
? `${pv.spec.claimRef.namespace}/${pv.spec.claimRef.name}`
: "N/A",
},
});
}
setResources(pvResources);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchData().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
return (
<KubernetesResourceTable
title="Persistent Volumes"
description="All PVs in this cluster with their current status."
resources={resources}
showNamespace={false}
showResourceMetrics={false}
columns={[
{
title: "Capacity",
key: "capacity",
},
{
title: "Storage Class",
key: "storageClass",
},
{
title: "Reclaim Policy",
key: "reclaimPolicy",
},
{
title: "Claim",
key: "claimRef",
},
]}
emptyMessage="No PVs found. PV data will appear here once the kubernetes-agent Helm chart has resourceSpecs.enabled set to true and includes persistentvolumes."
/>
);
};
export default KubernetesClusterPVs;

View File

@@ -3,13 +3,11 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -30,6 +28,7 @@ import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab"
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesPodObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterPodDetail: FunctionComponent<
PageComponentProps
@@ -147,7 +146,7 @@ const KubernetesClusterPodDetail: FunctionComponent<
title: "Container Memory Usage",
description: `Memory usage for containers in pod ${podName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -164,6 +163,7 @@ const KubernetesClusterPodDetail: FunctionComponent<
},
},
getSeries: getContainerSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
const podCpuQuery: MetricQueryConfigData = {
@@ -196,7 +196,7 @@ const KubernetesClusterPodDetail: FunctionComponent<
title: "Pod Memory Usage",
description: `Memory usage for pod ${podName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -212,6 +212,7 @@ const KubernetesClusterPodDetail: FunctionComponent<
attributes: true,
},
},
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
// Build overview summary fields from pod object
@@ -310,16 +311,11 @@ const KubernetesClusterPodDetail: FunctionComponent<
{
name: "Logs",
children: (
<Card
title="Application Logs"
description="Container logs for this pod from the last 6 hours."
>
<KubernetesLogsTab
clusterIdentifier={clusterIdentifier}
podName={podName}
namespace={podObject?.metadata.namespace}
/>
</Card>
<KubernetesLogsTab
clusterIdentifier={clusterIdentifier}
podName={podName}
namespace={podObject?.metadata.namespace}
/>
),
},
{
@@ -337,18 +333,7 @@ const KubernetesClusterPodDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="Pod Name" value={podName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterPodDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,37 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesPodObject } from "../Utils/KubernetesObjectParser";
function parseMemoryString(memory: string): number {
if (!memory) {
return 0;
}
const value: number = parseFloat(memory);
if (memory.endsWith("Gi")) {
return value * 1024 * 1024 * 1024;
}
if (memory.endsWith("Mi")) {
return value * 1024 * 1024;
}
if (memory.endsWith("Ki")) {
return value * 1024;
}
if (memory.endsWith("G")) {
return value * 1000 * 1000 * 1000;
}
if (memory.endsWith("M")) {
return value * 1000 * 1000;
}
if (memory.endsWith("K")) {
return value * 1000;
}
return value;
}
const KubernetesClusterPods: FunctionComponent<
PageComponentProps
@@ -48,8 +73,11 @@ const KubernetesClusterPods: FunctionComponent<
return;
}
const podList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [podList, podObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.pod.cpu.utilization",
memoryMetricName: "k8s.pod.memory.usage",
@@ -58,7 +86,46 @@ const KubernetesClusterPods: FunctionComponent<
"resource.k8s.node.name",
"resource.k8s.deployment.name",
],
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "pods",
}),
]);
for (const resource of podList) {
const key: string = `${resource.namespace}/${resource.name}`;
const podObj: KubernetesObjectType | undefined = podObjects.get(key);
if (podObj) {
const pod: KubernetesPodObject = podObj as KubernetesPodObject;
resource.status = pod.status.phase || "Unknown";
for (const cs of pod.status.containerStatuses) {
if (cs.state === "waiting" && cs.reason) {
resource.status = cs.reason;
break;
}
}
resource.age = KubernetesResourceUtils.formatAge(
pod.metadata.creationTimestamp,
);
resource.additionalAttributes["containers"] =
`${pod.spec.containers.length}`;
let totalMemoryLimit: number = 0;
for (const container of pod.spec.containers) {
if (container.resources.limits.memory) {
totalMemoryLimit += parseMemoryString(
container.resources.limits.memory,
);
}
}
if (totalMemoryLimit > 0) {
resource.memoryLimitBytes = totalMemoryLimit;
}
}
}
setResources(podList);
} catch (err) {
@@ -82,28 +149,30 @@ const KubernetesClusterPods: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="Pods"
description="All pods running in this cluster with their current resource usage."
resources={resources}
columns={[
<KubernetesResourceTable
title="Pods"
description="All pods running in this cluster with their current resource usage."
resources={resources}
columns={[
{
title: "Node",
key: "resource.k8s.node.name",
},
{
title: "Containers",
key: "containers",
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL] as Route,
{
title: "Node",
key: "resource.k8s.node.name",
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
);
}}
/>
);
};

View File

@@ -134,6 +134,30 @@ const KubernetesClusterSideMenu: FunctionComponent<ComponentProps> = (
}}
icon={IconProp.Cube}
/>
<SideMenuItem
link={{
title: "PVCs",
to: RouteUtil.populateRouteParams(
RouteMap[
PageMap.KUBERNETES_CLUSTER_VIEW_PVCS
] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Disc}
/>
<SideMenuItem
link={{
title: "PVs",
to: RouteUtil.populateRouteParams(
RouteMap[
PageMap.KUBERNETES_CLUSTER_VIEW_PVS
] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Disc}
/>
</SideMenuSection>
<SideMenuSection title="Observability">

View File

@@ -3,13 +3,12 @@ import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
import Card from "Common/UI/Components/Card/Card";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import MetricQueryConfigData, {
ChartSeries,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
@@ -28,6 +27,7 @@ import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEvents
import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab";
import { KubernetesStatefulSetObject } from "../Utils/KubernetesObjectParser";
import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher";
import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils";
const KubernetesClusterStatefulSetDetail: FunctionComponent<
PageComponentProps
@@ -145,7 +145,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
title: "Pod Memory Usage",
description: `Memory usage for pods in statefulset ${statefulSetName}`,
legend: "Memory",
legendUnit: "bytes",
legendUnit: "",
},
metricQueryData: {
filterData: {
@@ -162,6 +162,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
},
},
getSeries: getSeries,
yAxisValueFormatter: KubernetesResourceUtils.formatBytesForChart,
};
// Build overview summary fields from statefulset object
@@ -245,18 +246,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
},
];
return (
<Fragment>
<div className="mb-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
<InfoCard title="StatefulSet" value={statefulSetName || "Unknown"} />
<InfoCard title="Cluster" value={clusterIdentifier} />
</div>
</div>
<Tabs tabs={tabs} onTabChange={() => {}} />
</Fragment>
);
return <Tabs tabs={tabs} onTabChange={() => {}} />;
};
export default KubernetesClusterStatefulSetDetail;

View File

@@ -6,13 +6,7 @@ import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesRe
import KubernetesResourceUtils, {
KubernetesResource,
} from "../Utils/KubernetesResourceUtils";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
@@ -21,6 +15,11 @@ import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import {
fetchK8sObjectsBatch,
KubernetesObjectType,
} from "../Utils/KubernetesObjectFetcher";
import { KubernetesStatefulSetObject } from "../Utils/KubernetesObjectParser";
const KubernetesClusterStatefulSets: FunctionComponent<
PageComponentProps
@@ -48,13 +47,46 @@ const KubernetesClusterStatefulSets: FunctionComponent<
return;
}
const statefulsetList: Array<KubernetesResource> =
await KubernetesResourceUtils.fetchResourceListWithMemory({
const [statefulsetList, statefulsetObjects]: [
Array<KubernetesResource>,
Map<string, KubernetesObjectType>,
] = await Promise.all([
KubernetesResourceUtils.fetchResourceListWithMemory({
clusterIdentifier: cluster.clusterIdentifier,
metricName: "k8s.pod.cpu.utilization",
memoryMetricName: "k8s.pod.memory.usage",
resourceNameAttribute: "resource.k8s.statefulset.name",
});
}),
fetchK8sObjectsBatch({
clusterIdentifier: cluster.clusterIdentifier,
resourceType: "statefulsets",
}),
]);
for (const resource of statefulsetList) {
const key: string = `${resource.namespace}/${resource.name}`;
const stsObj: KubernetesObjectType | undefined =
statefulsetObjects.get(key);
if (stsObj) {
const sts: KubernetesStatefulSetObject =
stsObj as KubernetesStatefulSetObject;
const readyReplicas: number = sts.status.readyReplicas;
const replicas: number = sts.spec.replicas;
resource.status =
readyReplicas === replicas && replicas > 0
? "Ready"
: "Progressing";
resource.additionalAttributes["ready"] =
`${readyReplicas}/${replicas}`;
resource.age = KubernetesResourceUtils.formatAge(
sts.metadata.creationTimestamp,
);
}
}
setResources(statefulsetList);
} catch (err) {
@@ -78,24 +110,28 @@ const KubernetesClusterStatefulSets: FunctionComponent<
}
return (
<Fragment>
<KubernetesResourceTable
title="StatefulSets"
description="All statefulsets running in this cluster."
resources={resources}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[
PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL
] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
</Fragment>
<KubernetesResourceTable
title="StatefulSets"
description="All statefulsets running in this cluster."
resources={resources}
columns={[
{
title: "Ready",
key: "ready",
},
]}
getViewRoute={(resource: KubernetesResource) => {
return RouteUtil.populateRouteParams(
RouteMap[
PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL
] as Route,
{
modelId: modelId,
subModelId: new ObjectID(resource.name),
},
);
}}
/>
);
};

View File

@@ -238,6 +238,10 @@ enum PageMap {
KUBERNETES_CLUSTER_VIEW_NODE_DETAIL = "KUBERNETES_CLUSTER_VIEW_NODE_DETAIL",
KUBERNETES_CLUSTER_VIEW_CONTAINERS = "KUBERNETES_CLUSTER_VIEW_CONTAINERS",
KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL = "KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL",
KUBERNETES_CLUSTER_VIEW_PVCS = "KUBERNETES_CLUSTER_VIEW_PVCS",
KUBERNETES_CLUSTER_VIEW_PVC_DETAIL = "KUBERNETES_CLUSTER_VIEW_PVC_DETAIL",
KUBERNETES_CLUSTER_VIEW_PVS = "KUBERNETES_CLUSTER_VIEW_PVS",
KUBERNETES_CLUSTER_VIEW_PV_DETAIL = "KUBERNETES_CLUSTER_VIEW_PV_DETAIL",
KUBERNETES_CLUSTER_VIEW_EVENTS = "KUBERNETES_CLUSTER_VIEW_EVENTS",
KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE = "KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE",
KUBERNETES_CLUSTER_VIEW_DELETE = "KUBERNETES_CLUSTER_VIEW_DELETE",

View File

@@ -79,6 +79,10 @@ export const KubernetesRoutePath: Dictionary<string> = {
[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: `${RouteParams.ModelID}/nodes/${RouteParams.SubModelID}`,
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINERS]: `${RouteParams.ModelID}/containers`,
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL]: `${RouteParams.ModelID}/containers/${RouteParams.SubModelID}`,
[PageMap.KUBERNETES_CLUSTER_VIEW_PVCS]: `${RouteParams.ModelID}/pvcs`,
[PageMap.KUBERNETES_CLUSTER_VIEW_PVC_DETAIL]: `${RouteParams.ModelID}/pvcs/${RouteParams.SubModelID}`,
[PageMap.KUBERNETES_CLUSTER_VIEW_PVS]: `${RouteParams.ModelID}/pvs`,
[PageMap.KUBERNETES_CLUSTER_VIEW_PV_DETAIL]: `${RouteParams.ModelID}/pvs/${RouteParams.SubModelID}`,
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: `${RouteParams.ModelID}/events`,
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: `${RouteParams.ModelID}/control-plane`,
[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
@@ -1621,6 +1625,30 @@ const RouteMap: Dictionary<Route> = {
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_PVCS]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PVCS]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_PVC_DETAIL]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PVC_DETAIL]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_PVS]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PVS]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_PV_DETAIL]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PV_DETAIL]
}`,
),
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: new Route(
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]

View File

@@ -16,4 +16,5 @@ export default interface MetricQueryConfigData {
metricQueryData: MetricQueryData;
getSeries?: ((data: AggregatedModel) => ChartSeries) | undefined;
chartType?: MetricChartType | undefined;
yAxisValueFormatter?: ((value: number) => string) | undefined;
}

View File

@@ -63,6 +63,12 @@ data:
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
group: batch
- name: persistentvolumeclaims
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
- name: persistentvolumes
mode: pull
interval: {{ .Values.resourceSpecs.interval }}
{{- end }}
{{- if .Values.controlPlane.enabled }}