refactor and refresh k8s plugin at interval

Signed-off-by: mclarke47 <matthewclarke47@gmail.com>
This commit is contained in:
mclarke47
2021-10-19 19:49:45 +01:00
parent 8674bbd009
commit eaa78edcbc
9 changed files with 259 additions and 163 deletions
@@ -0,0 +1,60 @@
/*
* Copyright 2021 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 { wrapInTestApp } from '@backstage/test-utils';
import { Cluster } from './Cluster';
jest.mock('../../hooks');
import * as oneDeployment from '../../__fixtures__/1-deployments.json';
describe('Cluster', () => {
it('render 1 cluster', async () => {
const { getByText } = render(
wrapInTestApp(
<Cluster
{...({
clusterObjects: {
cluster: {
name: 'cluster-1',
},
resources: [
{
type: 'deployments',
resources: oneDeployment.deployments,
},
{
type: 'replicasets',
resources: oneDeployment.replicaSets,
},
{
type: 'pods',
resources: oneDeployment.pods,
},
],
errors: [],
},
podsWithErrors: new Set<string>(),
} as any)}
/>,
),
);
expect(getByText('cluster-1')).toBeInTheDocument();
expect(getByText('10 pods')).toBeInTheDocument();
});
});
@@ -23,32 +23,20 @@ import {
Grid,
Typography,
} from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import { ClusterObjects } from '@backstage/plugin-kubernetes-common';
import { ErrorPanel } from './ErrorPanel';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { DeploymentsAccordions } from '../DeploymentsAccordions';
import { ErrorReporting } from '../ErrorReporting';
import { groupResponses } from '../../utils/response';
import { DetectedError, detectErrors } from '../../error-detection';
import { IngressesAccordions } from '../IngressesAccordions';
import { ServicesAccordions } from '../ServicesAccordions';
import { CustomResources } from '../CustomResources';
import EmptyStateImage from '../../assets/emptystate.svg';
import {
ClusterContext,
GroupedResponsesContext,
PodNamesWithErrorsContext,
useKubernetesObjects,
} from '../../hooks';
import {
Content,
Page,
Progress,
StatusError,
StatusOK,
} from '@backstage/core-components';
import { StatusError, StatusOK } from '@backstage/core-components';
type ClusterSummaryProps = {
clusterName: string;
@@ -117,7 +105,7 @@ type ClusterProps = {
children?: React.ReactNode;
};
const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
const groupedResponses = groupResponses(clusterObjects.resources);
return (
<ClusterContext.Provider value={clusterObjects.cluster}>
@@ -153,107 +141,3 @@ const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
</ClusterContext.Provider>
);
};
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(entity);
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
const detectedErrors =
kubernetesObjects !== undefined
? detectErrors(kubernetesObjects)
: new Map<string, DetectedError[]>();
return (
<Page themeId="tool">
<Content>
{kubernetesObjects === undefined && error === undefined && <Progress />}
{/* errors retrieved from the kubernetes clusters */}
{clustersWithErrors.length > 0 && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
clustersWithErrors={clustersWithErrors}
/>
</Grid>
</Grid>
)}
{/* other errors */}
{error !== undefined && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
errorMessage={error}
/>
</Grid>
</Grid>
)}
{kubernetesObjects && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorReporting detectedErrors={detectedErrors} />
</Grid>
<Grid item>
<Divider />
</Grid>
<Grid item>
<Typography variant="h3">Your Clusters</Typography>
</Grid>
<Grid item container>
{kubernetesObjects?.items.length <= 0 && (
<Grid
container
justifyContent="space-around"
direction="row"
alignItems="center"
spacing={2}
>
<Grid item xs={4}>
<Typography variant="h5">
No resources on any known clusters for{' '}
{entity.metadata.name}
</Typography>
</Grid>
<Grid item xs={4}>
<img
src={EmptyStateImage}
alt="EmptyState"
data-testid="emptyStateImg"
/>
</Grid>
</Grid>
)}
{kubernetesObjects?.items.length > 0 &&
kubernetesObjects?.items.map((item, i) => {
const podsWithErrors = new Set<string>(
detectedErrors
.get(item.cluster.name)
?.filter(de => de.kind === 'Pod')
.map(de => de.names)
.flat() ?? [],
);
return (
<Grid item key={i} xs={12}>
<Cluster
clusterObjects={item}
podsWithErrors={podsWithErrors}
/>
</Grid>
);
})}
</Grid>
</Grid>
)}
</Content>
</Page>
);
};
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { KubernetesContent } from './KubernetesContent';
export { Cluster } from './Cluster';
@@ -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 { ErrorPanel } from './ErrorPanel';
@@ -18,11 +18,11 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { KubernetesContent } from './KubernetesContent';
import { useKubernetesObjects } from '../../hooks';
import { useKubernetesObjects } from '../hooks';
jest.mock('../../hooks');
import * as oneDeployment from '../../__fixtures__/1-deployments.json';
import * as twoDeployments from '../../__fixtures__/2-deployments.json';
jest.mock('../hooks');
import * as oneDeployment from '../__fixtures__/1-deployments.json';
import * as twoDeployments from '../__fixtures__/2-deployments.json';
describe('KubernetesContent', () => {
it('render empty response', async () => {
@@ -0,0 +1,130 @@
/*
* Copyright 2021 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 { Divider, Grid, Typography } from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import { ErrorPanel } from './ErrorPanel';
import { ErrorReporting } from './ErrorReporting';
import { DetectedError, detectErrors } from '../error-detection';
import { Cluster } from './Cluster';
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 };
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
const { kubernetesObjects, error } = useKubernetesObjects(entity);
const clustersWithErrors =
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
const detectedErrors =
kubernetesObjects !== undefined
? detectErrors(kubernetesObjects)
: new Map<string, DetectedError[]>();
return (
<Page themeId="tool">
<Content>
{kubernetesObjects === undefined && error === undefined && <Progress />}
{/* errors retrieved from the kubernetes clusters */}
{clustersWithErrors.length > 0 && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
clustersWithErrors={clustersWithErrors}
/>
</Grid>
</Grid>
)}
{/* other errors */}
{error !== undefined && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorPanel
entityName={entity.metadata.name}
errorMessage={error}
/>
</Grid>
</Grid>
)}
{kubernetesObjects && (
<Grid container spacing={3} direction="column">
<Grid item>
<ErrorReporting detectedErrors={detectedErrors} />
</Grid>
<Grid item>
<Divider />
</Grid>
<Grid item>
<Typography variant="h3">Your Clusters</Typography>
</Grid>
<Grid item container>
{kubernetesObjects?.items.length <= 0 && (
<Grid
container
justifyContent="space-around"
direction="row"
alignItems="center"
spacing={2}
>
<Grid item xs={4}>
<Typography variant="h5">
No resources on any known clusters for{' '}
{entity.metadata.name}
</Typography>
</Grid>
<Grid item xs={4}>
<img
src={EmptyStateImage}
alt="EmptyState"
data-testid="emptyStateImg"
/>
</Grid>
</Grid>
)}
{kubernetesObjects?.items.length > 0 &&
kubernetesObjects?.items.map((item, i) => {
const podsWithErrors = new Set<string>(
detectedErrors
.get(item.cluster.name)
?.filter(de => de.kind === 'Pod')
.map(de => de.names)
.flat() ?? [],
);
return (
<Grid item key={i} xs={12}>
<Cluster
clusterObjects={item}
podsWithErrors={podsWithErrors}
/>
</Grid>
);
})}
</Grid>
</Grid>
)}
</Content>
</Page>
);
};
@@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model';
import { kubernetesApiRef } from '../api/types';
import { kubernetesAuthProvidersApiRef } from '../kubernetes-auth-provider/types';
import { useEffect, useState } from 'react';
import { useInterval } from 'react-use';
import {
KubernetesRequestBody,
ObjectsByEntityResponse,
@@ -38,51 +39,56 @@ export const useKubernetesObjects = (entity: Entity): KubernetesObjects => {
const [error, setError] = useState<string | undefined>(undefined);
const getObjects = async () => {
let clusters = [];
try {
clusters = await kubernetesApi.getClusters();
} catch (e) {
setError(e.message);
return;
}
const authProviders: string[] = [
...new Set(clusters.map(c => c.authProvider)),
];
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
let requestBody: KubernetesRequestBody = {
entity,
};
for (const authProviderStr of authProviders) {
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
try {
requestBody =
await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
authProviderStr,
requestBody,
);
} catch (e) {
setError(e.message);
return;
}
}
try {
setKubernetesObjects(await kubernetesApi.getObjectsByEntity(requestBody));
} catch (e) {
setError(e.message);
return;
}
};
useEffect(() => {
(async () => {
let clusters = [];
try {
clusters = await kubernetesApi.getClusters();
} catch (e) {
setError(e.message);
return;
}
const authProviders: string[] = [
...new Set(clusters.map(c => c.authProvider)),
];
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
let requestBody: KubernetesRequestBody = {
entity,
};
for (const authProviderStr of authProviders) {
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
try {
requestBody =
await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
authProviderStr,
requestBody,
);
} catch (e) {
setError(e.message);
return;
}
}
try {
setKubernetesObjects(
await kubernetesApi.getObjectsByEntity(requestBody),
);
} catch (e) {
setError(e.message);
return;
}
})();
getObjects();
/* eslint-disable react-hooks/exhaustive-deps */
}, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);
/* eslint-enable react-hooks/exhaustive-deps */
useInterval(() => {
getObjects();
// TODO make configurable
}, 10000);
return {
kubernetesObjects,
error,