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
+96
View File
@@ -1,5 +1,101 @@
# @backstage/plugin-kubernetes
## 0.6.7-next.2
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.10.0-next.2
- @backstage/plugin-kubernetes-common@0.4.0-next.1
- @backstage/catalog-model@1.1.0-next.2
- @backstage/theme@0.2.16-next.1
- @backstage/plugin-catalog-react@1.1.2-next.2
## 0.6.7-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.9.6-next.1
- @backstage/catalog-model@1.1.0-next.1
- @backstage/theme@0.2.16-next.0
- @backstage/plugin-catalog-react@1.1.2-next.1
## 0.6.7-next.0
### Patch Changes
- Updated dependencies
- @backstage/catalog-model@1.1.0-next.0
- @backstage/core-components@0.9.6-next.0
- @backstage/plugin-kubernetes-common@0.4.0-next.0
- @backstage/plugin-catalog-react@1.1.2-next.0
## 0.6.6
### Patch Changes
- 4328737af6: Add support to fetch data for Stateful Sets and display an accordion in the same way as with Deployments
- b9b8bbc7d9: show request/limit CPU and Memory on the UI
- 5553f09e80: ability to configure refresh interval on Kubernetes tab
- 8f7b1835df: Updated dependency `msw` to `^0.41.0`.
- 81304e3e91: Fix for HPA matching when deploying same HPA in multiple namespaces
- Updated dependencies
- @backstage/plugin-catalog-react@1.1.1
- @backstage/core-components@0.9.5
- @backstage/plugin-kubernetes-common@0.3.0
- @backstage/core-plugin-api@1.0.3
- @backstage/catalog-model@1.0.3
## 0.6.6-next.2
### Patch Changes
- 5553f09e80: ability to configure refresh interval on Kubernetes tab
- Updated dependencies
- @backstage/core-components@0.9.5-next.2
## 0.6.6-next.1
### Patch Changes
- b9b8bbc7d9: show request/limit CPU and Memory on the UI
- 8f7b1835df: Updated dependency `msw` to `^0.41.0`.
- Updated dependencies
- @backstage/core-components@0.9.5-next.1
- @backstage/core-plugin-api@1.0.3-next.0
- @backstage/catalog-model@1.0.3-next.0
- @backstage/plugin-catalog-react@1.1.1-next.1
- @backstage/plugin-kubernetes-common@0.3.0-next.1
## 0.6.6-next.0
### Patch Changes
- 4328737af6: Add support to fetch data for Stateful Sets and display an accordion in the same way as with Deployments
- 81304e3e91: Fix for HPA matching when deploying same HPA in multiple namespaces
- Updated dependencies
- @backstage/plugin-catalog-react@1.1.1-next.0
- @backstage/core-components@0.9.5-next.0
- @backstage/plugin-kubernetes-common@0.3.0-next.0
## 0.6.5
### Patch Changes
- 1ef98cfe48: add Azure Identity auth provider and AKS dashboard formatter
- 447e060872: Add support for 'oidc' as authProvider for kubernetes authentication
and adds optional 'oidcTokenProvider' config value. This will allow
users to authenticate to kubernetes cluster using id tokens obtained
from the configured auth provider in their backstage instance.
- Updated dependencies
- @backstage/core-components@0.9.4
- @backstage/plugin-kubernetes-common@0.2.10
- @backstage/core-plugin-api@1.0.2
- @backstage/plugin-catalog-react@1.1.0
- @backstage/config@1.0.1
- @backstage/catalog-model@1.0.2
## 0.6.5-next.3
### Patch Changes
+16 -30
View File
@@ -30,17 +30,7 @@ import { V1ObjectMeta } from '@kubernetes/client-node';
import { V1Pod } from '@kubernetes/client-node';
import { V1ReplicaSet } from '@kubernetes/client-node';
import { V1Service } from '@kubernetes/client-node';
// Warning: (ae-forgotten-export) The symbol "KubernetesAuthProvider" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "AwsKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class AwsKubernetesAuthProvider implements KubernetesAuthProvider {
// (undocumented)
decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
}
import { V1StatefulSet } from '@kubernetes/client-node';
// Warning: (ae-forgotten-export) The symbol "ClusterProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "Cluster" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -143,7 +133,14 @@ export const detectErrors: (
// Warning: (ae-missing-release-tag) "EntityKubernetesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityKubernetesContent: (_props: {}) => JSX.Element;
export const EntityKubernetesContent: (
props: EntityKubernetesContentProps,
) => JSX.Element;
// @public
export type EntityKubernetesContentProps = {
refreshIntervalMs?: number;
};
// 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 exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -171,6 +168,7 @@ export function formatClusterLink(
options: FormatClusterLinkOptions,
): string | undefined;
// Warning: (ae-forgotten-export) The symbol "KubernetesAuthProvider" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "GoogleKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -184,18 +182,6 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
): Promise<KubernetesRequestBody>;
}
// Warning: (ae-missing-release-tag) "GoogleServiceAccountAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class GoogleServiceAccountAuthProvider
implements KubernetesAuthProvider
{
// (undocumented)
decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
}
// Warning: (ae-missing-release-tag) "GroupedResponses" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -212,6 +198,8 @@ export interface GroupedResponses extends DeploymentResources {
jobs: V1Job[];
// (undocumented)
services: V1Service[];
// (undocumented)
statefulsets: V1StatefulSet[];
}
// Warning: (ae-missing-release-tag) "GroupedResponsesContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -332,6 +320,7 @@ export class KubernetesBackendClient implements KubernetesApi {
// @public (undocumented)
export const KubernetesContent: ({
entity,
refreshIntervalMs,
}: KubernetesContentProps) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "KubernetesDrawerable" needs to be exported by the entry point index.d.ts
@@ -402,16 +391,13 @@ export const PodsTable: ({
extraColumns,
}: PodsTablesProps) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const Router: (_props: Props) => JSX.Element;
export const Router: (props: { refreshIntervalMs?: number }) => JSX.Element;
// Warning: (ae-missing-release-tag) "ServiceAccountKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class ServiceAccountKubernetesAuthProvider
// @public
export class ServerSideKubernetesAuthProvider
implements KubernetesAuthProvider
{
// (undocumented)
+14 -14
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-kubernetes",
"description": "A Backstage plugin that integrates towards Kubernetes",
"version": "0.6.5-next.3",
"version": "0.6.7-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -34,14 +34,14 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/catalog-model": "^1.0.2-next.0",
"@backstage/config": "^1.0.1-next.0",
"@backstage/core-components": "^0.9.4-next.2",
"@backstage/core-plugin-api": "^1.0.2-next.1",
"@backstage/plugin-catalog-react": "^1.1.0-next.2",
"@backstage/plugin-kubernetes-common": "^0.2.10-next.1",
"@backstage/theme": "^0.2.15",
"@kubernetes/client-node": "^0.16.0",
"@backstage/catalog-model": "^1.1.0-next.2",
"@backstage/config": "^1.0.1",
"@backstage/core-components": "^0.10.0-next.2",
"@backstage/core-plugin-api": "^1.0.3",
"@backstage/plugin-catalog-react": "^1.1.2-next.2",
"@backstage/plugin-kubernetes-common": "^0.4.0-next.1",
"@backstage/theme": "^0.2.16-next.1",
"@kubernetes/client-node": "^0.17.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@@ -57,10 +57,10 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.17.1-next.2",
"@backstage/core-app-api": "^1.0.2-next.1",
"@backstage/dev-utils": "^1.0.2-next.2",
"@backstage/test-utils": "^1.1.0-next.2",
"@backstage/cli": "^0.18.0-next.2",
"@backstage/core-app-api": "^1.0.4-next.0",
"@backstage/dev-utils": "^1.0.4-next.2",
"@backstage/test-utils": "^1.1.2-next.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.0",
@@ -68,7 +68,7 @@
"@types/jest": "^26.0.7",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.35.0"
"msw": "^0.43.0"
},
"files": [
"dist"
+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,
);
};