kubernetes: deployment pod table (#2618)
* deployment pod table * prettier manifest file * pr feedback
This commit is contained in:
@@ -8,7 +8,7 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dice-roller
|
||||
replicas: 2
|
||||
replicas: 10
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
@@ -21,6 +21,38 @@ spec:
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dice-roller-canary
|
||||
labels:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dice-roller-canary
|
||||
replicas: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dice-roller-canary
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 80
|
||||
- name: side-car
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 81
|
||||
- name: other-side-car
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 82
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@backstage/core": "^0.1.1-alpha.23",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23",
|
||||
"@backstage/theme": "^0.1.1-alpha.23",
|
||||
"@kubernetes/client-node": "^0.12.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -35,6 +36,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.23",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.23",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.23",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { DeploymentTables } from './DeploymentTables';
|
||||
import * as twoDeployFixture from './__fixtures__/2-deployments.json';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('DeploymentTables', () => {
|
||||
it('should render 2 deployments', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<DeploymentTables deploymentTriple={twoDeployFixture as any} />,
|
||||
),
|
||||
);
|
||||
|
||||
// title
|
||||
expect(getByText('dice-roller')).toBeInTheDocument();
|
||||
expect(getByText('dice-roller-canary')).toBeInTheDocument();
|
||||
|
||||
// pod names
|
||||
expect(getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('dice-roller-canary-7d64cd756c-55rfq'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { Chip, Grid } from '@material-ui/core';
|
||||
import {
|
||||
StatusAborted,
|
||||
StatusError,
|
||||
StatusOK,
|
||||
SubvalueCell,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
V1ComponentCondition,
|
||||
V1Deployment,
|
||||
V1Pod,
|
||||
V1ReplicaSet,
|
||||
} from '@kubernetes/client-node';
|
||||
import { V1OwnerReference } from '@kubernetes/client-node/dist/gen/model/v1OwnerReference';
|
||||
import { DeploymentTriple } from '../../types/types';
|
||||
|
||||
const renderCondition = (condition: V1ComponentCondition | undefined) => {
|
||||
if (!condition) {
|
||||
return <StatusAborted />;
|
||||
}
|
||||
|
||||
const status = condition.status;
|
||||
|
||||
if (status === 'True') {
|
||||
return <StatusOK />;
|
||||
} else if (status === 'False') {
|
||||
return <StatusError />;
|
||||
}
|
||||
return <StatusAborted />;
|
||||
};
|
||||
|
||||
const columns: TableColumn<V1Pod>[] = [
|
||||
{
|
||||
title: 'name',
|
||||
highlight: true,
|
||||
width: '20%',
|
||||
render: (pod: V1Pod) => pod.metadata?.name ?? 'un-named pod',
|
||||
},
|
||||
{
|
||||
title: 'images',
|
||||
width: '20%',
|
||||
render: (pod: V1Pod) => {
|
||||
const containerStatuses = pod.status?.containerStatuses ?? [];
|
||||
return containerStatuses.map((cs, i) => {
|
||||
return <Chip key={i} label={`${cs.name}=${cs.image}`} size="small" />;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'phase',
|
||||
render: (pod: V1Pod) => pod.status?.phase ?? 'unknown',
|
||||
},
|
||||
{
|
||||
title: 'containers ready',
|
||||
align: 'center',
|
||||
render: (pod: V1Pod) => {
|
||||
const containerStatuses = pod.status?.containerStatuses ?? [];
|
||||
const containersReady = containerStatuses.filter(cs => cs.ready).length;
|
||||
|
||||
return `${containersReady}/${containerStatuses.length}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'total restarts',
|
||||
render: (pod: V1Pod) => {
|
||||
const containerStatuses = pod.status?.containerStatuses ?? [];
|
||||
return containerStatuses?.reduce((a, b) => a + b.restartCount, 0);
|
||||
},
|
||||
type: 'numeric',
|
||||
},
|
||||
{
|
||||
title: 'status',
|
||||
width: '20%',
|
||||
render: (pod: V1Pod) => {
|
||||
const containerStatuses = pod.status?.containerStatuses ?? [];
|
||||
const errors = containerStatuses.reduce((accum, next) => {
|
||||
if (next.state === undefined) {
|
||||
return accum;
|
||||
}
|
||||
|
||||
const waiting = next.state.waiting;
|
||||
const terminated = next.state.terminated;
|
||||
|
||||
const renderCell = (reason: string | undefined) => (
|
||||
<Fragment key={`${pod.metadata?.name}-${next.name}`}>
|
||||
<SubvalueCell
|
||||
value={<StatusError>Container: {next.name}</StatusError>}
|
||||
subvalue={reason}
|
||||
/>
|
||||
<br />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
if (waiting) {
|
||||
accum.push(renderCell(waiting.reason));
|
||||
}
|
||||
|
||||
if (terminated) {
|
||||
accum.push(renderCell(terminated.reason));
|
||||
}
|
||||
|
||||
return accum;
|
||||
}, [] as React.ReactNode[]);
|
||||
|
||||
if (errors.length === 0) {
|
||||
return <StatusOK>OK</StatusOK>;
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Pod Initialized',
|
||||
align: 'center',
|
||||
render: (pod: V1Pod) => {
|
||||
const conditions = pod.status?.conditions ?? [];
|
||||
return renderCondition(conditions.find(c => c.type === 'Initialized'));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Pod Ready',
|
||||
align: 'center',
|
||||
render: (pod: V1Pod) => {
|
||||
const conditions = pod.status?.conditions ?? [];
|
||||
return renderCondition(conditions.find(c => c.type === 'Ready'));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Containers Ready',
|
||||
align: 'center',
|
||||
render: (pod: V1Pod) => {
|
||||
const conditions = pod.status?.conditions ?? [];
|
||||
return renderCondition(
|
||||
conditions.find(c => c.type === 'ContainersReady'),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Pod Scheduled',
|
||||
align: 'center',
|
||||
render: (pod: V1Pod) => {
|
||||
const conditions = pod.status?.conditions ?? [];
|
||||
return renderCondition(conditions.find(c => c.type === 'PodScheduled'));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type DeploymentTablesProps = {
|
||||
deploymentTriple: DeploymentTriple;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DeploymentTables = ({
|
||||
deploymentTriple,
|
||||
}: DeploymentTablesProps) => {
|
||||
const isOwnedBy = (
|
||||
ownerReferences: V1OwnerReference[],
|
||||
obj: V1Pod | V1ReplicaSet | V1Deployment,
|
||||
): boolean => {
|
||||
return ownerReferences?.some(or => or.name === obj.metadata?.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{deploymentTriple.deployments.map((deployment, i) => (
|
||||
<Grid container item key={i} xs>
|
||||
{deploymentTriple.replicaSets
|
||||
// Filter out replica sets with no replicas
|
||||
.filter(rs => rs.status && rs.status.replicas > 0)
|
||||
// Find the replica sets this deployment owns
|
||||
.filter(rs =>
|
||||
isOwnedBy(rs.metadata?.ownerReferences ?? [], deployment),
|
||||
)
|
||||
.map((rs, j) => {
|
||||
// Find the pods this replica set owns and render them in the table
|
||||
const ownedPods = deploymentTriple.pods.filter(pod =>
|
||||
isOwnedBy(pod.metadata?.ownerReferences ?? [], rs),
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid item key={j} xs>
|
||||
<Table
|
||||
options={{ paging: false, padding: 'dense', search: false }}
|
||||
data={ownedPods}
|
||||
columns={columns}
|
||||
title={deployment.metadata?.name ?? ''}
|
||||
subtitle="Deployment"
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { DeploymentTables } from './DeploymentTables';
|
||||
@@ -14,17 +14,57 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { InfoCard, Page, pageTheme, Content, useApi } from '@backstage/core';
|
||||
import {
|
||||
Content,
|
||||
InfoCard,
|
||||
Page,
|
||||
pageTheme,
|
||||
Progress,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { kubernetesApiRef } from '../../api/types';
|
||||
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
|
||||
import {
|
||||
FetchResponse,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { DeploymentTables } from '../DeploymentTables';
|
||||
import { DeploymentTriple } from '../../types/types';
|
||||
|
||||
// TODO this is a temporary component used to construct the Kubernetes plugin boilerplate
|
||||
const findDeployments = (fetchResponse: FetchResponse[]): DeploymentTriple => {
|
||||
return fetchResponse.reduce(
|
||||
(prev, next) => {
|
||||
switch (next.type) {
|
||||
case 'deployments':
|
||||
prev.deployments.push(...next.resources);
|
||||
break;
|
||||
case 'pods':
|
||||
prev.pods.push(...next.resources);
|
||||
break;
|
||||
case 'replicasets':
|
||||
prev.replicaSets.push(...next.resources);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
{
|
||||
pods: [],
|
||||
replicaSets: [],
|
||||
deployments: [],
|
||||
} as DeploymentTriple,
|
||||
);
|
||||
};
|
||||
|
||||
export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
// TODO proper error handling
|
||||
|
||||
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
|
||||
|
||||
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
const kubernetesApi = useApi(kubernetesApiRef);
|
||||
|
||||
const [kubernetesObjects, setKubernetesObjects] = useState<
|
||||
ObjectsByServiceIdResponse | undefined
|
||||
>(undefined);
|
||||
@@ -45,27 +85,17 @@ export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => {
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Content>
|
||||
<Grid container spacing={3} direction="column">
|
||||
{kubernetesObjects === undefined && <div>loading....</div>}
|
||||
{kubernetesObjects === undefined && <Progress />}
|
||||
{error !== undefined && <div>{error}</div>}
|
||||
{kubernetesObjects !== undefined && (
|
||||
<div>
|
||||
{kubernetesObjects.items.map((item, i) => (
|
||||
<Grid item key={i}>
|
||||
<InfoCard key={item.cluster.name} title={item.cluster.name}>
|
||||
{item.resources.map((fr, j) => (
|
||||
<div key={j}>
|
||||
<br />
|
||||
{fr.type}:{' '}
|
||||
{(fr.resources as any)
|
||||
.map((v: any) => v.metadata.name)
|
||||
.join(' ')}
|
||||
</div>
|
||||
))}
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{kubernetesObjects?.items.map((item, i) => (
|
||||
<Grid item key={i}>
|
||||
<InfoCard title={item.cluster.name} subheader="Cluster">
|
||||
<DeploymentTables
|
||||
deploymentTriple={findDeployments(item.resources)}
|
||||
/>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { V1Deployment, V1Pod, V1ReplicaSet } from '@kubernetes/client-node';
|
||||
|
||||
export interface DeploymentTriple {
|
||||
pods: V1Pod[];
|
||||
replicaSets: V1ReplicaSet[];
|
||||
deployments: V1Deployment[];
|
||||
}
|
||||
Reference in New Issue
Block a user