Merge branch 'backstage:master' into go/expose-detecterrors

This commit is contained in:
Guilherme Oenning
2022-07-11 09:44:05 +01:00
committed by GitHub
1851 changed files with 258634 additions and 220659 deletions
+10 -4
View File
@@ -32,9 +32,7 @@ export const isKubernetesAvailable = (entity: Entity) =>
entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION],
);
type Props = {};
export const Router = (_props: Props) => {
export const Router = (props: { refreshIntervalMs?: number }) => {
const { entity } = useEntity();
const kubernetesAnnotationValue =
@@ -49,7 +47,15 @@ export const Router = (_props: Props) => {
) {
return (
<Routes>
<Route path="/" element={<KubernetesContent entity={entity} />} />
<Route
path="/"
element={
<KubernetesContent
entity={entity}
refreshIntervalMs={props.refreshIntervalMs}
/>
}
/>
</Routes>
);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -29,6 +29,7 @@ import {
} from '@backstage/plugin-kubernetes-common';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { DeploymentsAccordions } from '../DeploymentsAccordions';
import { StatefulSetsAccordions } from '../StatefulSetsAccordions';
import { groupResponses } from '../../utils/response';
import { IngressesAccordions } from '../IngressesAccordions';
import { ServicesAccordions } from '../ServicesAccordions';
@@ -142,6 +143,9 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
<Grid item>
<DeploymentsAccordions />
</Grid>
<Grid item>
<StatefulSetsAccordions />
</Grid>
<Grid item>
<IngressesAccordions />
</Grid>
@@ -268,8 +268,11 @@ export const RolloutAccordions = ({
<RolloutAccordion
defaultExpanded={defaultExpanded}
matchingHpa={getMatchingHpa(
rollout.metadata?.name,
'rollout',
{
name: rollout.metadata?.name,
namespace: rollout.metadata?.namespace,
kind: 'rollout',
},
groupedResponses.horizontalPodAutoscalers,
)}
ownedPods={getOwnedPodsThroughReplicaSets(
@@ -186,8 +186,11 @@ export const DeploymentsAccordions = ({}: DeploymentsAccordionsProps) => {
<Grid item xs>
<DeploymentAccordion
matchingHpa={getMatchingHpa(
deployment.metadata?.name,
'deployment',
{
name: deployment.metadata?.name,
namespace: deployment.metadata?.namespace,
kind: 'deployment',
},
groupedResponses.horizontalPodAutoscalers,
)}
ownedPods={getOwnedPodsThroughReplicaSets(
@@ -25,10 +25,20 @@ import EmptyStateImage from '../assets/emptystate.svg';
import { useKubernetesObjects } from '../hooks';
import { Content, Page, Progress } from '@backstage/core-components';
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
type KubernetesContentProps = {
entity: Entity;
refreshIntervalMs?: number;
children?: React.ReactNode;
};
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(entity);
export const KubernetesContent = ({
entity,
refreshIntervalMs,
}: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(
entity,
refreshIntervalMs,
);
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
+4 -4
View File
@@ -108,10 +108,10 @@ describe('PodsTable', () => {
expect(getByText('1/1')).toBeInTheDocument();
expect(getByText('0')).toBeInTheDocument();
expect(getByText('OK')).toBeInTheDocument();
expect(getByText('requests: 99%')).toBeInTheDocument();
expect(getByText('limits: 99%')).toBeInTheDocument();
expect(getByText('requests: 1%')).toBeInTheDocument();
expect(getByText('limits: 0%')).toBeInTheDocument();
expect(getByText('requests: 99% of 50m')).toBeInTheDocument();
expect(getByText('limits: 99% of 50m')).toBeInTheDocument();
expect(getByText('requests: 1% of 64MiB')).toBeInTheDocument();
expect(getByText('limits: 0% of 128MiB')).toBeInTheDocument();
});
it('should render placehoplder when empty metrics context', async () => {
const podNameToClientPodStatus = new Map<string, ClientPodStatus>();
@@ -0,0 +1,63 @@
/*
* Copyright 2020 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 * as statefulsets from '../../__fixtures__/2-statefulsets.json';
import { renderInTestApp } from '@backstage/test-utils';
import { StatefulSetDrawer } from './StatefulSetDrawer';
describe('StatefulSetDrawer', () => {
it('should render statefulset drawer', async () => {
const { getByText, getAllByText } = await renderInTestApp(
<StatefulSetDrawer
statefulset={(statefulsets as any).statefulsets[0]}
expanded
/>,
);
expect(getAllByText('dice-roller')).toHaveLength(3);
expect(getByText('StatefulSet')).toBeInTheDocument();
expect(getByText('YAML')).toBeInTheDocument();
expect(getByText('Type: RollingUpdate')).toBeInTheDocument();
expect(getByText('Rolling Update:')).toBeInTheDocument();
expect(getByText('Max Surge: 25%')).toBeInTheDocument();
expect(getByText('Max Unavailable: 25%')).toBeInTheDocument();
expect(getByText('Pod Management Policy')).toBeInTheDocument();
expect(getByText('Parallel')).toBeInTheDocument();
expect(getByText('Service Name')).toBeInTheDocument();
expect(getByText('Selector')).toBeInTheDocument();
expect(getByText('Match Labels:')).toBeInTheDocument();
expect(getByText('App: dice-roller')).toBeInTheDocument();
expect(getByText('Revision History Limit')).toBeInTheDocument();
expect(getByText('10')).toBeInTheDocument();
expect(getByText('namespace: default')).toBeInTheDocument();
});
it('should render statefulset drawer without namespace', async () => {
const statefulset = (statefulsets as any).statefulsets[0];
const { queryByText } = await renderInTestApp(
<StatefulSetDrawer
statefulset={{
...statefulset,
metadata: { ...statefulset.metadata, namespace: undefined },
}}
expanded
/>,
);
expect(queryByText('namespace: default')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,79 @@
/*
* Copyright 2020 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 { V1StatefulSet } from '@kubernetes/client-node';
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
import { renderCondition } from '../../utils/pod';
import { Typography, Grid, Chip } from '@material-ui/core';
export const StatefulSetDrawer = ({
statefulset,
expanded,
}: {
statefulset: V1StatefulSet;
expanded?: boolean;
}) => {
const namespace = statefulset.metadata?.namespace;
return (
<KubernetesDrawer
object={statefulset}
expanded={expanded}
kind="StatefulSet"
renderObject={(statefulsetObj: V1StatefulSet) => {
const conditions = (statefulsetObj.status?.conditions ?? [])
.map(renderCondition)
.reduce((accum, next) => {
accum[next[0]] = next[1];
return accum;
}, {} as { [key: string]: React.ReactNode });
return {
updateStrategy: statefulset.spec?.updateStrategy ?? '???',
podManagementPolicy: statefulset.spec?.podManagementPolicy ?? '???',
serviceName: statefulset.spec?.serviceName ?? '???',
selector: statefulset.spec?.selector ?? '???',
revisionHistoryLimit: statefulset.spec?.revisionHistoryLimit ?? '???',
...conditions,
};
}}
>
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
spacing={0}
>
<Grid item>
<Typography variant="h5">
{statefulset.metadata?.name ?? 'unknown object'}
</Typography>
</Grid>
<Grid item>
<Typography color="textSecondary" variant="body1">
Stateful Set
</Typography>
</Grid>
{namespace && (
<Grid item>
<Chip size="small" label={`namespace: ${namespace}`} />
</Grid>
)}
</Grid>
</KubernetesDrawer>
);
};
@@ -0,0 +1,45 @@
/*
* Copyright 2020 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 { StatefulSetsAccordions } from './StatefulSetsAccordions';
import * as twoStatefulSetsFixture from '../../__fixtures__/2-statefulsets.json';
import { wrapInTestApp } from '@backstage/test-utils';
import { kubernetesProviders } from '../../hooks/test-utils';
describe('StatefulSetsAccordions', () => {
it('should render 2 statefulsets', async () => {
const wrapper = kubernetesProviders(
twoStatefulSetsFixture,
new Set(['dice-roller-canary-7d64cd756c-vtbdx']),
);
const { getByText, getAllByText } = render(
wrapper(wrapInTestApp(<StatefulSetsAccordions />)),
);
expect(getByText('dice-roller')).toBeInTheDocument();
expect(getByText('10 pods')).toBeInTheDocument();
expect(getByText('No pods with errors')).toBeInTheDocument();
expect(getByText('dice-roller-canary')).toBeInTheDocument();
expect(getByText('2 pods')).toBeInTheDocument();
expect(getByText('1 pod with errors')).toBeInTheDocument();
expect(getAllByText('namespace: default')).toHaveLength(2);
});
});
@@ -0,0 +1,201 @@
/*
* Copyright 2020 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 {
Accordion,
AccordionDetails,
AccordionSummary,
Divider,
Grid,
Typography,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import {
V1Pod,
V1HorizontalPodAutoscaler,
V1StatefulSet,
} from '@kubernetes/client-node';
import { PodsTable } from '../Pods';
import { StatefulSetDrawer } from './StatefulSetDrawer';
import { HorizontalPodAutoscalerDrawer } from '../HorizontalPodAutoscalers';
import { getMatchingHpa, getOwnedResources } from '../../utils/owner';
import {
GroupedResponsesContext,
PodNamesWithErrorsContext,
} from '../../hooks';
import { StatusError, StatusOK } from '@backstage/core-components';
import { READY_COLUMNS, RESOURCE_COLUMNS } from '../Pods/PodsTable';
type StatefulSetsAccordionsProps = {
children?: React.ReactNode;
};
type StatefulSetAccordionProps = {
statefulset: V1StatefulSet;
ownedPods: V1Pod[];
matchingHpa?: V1HorizontalPodAutoscaler;
children?: React.ReactNode;
};
type StatefulSetSummaryProps = {
statefulset: V1StatefulSet;
numberOfCurrentPods: number;
numberOfPodsWithErrors: number;
hpa?: V1HorizontalPodAutoscaler;
children?: React.ReactNode;
};
const StatefulSetSummary = ({
statefulset,
numberOfCurrentPods,
numberOfPodsWithErrors,
hpa,
}: StatefulSetSummaryProps) => {
return (
<Grid
container
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<Grid xs={3} item>
<StatefulSetDrawer statefulset={statefulset} />
</Grid>
<Grid item xs={1}>
<Divider style={{ height: '5em' }} orientation="vertical" />
</Grid>
{hpa && (
<Grid item xs={3}>
<HorizontalPodAutoscalerDrawer hpa={hpa}>
<Grid
item
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
spacing={0}
>
<Grid item>
<Typography variant="subtitle2">
min replicas {hpa.spec?.minReplicas ?? '?'} / max replicas{' '}
{hpa.spec?.maxReplicas ?? '?'}
</Typography>
</Grid>
<Grid item>
<Typography variant="subtitle2">
current CPU usage:{' '}
{hpa.status?.currentCPUUtilizationPercentage ?? '?'}%
</Typography>
</Grid>
<Grid item>
<Typography variant="subtitle2">
target CPU usage:{' '}
{hpa.spec?.targetCPUUtilizationPercentage ?? '?'}%
</Typography>
</Grid>
</Grid>
</HorizontalPodAutoscalerDrawer>
</Grid>
)}
<Grid
item
container
xs={3}
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item>
<StatusOK>{numberOfCurrentPods} pods</StatusOK>
</Grid>
<Grid item>
{numberOfPodsWithErrors > 0 ? (
<StatusError>
{numberOfPodsWithErrors} pod
{numberOfPodsWithErrors > 1 ? 's' : ''} with errors
</StatusError>
) : (
<StatusOK>No pods with errors</StatusOK>
)}
</Grid>
</Grid>
</Grid>
);
};
const StatefulSetAccordion = ({
statefulset,
ownedPods,
matchingHpa,
}: StatefulSetAccordionProps) => {
const podNamesWithErrors = useContext(PodNamesWithErrorsContext);
const podsWithErrors = ownedPods.filter(p =>
podNamesWithErrors.has(p.metadata?.name ?? ''),
);
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<StatefulSetSummary
statefulset={statefulset}
numberOfCurrentPods={ownedPods.length}
numberOfPodsWithErrors={podsWithErrors.length}
hpa={matchingHpa}
/>
</AccordionSummary>
<AccordionDetails>
<PodsTable
pods={ownedPods}
extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
/>
</AccordionDetails>
</Accordion>
);
};
export const StatefulSetsAccordions = ({}: StatefulSetsAccordionsProps) => {
const groupedResponses = useContext(GroupedResponsesContext);
return (
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
{groupedResponses.statefulsets.map((statefulset, i) => (
<Grid container item key={i} xs>
<Grid item xs>
<StatefulSetAccordion
matchingHpa={getMatchingHpa(
{
name: statefulset.metadata?.name,
namespace: statefulset.metadata?.namespace,
kind: 'statefulset',
},
groupedResponses.horizontalPodAutoscalers,
)}
ownedPods={getOwnedResources(statefulset, groupedResponses.pods)}
statefulset={statefulset}
/>
</Grid>
</Grid>
))}
</Grid>
);
};
@@ -13,15 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { KubernetesAuthProvider } from './types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
export class AwsKubernetesAuthProvider implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
// No-op, with aws auth, server's AWS credentials are used for access
return requestBody;
}
}
export { StatefulSetsAccordions } from './StatefulSetsAccordions';
@@ -27,4 +27,5 @@ export const GroupedResponsesContext = React.createContext<GroupedResponses>({
jobs: [],
cronJobs: [],
customResources: [],
statefulsets: [],
});
+1
View File
@@ -25,6 +25,7 @@ export {
kubernetesPlugin as plugin,
EntityKubernetesContent,
} from './plugin';
export type { EntityKubernetesContentProps } from './plugin';
export { Router, isKubernetesAvailable } from './Router';
export * from './api';
export * from './kubernetes-auth-provider';
@@ -1,27 +0,0 @@
/*
* Copyright 2020 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 { KubernetesAuthProvider } from './types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
export class AzureKubernetesAuthProvider implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
// No-op, with azure auth, server's Azure credentials are used for access
return requestBody;
}
}
@@ -17,11 +17,8 @@
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types';
import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
import { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider';
import { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider';
import { OAuthApi, OpenIdConnectApi } from '@backstage/core-plugin-api';
import { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider';
import { AzureKubernetesAuthProvider } from './AzureKubernetesAuthProvider';
import { OidcKubernetesAuthProvider } from './OidcKubernetesAuthProvider';
export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
@@ -41,16 +38,23 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
);
this.kubernetesAuthProviderMap.set(
'serviceAccount',
new ServiceAccountKubernetesAuthProvider(),
new ServerSideKubernetesAuthProvider(),
);
this.kubernetesAuthProviderMap.set(
'googleServiceAccount',
new GoogleServiceAccountAuthProvider(),
new ServerSideKubernetesAuthProvider(),
);
this.kubernetesAuthProviderMap.set(
'aws',
new ServerSideKubernetesAuthProvider(),
);
this.kubernetesAuthProviderMap.set('aws', new AwsKubernetesAuthProvider());
this.kubernetesAuthProviderMap.set(
'azure',
new AzureKubernetesAuthProvider(),
new ServerSideKubernetesAuthProvider(),
);
this.kubernetesAuthProviderMap.set(
'localKubectlProxy',
new ServerSideKubernetesAuthProvider(),
);
if (options.oidcProviders) {
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2022 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.
@@ -17,13 +17,18 @@
import { KubernetesAuthProvider } from './types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
export class GoogleServiceAccountAuthProvider
/**
* No-op KubernetesAuthProvider, authorization will be handled in the kubernetes-backend plugin
*
* @public
*/
export class ServerSideKubernetesAuthProvider
implements KubernetesAuthProvider
{
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
// No-op, with google service account auth, server's AWS credentials are used for access
// No-op, auth will be taken care of on the server-side
return requestBody;
}
}
@@ -1,29 +0,0 @@
/*
* Copyright 2020 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 { KubernetesAuthProvider } from './types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
export class ServiceAccountKubernetesAuthProvider
implements KubernetesAuthProvider
{
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
// No-op, with service account for auth, cluster config/details should already have serviceAccountToken
return requestBody;
}
}
@@ -17,7 +17,5 @@
export { kubernetesAuthProvidersApiRef } from './types';
export type { KubernetesAuthProvidersApi } from './types';
export { KubernetesAuthProviders } from './KubernetesAuthProviders';
export { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider';
export { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
export { GoogleServiceAccountAuthProvider } from './GoogleServiceAccountAuthProvider';
export { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
export { ServerSideKubernetesAuthProvider } from './ServerSideAuthProvider';
+15 -1
View File
@@ -76,7 +76,21 @@ export const kubernetesPlugin = createPlugin({
},
});
export const EntityKubernetesContent = kubernetesPlugin.provide(
/**
* Props of EntityKubernetesContent
*
* @public
*/
export type EntityKubernetesContentProps = {
/**
* Sets the refresh interval in milliseconds. The default value is 10000 (10 seconds)
*/
refreshIntervalMs?: number;
};
export const EntityKubernetesContent: (
props: EntityKubernetesContentProps,
) => JSX.Element = kubernetesPlugin.provide(
createRoutableExtension({
name: 'EntityKubernetesContent',
component: () => import('./Router').then(m => m.Router),
+11
View File
@@ -14,3 +14,14 @@
* limitations under the License.
*/
import '@testing-library/jest-dom';
// eslint-disable-next-line no-restricted-imports
import { TextDecoder, TextEncoder } from 'util';
// These are missing from jest-node, so not available on global.
Object.defineProperty(global, 'TextEncoder', {
value: TextEncoder,
});
Object.defineProperty(global, 'TextDecoder', {
value: TextDecoder,
});
+2
View File
@@ -25,6 +25,7 @@ import {
V1Ingress,
V1Job,
V1CronJob,
V1StatefulSet,
} from '@kubernetes/client-node';
export interface DeploymentResources {
@@ -41,6 +42,7 @@ export interface GroupedResponses extends DeploymentResources {
jobs: V1Job[];
cronJobs: V1CronJob[];
customResources: any[];
statefulsets: V1StatefulSet[];
}
export interface ClusterLinksFormatterOptions {
@@ -21,6 +21,7 @@ const kindMappings: Record<string, string> = {
ingress: 'ingress',
service: 'service',
horizontalpodautoscaler: 'deployment',
statefulset: 'statefulset',
};
export function standardFormatter(options: ClusterLinksFormatterOptions) {
+11 -4
View File
@@ -54,17 +54,24 @@ export const getOwnedPodsThroughReplicaSets = (
}, [] as V1Pod[]);
};
interface ResourceRef {
kind: string;
namespace?: string;
name?: string;
}
export const getMatchingHpa = (
ownerName: string | undefined,
ownerKind: string,
owner: ResourceRef,
hpas: V1HorizontalPodAutoscaler[],
): V1HorizontalPodAutoscaler | undefined => {
return hpas.find(hpa => {
return (
(hpa.spec?.scaleTargetRef?.kind ?? '').toLocaleLowerCase('en-US') ===
ownerKind.toLocaleLowerCase('en-US') &&
owner.kind.toLocaleLowerCase('en-US') &&
(hpa.metadata?.namespace ?? '') ===
(owner.namespace ?? 'unknown-namespace') &&
(hpa.spec?.scaleTargetRef?.name ?? '') ===
(ownerName ?? 'unknown-deployment')
(owner.name ?? 'unknown-deployment')
);
});
};
+29 -2
View File
@@ -15,7 +15,11 @@
*/
import React from 'react';
import { currentToDeclaredResourceToPerc, podStatusToCpuUtil } from './pod';
import {
currentToDeclaredResourceToPerc,
podStatusToCpuUtil,
podStatusToMemoryUtil,
} from './pod';
import { SubvalueCell } from '@backstage/core-components';
describe('pod', () => {
@@ -46,7 +50,30 @@ describe('pod', () => {
},
} as any);
expect(result).toStrictEqual(
<SubvalueCell subvalue="limits: 50%" value="requests: 99%" />,
<SubvalueCell
subvalue="limits: 50% of 100m"
value="requests: 99% of 50m"
/>,
);
});
});
describe('podStatusToMemoryUtil', () => {
it('does use correct units', () => {
const result = podStatusToMemoryUtil({
memory: {
// ~91.5 MiB
currentUsage: '95948800',
// 320 MiB
limitTotal: '335544320',
// 192 MiB
requestTotal: '201326592',
},
} as any);
expect(result).toStrictEqual(
<SubvalueCell
subvalue="limits: 28% of 320MiB"
value="requests: 47% of 192MiB"
/>,
);
});
});
+12 -4
View File
@@ -130,6 +130,10 @@ 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;
@@ -146,15 +150,19 @@ export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => {
value={`requests: ${currentToDeclaredResourceToPerc(
currentUsage,
cpuUtil.requestTotal,
)}`}
)} of ${formatMilicores(cpuUtil.requestTotal)}`}
subvalue={`limits: ${currentToDeclaredResourceToPerc(
currentUsage,
cpuUtil.limitTotal,
)}`}
)} of ${formatMilicores(cpuUtil.limitTotal)}`}
/>
);
};
const bytesToMiB = (value: string | number): string => {
return `${parseFloat(value.toString()) / 1024 / 1024}MiB`;
};
export const podStatusToMemoryUtil = (
podStatus: ClientPodStatus,
): ReactNode => {
@@ -165,11 +173,11 @@ export const podStatusToMemoryUtil = (
value={`requests: ${currentToDeclaredResourceToPerc(
memUtil.currentUsage,
memUtil.requestTotal,
)}`}
)} of ${bytesToMiB(memUtil.requestTotal)}`}
subvalue={`limits: ${currentToDeclaredResourceToPerc(
memUtil.currentUsage,
memUtil.limitTotal,
)}`}
)} of ${bytesToMiB(memUtil.limitTotal)}`}
/>
);
};
+4
View File
@@ -54,6 +54,9 @@ export const groupResponses = (
case 'customresources':
prev.customResources.push(...next.resources);
break;
case 'statefulsets':
prev.statefulsets.push(...next.resources);
break;
default:
}
return prev;
@@ -69,6 +72,7 @@ export const groupResponses = (
jobs: [],
cronJobs: [],
customResources: [],
statefulsets: [],
} as GroupedResponses,
);
};