mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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'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."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
187
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVCDetail.tsx
Normal file
187
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVCDetail.tsx
Normal 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;
|
||||
185
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVDetail.tsx
Normal file
185
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVDetail.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -16,4 +16,5 @@ export default interface MetricQueryConfigData {
|
||||
metricQueryData: MetricQueryData;
|
||||
getSeries?: ((data: AggregatedModel) => ChartSeries) | undefined;
|
||||
chartType?: MetricChartType | undefined;
|
||||
yAxisValueFormatter?: ((value: number) => string) | undefined;
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user