From d7c94b20a2bfd40cf3b2b655e058ff9e4d770835 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubenko Date: Tue, 10 Dec 2024 16:32:04 +0100 Subject: [PATCH 1/4] frontend: Add support for refetching lists on interval Signed-off-by: Oleksandr Dubenko --- frontend/src/lib/k8s/KubeObject.ts | 4 ++++ frontend/src/lib/k8s/api/v2/useKubeObjectList.ts | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts index 4d457e2e7d..b415d09e22 100644 --- a/frontend/src/lib/k8s/KubeObject.ts +++ b/frontend/src/lib/k8s/KubeObject.ts @@ -300,11 +300,14 @@ export class KubeObject { cluster, clusters, namespace, + refetchInterval, ...queryParams }: { cluster?: string; clusters?: string[]; namespace?: string | string[]; + /** How often to refetch the list. Won't refetch by default. Disables watching if set. */ + refetchInterval?: number; } & QueryParameters = {} ) { const fallbackClusters = useClusterGroup(); @@ -334,6 +337,7 @@ export class KubeObject { queryParams: queryParams, kubeObjectClass: this, requests, + refetchInterval, }); return result; diff --git a/frontend/src/lib/k8s/api/v2/useKubeObjectList.ts b/frontend/src/lib/k8s/api/v2/useKubeObjectList.ts index 4e11f8033f..b8a314a504 100644 --- a/frontend/src/lib/k8s/api/v2/useKubeObjectList.ts +++ b/frontend/src/lib/k8s/api/v2/useKubeObjectList.ts @@ -48,10 +48,12 @@ export function kubeObjectListQuery( endpoint: KubeObjectEndpoint, namespace: string | undefined, cluster: string, - queryParams: QueryParameters + queryParams: QueryParameters, + refetchInterval?: number ): QueryObserverOptions | undefined | null, ListError> { return { placeholderData: null, + refetchInterval, queryKey: [ 'kubeObject', 'list', @@ -192,6 +194,7 @@ export function useKubeObjectList({ kubeObjectClass, queryParams, watch = true, + refetchInterval, }: { requests: Array<{ cluster: string; namespaces?: string[] }>; /** Class to instantiate the object with */ @@ -199,6 +202,8 @@ export function useKubeObjectList({ queryParams?: QueryParameters; /** Watch for updates @default true */ watch?: boolean; + /** How often to refetch the list. Won't refetch by default. Disables watching if set. */ + refetchInterval?: number; }): [Array | null, ApiError | null] & QueryListResponse | undefined | null>, K, ApiError> { const maybeNamespace = requests.find(it => it.namespaces)?.namespaces?.[0]; @@ -226,7 +231,8 @@ export function useKubeObjectList({ endpoint, namespace, cluster, - cleanedUpQueryParams + cleanedUpQueryParams, + refetchInterval ) ) : kubeObjectListQuery( @@ -234,7 +240,8 @@ export function useKubeObjectList({ endpoint, undefined, cluster, - cleanedUpQueryParams + cleanedUpQueryParams, + refetchInterval ) ) : [], @@ -281,7 +288,7 @@ export function useKubeObjectList({ }, }); - const shouldWatch = watch && !query.isLoading; + const shouldWatch = watch && !refetchInterval && !query.isLoading; const [listsToWatch, setListsToWatch] = useState< { cluster: string; namespace?: string; resourceVersion: string }[] From 8ef501111fee1fd916f670a102795acacb8a2335 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubenko Date: Tue, 10 Dec 2024 16:32:23 +0100 Subject: [PATCH 2/4] frontend: Add PodMetrics class Signed-off-by: Oleksandr Dubenko --- frontend/src/lib/k8s/PodMetrics.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frontend/src/lib/k8s/PodMetrics.ts diff --git a/frontend/src/lib/k8s/PodMetrics.ts b/frontend/src/lib/k8s/PodMetrics.ts new file mode 100644 index 0000000000..e27608e695 --- /dev/null +++ b/frontend/src/lib/k8s/PodMetrics.ts @@ -0,0 +1,22 @@ +import { KubeObject, KubeObjectInterface } from './KubeObject'; + +interface KubePodMetrics extends KubeObjectInterface { + timestamp: string; + window: string; + containers: Array<{ + name: string; + usage: { + cpu: string; + memory: string; + }; + }>; +} + +export const METRIC_REFETCH_INTERVAL_MS = 5_000; + +export class PodMetrics extends KubeObject { + static kind = 'PodMetric'; + static apiName = 'pods'; + static apiVersion = 'metrics.k8s.io/v1beta1'; + static isNamespaced = true; +} From 87b3df4f92a1418ce6ddc6d97d2cdbc4fb1d941b Mon Sep 17 00:00:00 2001 From: Oleksandr Dubenko Date: Tue, 10 Dec 2024 16:32:32 +0100 Subject: [PATCH 3/4] frontend: Add PodMetrics to the Pod list Signed-off-by: Oleksandr Dubenko --- frontend/src/components/pod/List.tsx | 51 +++++- .../src/components/pod/PodList.stories.tsx | 23 +++ .../PodList.Items.stories.storyshot | 164 +++++++++++++++++- 3 files changed, 236 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/pod/List.tsx b/frontend/src/components/pod/List.tsx index f4887f3b8a..11aba09604 100644 --- a/frontend/src/components/pod/List.tsx +++ b/frontend/src/components/pod/List.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { ApiError } from '../../lib/k8s/apiProxy'; import Pod from '../../lib/k8s/pod'; +import { METRIC_REFETCH_INTERVAL_MS, PodMetrics } from '../../lib/k8s/PodMetrics'; +import { parseCpu, parseRam, unparseCpu, unparseRam } from '../../lib/units'; import { timeAgo } from '../../lib/util'; import { useNamespaces } from '../../redux/filterSlice'; import { HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; @@ -62,6 +64,7 @@ function getReadinessGatesStatus(pods: Pod) { export interface PodListProps { pods: Pod[] | null; error: ApiError | null; + metrics: PodMetrics[] | null; hideColumns?: ('namespace' | 'restarts')[]; reflectTableInURL?: SimpleTableProps['reflectInURL']; noNamespaceFilter?: boolean; @@ -72,6 +75,7 @@ export function PodListRenderer(props: PodListProps) { const { pods, error, + metrics, hideColumns = [], reflectTableInURL = 'pods', noNamespaceFilter, @@ -117,6 +121,41 @@ export function PodListRenderer(props: PodListProps) { getValue: pod => pod.getDetailedStatus().reason, render: makePodStatusLabel, }, + { + id: 'cpu', + label: t('CPU'), + gridTemplate: 'min-content', + getValue: pod => { + const metric = metrics?.find(it => it.getName() === pod.getName()); + if (!metric) return; + + const cpuUsage = + metric?.jsonData.containers + .map(it => parseCpu(it.usage.cpu)) + .reduce((a, b) => a + b, 0) ?? 0; + + const { value, unit } = unparseCpu(String(cpuUsage)); + + return `${value} ${unit}`; + }, + }, + { + id: 'memory', + label: t('Memory'), + getValue: pod => { + const metric = metrics?.find(it => it.getName() === pod.getName()); + if (!metric) return; + + const memoryUsage = + metric?.jsonData.containers + .map(it => parseRam(it.usage.memory)) + .reduce((a, b) => a + b, 0) ?? 0; + + const { value, unit } = unparseRam(memoryUsage); + + return `${value} ${unit}`; + }, + }, { id: 'ip', label: t('glossary|IP'), @@ -218,6 +257,10 @@ export function PodListRenderer(props: PodListProps) { export default function PodList() { const { items, error, clusterErrors } = Pod.useList({ namespace: useNamespaces() }); + const { items: podMetrics, clusterErrors: metricsClusterErrors } = PodMetrics.useList({ + namespace: useNamespaces(), + refetchInterval: METRIC_REFETCH_INTERVAL_MS, + }); const dispatchHeadlampEvent = useEventCallback(HeadlampEventType.LIST_VIEW); @@ -230,6 +273,12 @@ export default function PodList() { }, [items, error]); return ( - + ); } diff --git a/frontend/src/components/pod/PodList.stories.tsx b/frontend/src/components/pod/PodList.stories.tsx index 9ae17a2b53..d957f6e1ae 100644 --- a/frontend/src/components/pod/PodList.stories.tsx +++ b/frontend/src/components/pod/PodList.stories.tsx @@ -29,6 +29,29 @@ export default { items: podList, }) ), + http.get('http://localhost:4466/apis/metrics.k8s.io/v1beta1/pods', () => + HttpResponse.json({ + kind: 'PodMetricsList', + apiVersion: 'metrics.k8s.io/v1beta1', + metadata: {}, + items: [ + { + metadata: { + name: 'successful', + }, + containers: [ + { + name: 'etcd', + usage: { + cpu: '16317640n', + memory: '47544Ki', + }, + }, + ], + }, + ], + }) + ), ], }, }, diff --git a/frontend/src/components/pod/__snapshots__/PodList.Items.stories.storyshot b/frontend/src/components/pod/__snapshots__/PodList.Items.stories.storyshot index fb9318ec82..620a31a0da 100644 --- a/frontend/src/components/pod/__snapshots__/PodList.Items.stories.storyshot +++ b/frontend/src/components/pod/__snapshots__/PodList.Items.stories.storyshot @@ -240,7 +240,7 @@ + + + + + + + + + +
+
+ +
+
+
+
+ +
+
+
+ + @@ -876,6 +992,16 @@ + 16.32 m + + 46.4 Mi + @@ -983,6 +1109,12 @@ + @@ -1092,6 +1224,12 @@ + @@ -1201,6 +1339,12 @@ + @@ -1310,6 +1454,12 @@ + @@ -1419,6 +1569,12 @@ + @@ -1528,6 +1684,12 @@ + From 72f6e1ae34325623daf1b53c743a736cb09452c6 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubenko Date: Tue, 10 Dec 2024 16:38:55 +0100 Subject: [PATCH 4/4] frontend: Add pod metrics to owned pods section Signed-off-by: Oleksandr Dubenko --- .../components/common/Resource/Resource.tsx | 6 +++++ .../namespace/NamespaceDetails.stories.tsx | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/frontend/src/components/common/Resource/Resource.tsx b/frontend/src/components/common/Resource/Resource.tsx index 26777726d0..dac01c830b 100644 --- a/frontend/src/components/common/Resource/Resource.tsx +++ b/frontend/src/components/common/Resource/Resource.tsx @@ -25,6 +25,7 @@ import { KubeObject } from '../../../lib/k8s/KubeObject'; import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { KubeObjectClass } from '../../../lib/k8s/KubeObject'; import Pod, { KubePod, KubeVolume } from '../../../lib/k8s/pod'; +import { METRIC_REFETCH_INTERVAL_MS, PodMetrics } from '../../../lib/k8s/PodMetrics'; import { createRouteURL, RouteURLProps } from '../../../lib/router'; import { getThemeName } from '../../../lib/themes'; import { localeDate, useId } from '../../../lib/util'; @@ -976,6 +977,10 @@ export function OwnedPodsSection(props: OwnedPodsSectionProps) { }; const [pods, error] = Pod.useList(queryData); + const { items: podMetrics } = PodMetrics.useList({ + ...queryData, + refetchInterval: METRIC_REFETCH_INTERVAL_MS, + }); const onlyOneNamespace = !!resource.metadata.namespace || resource.kind === 'Namespace'; const hideNamespaceFilter = onlyOneNamespace || noSearch; @@ -984,6 +989,7 @@ export function OwnedPodsSection(props: OwnedPodsSectionProps) { hideColumns={hideColumns || onlyOneNamespace ? ['namespace'] : undefined} pods={pods} error={error} + metrics={podMetrics} noNamespaceFilter={hideNamespaceFilter} /> ); diff --git a/frontend/src/components/namespace/NamespaceDetails.stories.tsx b/frontend/src/components/namespace/NamespaceDetails.stories.tsx index 0a3a4c3496..cc952d4c56 100644 --- a/frontend/src/components/namespace/NamespaceDetails.stories.tsx +++ b/frontend/src/components/namespace/NamespaceDetails.stories.tsx @@ -69,6 +69,31 @@ Active.parameters = { http.get('http://localhost:4466/api/v1/resourcequotas', () => HttpResponse.error()), http.get('http://localhost:4466/api/v1/limitranges', () => HttpResponse.error()), http.get('http://localhost:4466/api/v1/pods', () => HttpResponse.error()), + http.get( + 'http://localhost:4466/apis/metrics.k8s.io/v1beta1/namespaces/my-namespaces/pods', + () => + HttpResponse.json({ + kind: 'PodMetricsList', + apiVersion: 'metrics.k8s.io/v1beta1', + metadata: {}, + items: [ + { + metadata: { + name: 'successful', + }, + containers: [ + { + name: 'etcd', + usage: { + cpu: '16317640n', + memory: '47544Ki', + }, + }, + ], + }, + ], + }) + ), ], }, },