Add support for statefulsets in kubernetes

Signed-off-by: ivgo <ivgo@spreadgroup.com>
This commit is contained in:
ivgo
2022-05-13 11:09:08 +02:00
parent b96380b114
commit 4328737af6
20 changed files with 7879 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': minor
---
Add support to fetch data for Stateful Sets and display an accordion in the same way as with Deployments
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-common': minor
---
Add support to fetch data for Stateful Sets
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': minor
---
Add support to fetch data for Stateful Sets from Kubernetes
+1
View File
@@ -248,6 +248,7 @@ rebase
Recharts
Redash
replicasets
statefulsets
repo
Repo
repos
@@ -371,6 +371,7 @@ following objects:
- replicasets
- horizontalpodautoscalers
- ingresses
- statefulsets
The following RBAC permissions are required on the batch API group for the
following objects:
@@ -97,6 +97,12 @@ export const DEFAULT_OBJECTS: ObjectToFetch[] = [
plural: 'ingresses',
objectType: 'ingresses',
},
{
group: 'apps',
apiVersion: 'v1',
plural: 'statefulsets',
objectType: 'statefulsets',
},
];
export interface KubernetesFanOutHandlerOptions
@@ -77,7 +77,8 @@ export type KubernetesObjectTypes =
| 'jobs'
| 'cronjobs'
| 'ingresses'
| 'customresources';
| 'customresources'
| 'statefulsets';
// Used to load cluster details from different sources
export interface KubernetesClustersSupplier {
+8 -1
View File
@@ -25,6 +25,7 @@ import {
V1Pod,
V1ReplicaSet,
V1Service,
V1StatefulSet,
} from '@kubernetes/client-node';
import { Entity } from '@backstage/catalog-model';
@@ -99,7 +100,8 @@ export type FetchResponse =
| JobsFetchResponse
| CronJobsFetchResponse
| IngressesFetchResponse
| CustomResourceFetchResponse;
| CustomResourceFetchResponse
| StatefulSetsFetchResponse;
export interface PodFetchResponse {
type: 'pods';
@@ -151,6 +153,11 @@ export interface CustomResourceFetchResponse {
resources: Array<any>;
}
export interface StatefulSetsFetchResponse {
type: 'statefulsets';
resources: Array<V1StatefulSet>;
}
export interface KubernetesFetchError {
errorType: KubernetesErrorTypes;
statusCode?: number;
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>
@@ -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,198 @@
/*
* 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(
statefulset.metadata?.name,
'statefulset',
groupedResponses.horizontalPodAutoscalers,
)}
ownedPods={getOwnedResources(statefulset, groupedResponses.pods)}
statefulset={statefulset}
/>
</Grid>
</Grid>
))}
</Grid>
);
};
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { StatefulSetsAccordions } from './StatefulSetsAccordions';
@@ -27,4 +27,5 @@ export const GroupedResponsesContext = React.createContext<GroupedResponses>({
jobs: [],
cronJobs: [],
customResources: [],
statefulsets: [],
});
+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) {
+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,
);
};