add better error handling (#2664)

This commit is contained in:
Matthew Clarke
2020-09-30 16:07:30 +01:00
committed by GitHub
parent 1fb8f1a93c
commit 44e18975bd
10 changed files with 503 additions and 100 deletions
@@ -0,0 +1,83 @@
/*
* 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 { wrapInTestApp } from '@backstage/test-utils';
import { ErrorPanel } from './ErrorPanel';
describe('ErrorPanel', () => {
it('render with error message', async () => {
const { getByText } = render(
wrapInTestApp(
<ErrorPanel
entityName="THIS_ENTITY"
errorMessage="SOME_ERROR_MESSAGE"
/>,
),
);
// title
expect(
getByText(
'There was an error retrieving some Kubernetes resources for the entity: THIS_ENTITY',
),
).toBeInTheDocument();
// message
expect(getByText('Errors: SOME_ERROR_MESSAGE')).toBeInTheDocument();
});
it('render with cluster errors', async () => {
const { getByText } = render(
wrapInTestApp(
<ErrorPanel
entityName="THIS_ENTITY"
clustersWithErrors={[
{
cluster: {
name: 'THIS_CLUSTER',
},
resources: [],
errors: [
{
errorType: 'SYSTEM_ERROR',
statusCode: 500,
resourcePath: 'some/resource',
},
],
},
]}
/>,
),
);
// title
expect(
getByText(
'There was an error retrieving some Kubernetes resources for the entity: THIS_ENTITY',
),
).toBeInTheDocument();
// message
expect(getByText('Errors:')).toBeInTheDocument();
expect(getByText('Cluster: THIS_CLUSTER')).toBeInTheDocument();
expect(
getByText(
"Error fetching Kubernetes resource: 'some/resource', error: SYSTEM_ERROR",
),
).toBeInTheDocument();
});
});
@@ -0,0 +1,65 @@
/*
* 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 { WarningPanel } from '@backstage/core';
import { Typography } from '@material-ui/core';
import { ClusterObjects } from '../../../../kubernetes-backend/src';
const clustersWithErrorsToErrorMessage = (
clustersWithErrors: ClusterObjects[],
): React.ReactNode => {
return clustersWithErrors.map((c, i) => {
return (
<div key={i}>
<Typography variant="body2">{`Cluster: ${c.cluster.name}`}</Typography>
{c.errors.map((e, j) => {
return (
<Typography variant="body2" key={j}>
{`Error fetching Kubernetes resource: '${e.resourcePath}', error: ${e.errorType}`}
</Typography>
);
})}
<br />
</div>
);
});
};
type ErrorPanelProps = {
entityName: string;
errorMessage?: string;
clustersWithErrors?: ClusterObjects[];
children?: React.ReactNode;
};
export const ErrorPanel = ({
entityName,
errorMessage,
clustersWithErrors,
}: ErrorPanelProps) => (
<WarningPanel
title="There was an error retrieving kubernetes objects"
message={`There was an error retrieving some Kubernetes resources for the entity: ${entityName}`}
>
{clustersWithErrors && (
<div>Errors: {clustersWithErrorsToErrorMessage(clustersWithErrors)}</div>
)}
{errorMessage && (
<Typography variant="body2">Errors: {errorMessage}</Typography>
)}
</WarningPanel>
);
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
import { Grid } from '@material-ui/core';
import React, { ReactElement, useEffect, useState } from 'react';
import { Grid, TabProps } from '@material-ui/core';
import {
CardTab,
Content,
@@ -44,6 +44,7 @@ import { Services } from '../Services';
import { ConfigMaps } from '../ConfigMaps';
import { Ingresses } from '../Ingresses';
import { HorizontalPodAutoscalers } from '../HorizontalPodAutoscalers';
import { ErrorPanel } from './ErrorPanel';
interface GroupedResponses extends DeploymentTriple {
services: V1Service[];
@@ -94,8 +95,6 @@ const groupResponses = (fetchResponse: FetchResponse[]) => {
);
};
// TODO proper error handling
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
@@ -117,12 +116,33 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
});
}, [entity.metadata.name, kubernetesApi]);
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
return (
<Page theme={pageTheme.tool}>
<Content>
<Grid container spacing={3} direction="column">
{kubernetesObjects === undefined && <Progress />}
{error !== undefined && <div>{error}</div>}
{kubernetesObjects === undefined && error === undefined && (
<Progress />
)}
{/* errors retrieved from the kubernetes clusters */}
{clustersWithErrors.length > 0 && (
<ErrorPanel
entityName={entity.metadata.name}
clustersWithErrors={clustersWithErrors}
/>
)}
{/* other errors */}
{error !== undefined && (
<ErrorPanel
entityName={entity.metadata.name}
errorMessage={error}
/>
)}
{kubernetesObjects?.items.map((item, i) => (
<Grid item key={i}>
<Cluster clusterObjects={item} />
@@ -151,6 +171,43 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
const hpas = groupedResponses.horizontalPodAutoscalers;
const ingresses = groupedResponses.ingresses;
const tabs: ReactElement<TabProps>[] = [
<CardTab key={1} value="one" label="Deployments">
<DeploymentTables
deploymentTriple={{
deployments: groupedResponses.deployments,
replicaSets: groupedResponses.replicaSets,
pods: groupedResponses.pods,
}}
/>
</CardTab>,
<CardTab key={2} value="two" label="Services">
<Services services={groupedResponses.services} />
</CardTab>,
];
if (configMaps.length > 0) {
tabs.push(
<CardTab key={3} value="three" label="Config Maps">
<ConfigMaps configMaps={configMaps} />
</CardTab>,
);
}
if (hpas.length > 0) {
tabs.push(
<CardTab key={4} value="four" label="Horizontal Pod Autoscalers">
<HorizontalPodAutoscalers hpas={hpas} />
</CardTab>,
);
}
if (ingresses.length > 0) {
tabs.push(
<CardTab key={5} value="five" label="Ingresses">
<Ingresses ingresses={ingresses} />
</CardTab>,
);
}
return (
<>
<TabbedCard
@@ -158,33 +215,7 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
onChange={handleChange}
title={clusterObjects.cluster.name}
>
<CardTab value="one" label="Deployments">
<DeploymentTables
deploymentTriple={{
deployments: groupedResponses.deployments,
replicaSets: groupedResponses.replicaSets,
pods: groupedResponses.pods,
}}
/>
</CardTab>
<CardTab value="two" label="Services">
<Services services={groupedResponses.services} />
</CardTab>
{configMaps && (
<CardTab value="three" label="Config Maps">
<ConfigMaps configMaps={configMaps} />
</CardTab>
)}
{hpas && (
<CardTab value="four" label="Horizontal Pod Autoscalers">
<HorizontalPodAutoscalers hpas={hpas} />
</CardTab>
)}
{ingresses && (
<CardTab value="five" label="Ingresses">
<Ingresses ingresses={ingresses} />
</CardTab>
)}
{tabs}
</TabbedCard>
</>
);