Add support for statefulsets in kubernetes
Signed-off-by: ivgo <ivgo@spreadgroup.com>
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-common': minor
|
||||
---
|
||||
|
||||
Add support to fetch data for Stateful Sets
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
Add support to fetch data for Stateful Sets from Kubernetes
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+45
@@ -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: [],
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user