+ {podMetrics && (
+
+
+ Resource utilization
+
+
+
+
+
+
+
+
+
+
+ )}
{podAndErrors.pod.status?.phase === 'Pending' && (
)}
@@ -111,9 +150,13 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
podAndErrors.pod,
containerStatus.name,
);
+ const containerMetrics = (
+ podMetrics?.containers ?? []
+ ).find(c => c.container === containerStatus.name);
return (
{
expect(getByText('OK')).toBeInTheDocument();
});
it('should render pod, with metrics context', async () => {
- const podNameToClientPodStatus = new Map();
+ const clusterToClientPodStatus = new Map();
- podNameToClientPodStatus.set('dice-roller-6c8646bfd-2m5hv', {
- memory: {
- currentUsage: '1069056',
- requestTotal: '67108864',
- limitTotal: '134217728',
+ clusterToClientPodStatus.set('some-cluster', [
+ {
+ pod: {
+ metadata: {
+ name: 'dice-roller-6c8646bfd-2m5hv',
+ namespace: 'default',
+ },
+ },
+ memory: {
+ currentUsage: '1069056',
+ requestTotal: '67108864',
+ limitTotal: '134217728',
+ },
+ cpu: {
+ currentUsage: 0.4966115,
+ requestTotal: 0.05,
+ limitTotal: 0.05,
+ },
},
- cpu: {
- currentUsage: 0.4966115,
- requestTotal: 0.05,
- limitTotal: 0.05,
- },
- } as any);
+ ] as any);
const wrapper = kubernetesProviders(
undefined,
undefined,
- podNameToClientPodStatus,
+ clusterToClientPodStatus,
+ {
+ name: 'some-cluster',
+ },
);
const { getByText } = render(
wrapper(
@@ -114,7 +125,7 @@ describe('PodsTable', () => {
expect(getByText('limits: 0% of 128MiB')).toBeInTheDocument();
});
it('should render placehoplder when empty metrics context', async () => {
- const podNameToClientPodStatus = new Map();
+ const podNameToClientPodStatus = new Map();
const wrapper = kubernetesProviders(
undefined,
diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx
index 620e91ce72..bebfbd0b5d 100644
--- a/plugins/kubernetes/src/components/Pods/PodsTable.tsx
+++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx
@@ -24,11 +24,12 @@ import {
totalRestarts,
} from '../../utils/pod';
import { Table, TableColumn } from '@backstage/core-components';
-import { PodNamesWithMetricsContext } from '../../hooks/PodNamesWithMetrics';
import { ClusterContext } from '../../hooks/Cluster';
import { useMatchingErrors } from '../../hooks/useMatchingErrors';
import { Pod } from 'kubernetes-models/v1/Pod';
import { V1Pod } from '@kubernetes/client-node';
+import { usePodMetrics } from '../../hooks/usePodMetrics';
+import { Typography } from '@material-ui/core';
export const READY_COLUMNS: PodColumns = 'READY';
export const RESOURCE_COLUMNS: PodColumns = 'RESOURCE';
@@ -74,8 +75,28 @@ const PodDrawerTrigger = ({ pod }: { pod: Pod }) => {
);
};
+const Cpu = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => {
+ const metrics = usePodMetrics(clusterName, pod);
+
+ if (!metrics) {
+ return unknown;
+ }
+
+ return <>{podStatusToCpuUtil(metrics)}>;
+};
+
+const Memory = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => {
+ const metrics = usePodMetrics(clusterName, pod);
+
+ if (!metrics) {
+ return unknown;
+ }
+
+ return <>{podStatusToMemoryUtil(metrics)}>;
+};
+
export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
- const podNamesWithMetrics = useContext(PodNamesWithMetricsContext);
+ const cluster = useContext(ClusterContext);
const defaultColumns: TableColumn[] = [
{
title: 'name',
@@ -104,26 +125,14 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
{
title: 'CPU usage %',
render: (pod: Pod) => {
- const metrics = podNamesWithMetrics.get(pod.metadata?.name ?? '');
-
- if (!metrics) {
- return 'unknown';
- }
-
- return podStatusToCpuUtil(metrics);
+ return ;
},
width: 'auto',
},
{
title: 'Memory usage %',
render: (pod: Pod) => {
- const metrics = podNamesWithMetrics.get(pod.metadata?.name ?? '');
-
- if (!metrics) {
- return 'unknown';
- }
-
- return podStatusToMemoryUtil(metrics);
+ return ;
},
width: 'auto',
},
diff --git a/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.test.tsx b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.test.tsx
new file mode 100644
index 0000000000..56ba0b7ef0
--- /dev/null
+++ b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.test.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import { ResourceUtilization } from './ResourceUtilization';
+import { wrapInTestApp } from '@backstage/test-utils';
+
+describe('ResourceUtilization', () => {
+ it('should render utilization', async () => {
+ const { getByText } = render(
+ wrapInTestApp(
+ ,
+ ),
+ );
+
+ expect(getByText('some-title: 15%')).toBeInTheDocument();
+ expect(getByText('usage: 10%')).toBeInTheDocument();
+ });
+ it('no usage when compressed', async () => {
+ const { getByText, queryByText } = render(
+ wrapInTestApp(
+ ,
+ ),
+ );
+
+ expect(getByText('some-title: 15%')).toBeInTheDocument();
+ expect(queryByText('usage: 10%')).toBeNull();
+ });
+});
diff --git a/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.tsx b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.tsx
new file mode 100644
index 0000000000..492ec0469b
--- /dev/null
+++ b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Grid, Typography } from '@material-ui/core';
+
+import React from 'react';
+import { GaugePropsGetColor, LinearGauge } from '@backstage/core-components';
+import { currentToDeclaredResourceToPerc } from '../../utils/resources';
+
+/**
+ * Context for Pod Metrics
+ *
+ * @public
+ */
+export interface ResourceUtilizationProps {
+ compressed?: boolean;
+ title: string;
+ usage: number | string;
+ total: number | string;
+ totalFormated: string;
+}
+
+// Visible for testing
+export const getProgressColor: GaugePropsGetColor = ({
+ palette,
+ value,
+ inverse,
+ max,
+}) => {
+ if (isNaN(value)) {
+ return palette.status.pending;
+ }
+ const actualMax = max ? max : 100;
+ const actualValue = inverse ? actualMax - value : value;
+
+ if (actualValue >= actualMax) {
+ return palette.status.error;
+ } else if (actualValue > 90 || actualValue < 40) {
+ return palette.status.warning;
+ }
+
+ return palette.status.ok;
+};
+
+/**
+ * Context for Pod Metrics
+ *
+ * @public
+ */
+export const ResourceUtilization = ({
+ compressed = false,
+ title,
+ usage,
+ total,
+ totalFormated,
+}: ResourceUtilizationProps) => {
+ const utilization = currentToDeclaredResourceToPerc(usage, total);
+ return (
+
+
+ {`${title}: ${totalFormated}`}
+
+
+
+ {!compressed && (
+ usage: {`${utilization}%`}
+ )}
+
+
+ );
+};
diff --git a/plugins/kubernetes/src/components/ResourceUtilization/index.ts b/plugins/kubernetes/src/components/ResourceUtilization/index.ts
new file mode 100644
index 0000000000..33d232cbf4
--- /dev/null
+++ b/plugins/kubernetes/src/components/ResourceUtilization/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { ResourceUtilization } from './ResourceUtilization';
diff --git a/plugins/kubernetes/src/components/index.ts b/plugins/kubernetes/src/components/index.ts
index 11550e767b..e0583a1f9e 100644
--- a/plugins/kubernetes/src/components/index.ts
+++ b/plugins/kubernetes/src/components/index.ts
@@ -26,3 +26,4 @@ export * from './KubernetesDrawer';
export * from './Pods';
export * from './ServicesAccordions';
export * from './KubernetesContent';
+export * from './ResourceUtilization';
diff --git a/plugins/kubernetes/src/error-detection/index.ts b/plugins/kubernetes/src/error-detection/index.ts
index 67bfb70f3b..3a8789d79d 100644
--- a/plugins/kubernetes/src/error-detection/index.ts
+++ b/plugins/kubernetes/src/error-detection/index.ts
@@ -18,5 +18,6 @@ export type {
DetectedError,
DetectedErrorsByCluster,
ErrorSeverity,
+ ResourceRef,
} from './types';
export { detectErrors } from './error-detection';
diff --git a/plugins/kubernetes/src/error-detection/types.ts b/plugins/kubernetes/src/error-detection/types.ts
index 7dcc6cd2f4..27dcbdd55b 100644
--- a/plugins/kubernetes/src/error-detection/types.ts
+++ b/plugins/kubernetes/src/error-detection/types.ts
@@ -28,6 +28,11 @@ export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
*/
export type DetectedErrorsByCluster = Map;
+/**
+ * A reference to a Kubernetes object
+ *
+ * @public
+ */
export interface ResourceRef {
name: string;
namespace: string;
diff --git a/plugins/kubernetes/src/hooks/PodNamesWithMetrics.ts b/plugins/kubernetes/src/hooks/PodNamesWithMetrics.ts
index 66a833ad88..0df42a8ab0 100644
--- a/plugins/kubernetes/src/hooks/PodNamesWithMetrics.ts
+++ b/plugins/kubernetes/src/hooks/PodNamesWithMetrics.ts
@@ -16,6 +16,9 @@
import React from 'react';
import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
+/*
+ * @deprecated
+ */
export const PodNamesWithMetricsContext = React.createContext<
Map
>(new Map());
diff --git a/plugins/kubernetes/src/hooks/index.ts b/plugins/kubernetes/src/hooks/index.ts
index c3968a19b2..12400132aa 100644
--- a/plugins/kubernetes/src/hooks/index.ts
+++ b/plugins/kubernetes/src/hooks/index.ts
@@ -20,3 +20,5 @@ export * from './PodNamesWithErrors';
export * from './PodNamesWithMetrics';
export * from './GroupedResponses';
export * from './Cluster';
+export * from './usePodMetrics';
+export * from './useMatchingErrors';
diff --git a/plugins/kubernetes/src/hooks/test-utils.tsx b/plugins/kubernetes/src/hooks/test-utils.tsx
index a7a6f42512..a98f3e4162 100644
--- a/plugins/kubernetes/src/hooks/test-utils.tsx
+++ b/plugins/kubernetes/src/hooks/test-utils.tsx
@@ -17,26 +17,31 @@
import React from 'react';
import { GroupedResponsesContext } from './GroupedResponses';
import { PodNamesWithErrorsContext } from './PodNamesWithErrors';
-import { PodNamesWithMetricsContext } from './PodNamesWithMetrics';
-import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
+import {
+ ClientPodStatus,
+ ClusterAttributes,
+} from '@backstage/plugin-kubernetes-common';
+import { PodMetricsContext } from './usePodMetrics';
+import { ClusterContext } from './Cluster';
export const kubernetesProviders = (
groupedResponses: any = undefined,
podsWithErrors: Set = new Set(),
- podNameToMetrics: Map = new Map<
+ podNameToMetrics: Map = new Map<
string,
- ClientPodStatus
+ ClientPodStatus[]
>(),
+ cluster: ClusterAttributes = { name: 'some-cluster' },
) => {
return (node: React.ReactNode) => (
- <>
+
-
+
{node}
-
+
- >
+
);
};
diff --git a/plugins/kubernetes/src/hooks/useMatchingErrors.ts b/plugins/kubernetes/src/hooks/useMatchingErrors.ts
index 623a32bdf8..aa90d52cbb 100644
--- a/plugins/kubernetes/src/hooks/useMatchingErrors.ts
+++ b/plugins/kubernetes/src/hooks/useMatchingErrors.ts
@@ -18,13 +18,27 @@ import { DetectedError, ResourceRef } from '../error-detection/types';
import { TypeMeta } from '@kubernetes-models/base';
import { IIoK8sApimachineryPkgApisMetaV1ObjectMeta as V1ObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
+/**
+ * Context for detected errors
+ *
+ * @public
+ */
export const DetectedErrorsContext = React.createContext([]);
-type Matcher = {
+/**
+ *
+ * @public
+ */
+export type ErrorMatcher = {
metadata?: V1ObjectMeta;
} & TypeMeta;
-export const useMatchingErrors = (matcher: Matcher): DetectedError[] => {
+/**
+ * Find errors which match the resource
+ *
+ * @public
+ */
+export const useMatchingErrors = (matcher: ErrorMatcher): DetectedError[] => {
const targetRef: ResourceRef = {
name: matcher.metadata?.name ?? '',
namespace: matcher.metadata?.namespace ?? '',
diff --git a/plugins/kubernetes/src/hooks/usePodMetrics.test.tsx b/plugins/kubernetes/src/hooks/usePodMetrics.test.tsx
new file mode 100644
index 0000000000..be74448abd
--- /dev/null
+++ b/plugins/kubernetes/src/hooks/usePodMetrics.test.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { renderHook } from '@testing-library/react-hooks';
+import { PodMetricsContext, usePodMetrics } from './usePodMetrics';
+import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
+
+describe('usePodMetrics', () => {
+ const clientPodStatus = {
+ pod: {
+ metadata: {
+ name: 'some-pod',
+ namespace: 'some-namespace',
+ },
+ },
+ cpu: {},
+ memory: {},
+ containers: [],
+ } as any;
+ const otherClientPodStatus = {
+ pod: {
+ metadata: {
+ name: 'some-other-pod',
+ namespace: 'some-namespace',
+ },
+ },
+ cpu: {},
+ memory: {},
+ containers: [],
+ } as any;
+ it('should filter non-matching ClientPodStatus', () => {
+ const metrics = new Map();
+ metrics.set('cluster', [clientPodStatus]);
+ metrics.set('other-cluster', [otherClientPodStatus]);
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+ );
+
+ const { result } = renderHook(
+ () =>
+ usePodMetrics('cluster', {
+ metadata: {
+ name: 'some-pod',
+ namespace: 'some-namespace',
+ },
+ }),
+ { wrapper },
+ );
+ expect(result.current).toStrictEqual(clientPodStatus);
+ });
+});
diff --git a/plugins/kubernetes/src/hooks/usePodMetrics.ts b/plugins/kubernetes/src/hooks/usePodMetrics.ts
new file mode 100644
index 0000000000..4392487cac
--- /dev/null
+++ b/plugins/kubernetes/src/hooks/usePodMetrics.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useContext } from 'react';
+import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
+import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
+
+/**
+ * Context for Pod Metrics
+ *
+ * @public
+ */
+export const PodMetricsContext = React.createContext<
+ Map
+>(new Map());
+
+/*
+ * @alpha
+ */
+export type PodMetricsMatcher = {
+ metadata?: IObjectMeta;
+};
+
+/**
+ * Find metrics matching the provided pod
+ *
+ * @public
+ */
+export const usePodMetrics = (
+ clusterName: string,
+ matcher: PodMetricsMatcher,
+): ClientPodStatus | undefined => {
+ const targetRef = {
+ name: matcher.metadata?.name ?? '',
+ namespace: matcher.metadata?.namespace ?? '',
+ };
+
+ const metricsMap = useContext(PodMetricsContext);
+
+ const metrics = metricsMap.get(clusterName);
+
+ return metrics?.find(m => {
+ const pod = m.pod;
+ return (
+ targetRef.name === (pod.metadata?.name ?? '') &&
+ targetRef.namespace === (pod.metadata?.namespace ?? '')
+ );
+ });
+};
diff --git a/plugins/kubernetes/src/utils/pod.tsx b/plugins/kubernetes/src/utils/pod.tsx
index ce556fbc8d..f0517f4ec0 100644
--- a/plugins/kubernetes/src/utils/pod.tsx
+++ b/plugins/kubernetes/src/utils/pod.tsx
@@ -29,6 +29,7 @@ import {
} from '@backstage/core-components';
import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
import { Pod } from 'kubernetes-models/v1/Pod';
+import { bytesToMiB, formatMilicores } from './resources';
export const imageChips = (pod: V1Pod): ReactNode => {
const containerStatuses = pod.status?.containerStatuses ?? [];
@@ -131,10 +132,6 @@ export const currentToDeclaredResourceToPerc = (
return `${(numerator * BigInt(100)) / denominator}%`;
};
-const formatMilicores = (value: string | number): string => {
- return `${parseFloat(value.toString()) * 1000}m`;
-};
-
export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => {
const cpuUtil = podStatus.cpu;
@@ -160,10 +157,6 @@ export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => {
);
};
-const bytesToMiB = (value: string | number): string => {
- return `${parseFloat(value.toString()) / 1024 / 1024}MiB`;
-};
-
export const podStatusToMemoryUtil = (
podStatus: ClientPodStatus,
): ReactNode => {
diff --git a/plugins/kubernetes/src/utils/resources.ts b/plugins/kubernetes/src/utils/resources.ts
new file mode 100644
index 0000000000..b1d8dfdc2e
--- /dev/null
+++ b/plugins/kubernetes/src/utils/resources.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const currentToDeclaredResourceToPerc = (
+ current: number | string,
+ resource: number | string,
+): number => {
+ if (Number(resource) === 0) return 0;
+
+ if (typeof current === 'number' && typeof resource === 'number') {
+ return Math.round((current / resource) * 100);
+ }
+
+ const numerator: bigint = BigInt(current);
+ const denominator: bigint = BigInt(resource);
+
+ return Number((numerator * BigInt(100)) / denominator);
+};
+
+export const bytesToMiB = (value: string | number): string => {
+ return `${(parseFloat(value.toString()) / 1024 / 1024).toFixed(0)}MiB`;
+};
+
+export const formatMilicores = (value: string | number): string => {
+ return `${(parseFloat(value.toString()) * 1000).toFixed(0)}m`;
+};