feat: Add resource util pod drawer (#18169)

* feat: add resource utilization to pod drawer

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* fix: lint

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* chore: api report

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* chore: api reports

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* test: fix tests

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* test: tests

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

---------

Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
Matthew Clarke
2023-06-15 10:13:52 -04:00
committed by GitHub
parent e12a18894c
commit 4e697e88f0
23 changed files with 607 additions and 65 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-components': patch
'@backstage/plugin-kubernetes': patch
---
Add resource utilization to Pod Drawer
@@ -34,4 +34,28 @@ describe('<LinearGauge />', () => {
const { getByTitle } = await renderInTestApp(<LinearGauge value={1.5} />);
expect(getByTitle('100%')).toBeInTheDocument();
});
it('renders thick', async () => {
const { container, getByTitle } = await renderInTestApp(
<LinearGauge value={0.5} width="thick" />,
);
expect(getByTitle('50%')).toBeInTheDocument();
const linePaths = container.getElementsByClassName('rc-progress-line-path');
expect(linePaths).toHaveLength(1);
const linePath = linePaths[0];
expect(linePath).toHaveAttribute('stroke-width');
expect(linePath.getAttribute('stroke-width')).toBe('4');
});
it('renders thin', async () => {
const { container, getByTitle } = await renderInTestApp(
<LinearGauge value={0.5} width="thin" />,
);
expect(getByTitle('50%')).toBeInTheDocument();
const linePaths = container.getElementsByClassName('rc-progress-line-path');
expect(linePaths).toHaveLength(1);
const linePath = linePaths[0];
expect(linePath).toHaveAttribute('stroke-width');
expect(linePath.getAttribute('stroke-width')).toBe('1');
});
});
@@ -27,11 +27,12 @@ type Props = {
* Progress value between 0.0 - 1.0.
*/
value: number;
width?: 'thick' | 'thin';
getColor?: GaugePropsGetColor;
};
export function LinearGauge(props: Props) {
const { value, getColor = getProgressColor } = props;
const { value, getColor = getProgressColor, width = 'thick' } = props;
const { palette } = useTheme<BackstageTheme>();
if (isNaN(value)) {
return null;
@@ -40,6 +41,7 @@ export function LinearGauge(props: Props) {
if (percent > 100) {
percent = 100;
}
const lineWidth = width === 'thick' ? 4 : 1;
const strokeColor = getColor({
palette,
value: percent,
@@ -51,8 +53,8 @@ export function LinearGauge(props: Props) {
<Typography component="span">
<Line
percent={percent}
strokeWidth={4}
trailWidth={4}
strokeWidth={lineWidth}
trailWidth={lineWidth}
strokeColor={strokeColor}
/>
</Typography>
+55 -2
View File
@@ -8,6 +8,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { AsyncState } from 'react-use/lib/useAsyncFn';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ClientContainerStatus } from '@backstage/plugin-kubernetes-common';
import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
import { ClusterObjects } from '@backstage/plugin-kubernetes-common';
@@ -19,6 +20,7 @@ import { Event as Event_2 } from 'kubernetes-models/v1';
import { IContainer } from 'kubernetes-models/v1';
import { IContainerStatus } from 'kubernetes-models/v1';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IIoK8sApimachineryPkgApisMetaV1ObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
import type { JsonObject } from '@backstage/types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
@@ -29,6 +31,7 @@ import { Pod } from 'kubernetes-models/v1/Pod';
import { Pod as Pod_2 } from 'kubernetes-models/v1';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { TypeMeta } from '@kubernetes-models/base';
import { V1ConfigMap } from '@kubernetes/client-node';
import { V1CronJob } from '@kubernetes/client-node';
import { V1Deployment } from '@kubernetes/client-node';
@@ -87,6 +90,8 @@ export const ContainerCard: React_2.FC<ContainerCardProps>;
// @public
export interface ContainerCardProps {
// (undocumented)
containerMetrics?: ClientContainerStatus;
// (undocumented)
containerSpec?: IContainer;
// (undocumented)
@@ -139,8 +144,6 @@ export interface DetectedError {
proposedFix?: ProposedFix;
// (undocumented)
severity: ErrorSeverity;
// Warning: (ae-forgotten-export) The symbol "ResourceRef" needs to be exported by the entry point index.d.ts
//
// (undocumented)
sourceRef: ResourceRef;
// (undocumented)
@@ -150,6 +153,9 @@ export interface DetectedError {
// @public
export type DetectedErrorsByCluster = Map<string, DetectedError[]>;
// @public
export const DetectedErrorsContext: React_2.Context<DetectedError[]>;
// @public
export const detectErrors: (
objects: ObjectsByEntityResponse,
@@ -176,6 +182,11 @@ export interface ErrorListProps {
podAndErrors: PodAndErrors[];
}
// @public (undocumented)
export type ErrorMatcher = {
metadata?: IIoK8sApimachineryPkgApisMetaV1ObjectMeta;
} & TypeMeta;
// Warning: (ae-forgotten-export) The symbol "ErrorPanelProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -642,6 +653,16 @@ export interface PodLogsProps {
previous?: boolean;
}
// @public
export const PodMetricsContext: React_2.Context<Map<string, ClientPodStatus[]>>;
// Warning: (ae-missing-release-tag) "PodMetricsMatcher" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PodMetricsMatcher = {
metadata?: IObjectMeta;
};
// Warning: (ae-missing-release-tag) "PodNamesWithErrorsContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -673,6 +694,29 @@ export const PodsTable: ({
extraColumns,
}: PodsTablesProps) => JSX.Element;
// @public
export interface ResourceRef {
// (undocumented)
apiGroup: string;
// (undocumented)
kind: string;
// (undocumented)
name: string;
// (undocumented)
namespace: string;
}
// Warning: (ae-forgotten-export) The symbol "ResourceUtilizationProps" needs to be exported by the entry point index.d.ts
//
// @public
export const ResourceUtilization: ({
compressed,
title,
usage,
total,
totalFormated,
}: ResourceUtilizationProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -718,6 +762,9 @@ export const useKubernetesObjects: (
intervalMs?: number,
) => KubernetesObjects;
// @public
export const useMatchingErrors: (matcher: ErrorMatcher) => DetectedError[];
// @public
export const usePodLogs: ({
containerScope,
@@ -725,4 +772,10 @@ export const usePodLogs: ({
}: PodLogsOptions) => AsyncState<{
text: string;
}>;
// @public
export const usePodMetrics: (
clusterName: string,
matcher: PodMetricsMatcher,
) => ClientPodStatus | undefined;
```
@@ -41,7 +41,7 @@ import {
} from '../../hooks';
import { StatusError, StatusOK } from '@backstage/core-components';
import { PodNamesWithMetricsContext } from '../../hooks/PodNamesWithMetrics';
import { PodMetricsContext } from '../../hooks/usePodMetrics';
type ClusterSummaryProps = {
clusterName: string;
@@ -111,19 +111,13 @@ type ClusterProps = {
export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
const groupedResponses = groupResponses(clusterObjects.resources);
const podNameToMetrics = clusterObjects.podMetrics
.flat()
.reduce((accum, next) => {
const name = next.pod.metadata?.name;
if (name !== undefined) {
accum.set(name, next);
}
return accum;
}, new Map<string, ClientPodStatus>());
const podMetricsMap = new Map<string, ClientPodStatus[]>();
podMetricsMap.set(clusterObjects.cluster.name, clusterObjects.podMetrics);
return (
<ClusterContext.Provider value={clusterObjects.cluster}>
<GroupedResponsesContext.Provider value={groupedResponses}>
<PodNamesWithMetricsContext.Provider value={podNameToMetrics}>
<PodMetricsContext.Provider value={podMetricsMap}>
<PodNamesWithErrorsContext.Provider value={podsWithErrors}>
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
@@ -169,7 +163,7 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
</AccordionDetails>
</Accordion>
</PodNamesWithErrorsContext.Provider>
</PodNamesWithMetricsContext.Provider>
</PodMetricsContext.Provider>
</GroupedResponsesContext.Provider>
</ClusterContext.Provider>
);
@@ -29,6 +29,9 @@ import { DateTime } from 'luxon';
import { PodScope, PodLogsDialog } from '../PodLogs';
import { StructuredMetadataTable } from '@backstage/core-components';
import { ClientContainerStatus } from '@backstage/plugin-kubernetes-common';
import { ResourceUtilization } from '../../ResourceUtilization';
import { bytesToMiB, formatMilicores } from '../../../utils/resources';
const getContainerHealthChecks = (
containerSpec: IContainer,
@@ -92,6 +95,7 @@ export interface ContainerCardProps {
podScope: PodScope;
containerSpec?: IContainer;
containerStatus: IContainerStatus;
containerMetrics?: ClientContainerStatus;
}
/**
@@ -103,6 +107,7 @@ export const ContainerCard: React.FC<ContainerCardProps> = ({
podScope,
containerSpec,
containerStatus,
containerMetrics,
}: ContainerCardProps) => {
// This should never be undefined
if (containerSpec === undefined) {
@@ -168,6 +173,53 @@ export const ContainerCard: React.FC<ContainerCardProps> = ({
)}
/>
</Grid>
{containerMetrics && (
<Grid container item xs={12} spacing={0}>
<Grid item xs={12}>
<Typography variant="subtitle1">
Resource utilization
</Typography>
</Grid>
<Grid item xs={12} style={{ minHeight: '5rem' }}>
<ResourceUtilization
compressed
title="CPU requests"
usage={containerMetrics.cpuUsage.currentUsage}
total={containerMetrics.cpuUsage.requestTotal}
totalFormated={formatMilicores(
containerMetrics.cpuUsage.requestTotal,
)}
/>
<ResourceUtilization
compressed
title="CPU limits"
usage={containerMetrics.cpuUsage.currentUsage}
total={containerMetrics.cpuUsage.limitTotal}
totalFormated={formatMilicores(
containerMetrics.cpuUsage.limitTotal,
)}
/>
<ResourceUtilization
compressed
title="Memory requests"
usage={containerMetrics.memoryUsage.currentUsage}
total={containerMetrics.memoryUsage.requestTotal}
totalFormated={bytesToMiB(
containerMetrics.memoryUsage.requestTotal,
)}
/>
<ResourceUtilization
compressed
title="Memory limits"
usage={containerMetrics.memoryUsage.currentUsage}
total={containerMetrics.memoryUsage.limitTotal}
totalFormated={bytesToMiB(
containerMetrics.memoryUsage.requestTotal,
)}
/>
</Grid>
</Grid>
)}
</Grid>
</CardContent>
<CardActions disableSpacing>
@@ -33,6 +33,9 @@ import { PodAndErrors } from '../types';
import { KubernetesDrawer } from '../../KubernetesDrawer';
import { PendingPodContent } from './PendingPodContent';
import { ErrorList } from '../ErrorList';
import { usePodMetrics } from '../../../hooks/usePodMetrics';
import { ResourceUtilization } from '../../ResourceUtilization';
import { bytesToMiB, formatMilicores } from '../../../utils/resources';
const useDrawerContentStyles = makeStyles((_theme: Theme) =>
createStyles({
@@ -76,6 +79,7 @@ interface PodDrawerProps {
*/
export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
const classes = useDrawerContentStyles();
const podMetrics = usePodMetrics(podAndErrors.clusterName, podAndErrors.pod);
return (
<KubernetesDrawer
@@ -95,6 +99,41 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
}
>
<div className={classes.content}>
{podMetrics && (
<Grid container item xs={12}>
<Grid item xs={12}>
<Typography variant="h5">Resource utilization</Typography>
</Grid>
<Grid item xs={6}>
<ResourceUtilization
title="CPU requests"
usage={podMetrics.cpu.currentUsage}
total={podMetrics.cpu.requestTotal}
totalFormated={formatMilicores(podMetrics.cpu.requestTotal)}
/>
<ResourceUtilization
title="CPU limits"
usage={podMetrics.cpu.currentUsage}
total={podMetrics.cpu.limitTotal}
totalFormated={formatMilicores(podMetrics.cpu.limitTotal)}
/>
</Grid>
<Grid item xs={6}>
<ResourceUtilization
title="Memory requests"
usage={podMetrics.memory.currentUsage}
total={podMetrics.memory.requestTotal}
totalFormated={bytesToMiB(podMetrics.memory.requestTotal)}
/>
<ResourceUtilization
title="Memory limits"
usage={podMetrics.memory.currentUsage}
total={podMetrics.memory.limitTotal}
totalFormated={bytesToMiB(podMetrics.memory.requestTotal)}
/>
</Grid>
</Grid>
)}
{podAndErrors.pod.status?.phase === 'Pending' && (
<PendingPodContent pod={podAndErrors.pod} />
)}
@@ -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 (
<ContainerCard
key={`container-card-${podAndErrors.pod.metadata?.name}-${i}`}
containerMetrics={containerMetrics}
podScope={{
podName: podAndErrors.pod.metadata?.name ?? 'unknown',
podNamespace:
+25 -14
View File
@@ -62,25 +62,36 @@ describe('PodsTable', () => {
expect(getByText('OK')).toBeInTheDocument();
});
it('should render pod, with metrics context', async () => {
const podNameToClientPodStatus = new Map<string, ClientPodStatus>();
const clusterToClientPodStatus = new Map<string, ClientPodStatus[]>();
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<string, ClientPodStatus>();
const podNameToClientPodStatus = new Map<string, ClientPodStatus[]>();
const wrapper = kubernetesProviders(
undefined,
+25 -16
View File
@@ -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 <Typography>unknown</Typography>;
}
return <>{podStatusToCpuUtil(metrics)}</>;
};
const Memory = ({ clusterName, pod }: { clusterName: string; pod: Pod }) => {
const metrics = usePodMetrics(clusterName, pod);
if (!metrics) {
return <Typography>unknown</Typography>;
}
return <>{podStatusToMemoryUtil(metrics)}</>;
};
export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
const podNamesWithMetrics = useContext(PodNamesWithMetricsContext);
const cluster = useContext(ClusterContext);
const defaultColumns: TableColumn<Pod>[] = [
{
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 <Cpu clusterName={cluster.name} pod={pod} />;
},
width: 'auto',
},
{
title: 'Memory usage %',
render: (pod: Pod) => {
const metrics = podNamesWithMetrics.get(pod.metadata?.name ?? '');
if (!metrics) {
return 'unknown';
}
return podStatusToMemoryUtil(metrics);
return <Memory clusterName={cluster.name} pod={pod} />;
},
width: 'auto',
},
@@ -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(
<ResourceUtilization
title="some-title"
usage="1000"
total="10000"
totalFormated="15%"
/>,
),
);
expect(getByText('some-title: 15%')).toBeInTheDocument();
expect(getByText('usage: 10%')).toBeInTheDocument();
});
it('no usage when compressed', async () => {
const { getByText, queryByText } = render(
wrapInTestApp(
<ResourceUtilization
compressed
title="some-title"
usage="1000"
total="10000"
totalFormated="15%"
/>,
),
);
expect(getByText('some-title: 15%')).toBeInTheDocument();
expect(queryByText('usage: 10%')).toBeNull();
});
});
@@ -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 (
<Grid container spacing={0}>
<Grid item xs={12}>
<Typography
variant={compressed ? 'caption' : 'subtitle2'}
>{`${title}: ${totalFormated}`}</Typography>
</Grid>
<Grid item xs={12}>
<LinearGauge
getColor={getProgressColor}
width={compressed ? 'thin' : 'thick'}
value={utilization / 100}
/>
{!compressed && (
<Typography variant="caption">usage: {`${utilization}%`}</Typography>
)}
</Grid>
</Grid>
);
};
@@ -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';
@@ -26,3 +26,4 @@ export * from './KubernetesDrawer';
export * from './Pods';
export * from './ServicesAccordions';
export * from './KubernetesContent';
export * from './ResourceUtilization';
@@ -18,5 +18,6 @@ export type {
DetectedError,
DetectedErrorsByCluster,
ErrorSeverity,
ResourceRef,
} from './types';
export { detectErrors } from './error-detection';
@@ -28,6 +28,11 @@ export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
*/
export type DetectedErrorsByCluster = Map<string, DetectedError[]>;
/**
* A reference to a Kubernetes object
*
* @public
*/
export interface ResourceRef {
name: string;
namespace: string;
@@ -16,6 +16,9 @@
import React from 'react';
import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
/*
* @deprecated
*/
export const PodNamesWithMetricsContext = React.createContext<
Map<string, ClientPodStatus>
>(new Map<string, ClientPodStatus>());
+2
View File
@@ -20,3 +20,5 @@ export * from './PodNamesWithErrors';
export * from './PodNamesWithMetrics';
export * from './GroupedResponses';
export * from './Cluster';
export * from './usePodMetrics';
export * from './useMatchingErrors';
+13 -8
View File
@@ -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<string> = new Set<string>(),
podNameToMetrics: Map<string, ClientPodStatus> = new Map<
podNameToMetrics: Map<string, ClientPodStatus[]> = new Map<
string,
ClientPodStatus
ClientPodStatus[]
>(),
cluster: ClusterAttributes = { name: 'some-cluster' },
) => {
return (node: React.ReactNode) => (
<>
<ClusterContext.Provider value={cluster}>
<GroupedResponsesContext.Provider value={groupedResponses}>
<PodNamesWithMetricsContext.Provider value={podNameToMetrics}>
<PodMetricsContext.Provider value={podNameToMetrics}>
<PodNamesWithErrorsContext.Provider value={podsWithErrors}>
{node}
</PodNamesWithErrorsContext.Provider>
</PodNamesWithMetricsContext.Provider>
</PodMetricsContext.Provider>
</GroupedResponsesContext.Provider>
</>
</ClusterContext.Provider>
);
};
@@ -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<DetectedError[]>([]);
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 ?? '',
@@ -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<string, ClientPodStatus[]>();
metrics.set('cluster', [clientPodStatus]);
metrics.set('other-cluster', [otherClientPodStatus]);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<PodMetricsContext.Provider value={metrics}>
{children}
</PodMetricsContext.Provider>
);
const { result } = renderHook(
() =>
usePodMetrics('cluster', {
metadata: {
name: 'some-pod',
namespace: 'some-namespace',
},
}),
{ wrapper },
);
expect(result.current).toStrictEqual(clientPodStatus);
});
});
@@ -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<string, ClientPodStatus[]>
>(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 ?? '')
);
});
};
+1 -8
View File
@@ -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 => {
+38
View File
@@ -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`;
};