gracefully surface FetchErrors

Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
Jamie Klassen
2022-11-30 13:17:40 -05:00
parent fcf8d33014
commit 0ad476a720
5 changed files with 141 additions and 10 deletions
@@ -20,8 +20,14 @@ import {
CustomResource,
FetchResponseWrapper,
ObjectFetchParams,
KubernetesServiceLocator,
} from '../types/types';
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
const fetchObjectsForService = jest.fn();
const fetchPodMetricsByNamespaces = jest.fn();
@@ -751,6 +757,101 @@ describe('getKubernetesObjectsByEntity', () => {
],
});
});
describe('with a real fetcher', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
it('fetch error short-circuits requests to a single cluster, recovering across the fleet', async () => {
const pods = [{ metadata: { name: 'pod-name' } }];
const services = [{ metadata: { name: 'service-name' } }];
worker.use(
rest.get('https://works/api/v1/pods', (_, res, ctx) =>
res(ctx.json({ items: pods })),
),
rest.get('https://works/api/v1/services', (_, res, ctx) =>
res(ctx.json({ items: services })),
),
rest.get('https://fails/api/v1/pods', (_, res) =>
res.networkError('socket error'),
),
rest.get('https://fails/api/v1/services', (_, res, ctx) =>
res(ctx.json({ items: services })),
),
);
const fleet: jest.Mocked<KubernetesServiceLocator> = {
getClustersByEntity: jest.fn().mockResolvedValue({
clusters: [
{
name: 'works',
url: 'https://works',
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
skipMetricsLookup: true,
},
{
name: 'fails',
url: 'https://fails',
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
skipMetricsLookup: true,
},
],
}),
};
const logger = getVoidLogger();
const sut = new KubernetesFanOutHandler({
logger,
fetcher: new KubernetesClientBasedFetcher({ logger }),
serviceLocator: fleet,
customResources: [],
objectTypesToFetch: [
{
group: '',
apiVersion: 'v1',
plural: 'pods',
objectType: 'pods',
},
{
group: '',
apiVersion: 'v1',
plural: 'services',
objectType: 'services',
},
],
});
const result = await sut.getKubernetesObjectsByEntity({
entity,
auth: {},
});
const expected: ObjectsByEntityResponse = {
items: [
{
cluster: { name: 'works' },
resources: [
{ type: 'pods', resources: pods },
{ type: 'services', resources: services },
],
podMetrics: [],
errors: [],
},
{
cluster: { name: 'fails' },
resources: [],
podMetrics: [],
errors: [
{
errorType: 'FETCH_ERROR',
message:
'request to https://fails/api/v1/pods?labelSelector=backstage.io/kubernetes-id=test-component failed, reason: socket error',
},
],
},
],
};
expect(result).toStrictEqual(expected);
});
});
});
describe('getCustomResourcesByEntity', () => {
@@ -274,6 +274,15 @@ export class KubernetesFanOutHandler {
namespace,
})
.then(result => this.getMetricsForPods(clusterDetailsItem, result))
.catch((e): responseWithMetrics => {
return [
{
errors: [{ errorType: 'FETCH_ERROR', message: e.message }],
responses: [],
},
[],
];
})
.then(r => this.toClusterObjects(clusterDetailsItem, r));
}),
).then(this.toObjectsByEntityResponse);
+19 -8
View File
@@ -184,14 +184,7 @@ export type KubernetesErrorTypes =
| 'UNKNOWN_ERROR';
// @public (undocumented)
export interface KubernetesFetchError {
// (undocumented)
errorType: KubernetesErrorTypes;
// (undocumented)
resourcePath?: string;
// (undocumented)
statusCode?: number;
}
export type KubernetesFetchError = StatusError | RawFetchError;
// @public (undocumented)
export interface KubernetesRequestAuth {
@@ -241,6 +234,14 @@ export interface PodStatusFetchResponse {
type: 'podstatus';
}
// @public (undocumented)
export interface RawFetchError {
// (undocumented)
errorType: 'FETCH_ERROR';
// (undocumented)
message: string;
}
// @public (undocumented)
export interface ReplicaSetsFetchResponse {
// (undocumented)
@@ -265,6 +266,16 @@ export interface StatefulSetsFetchResponse {
type: 'statefulsets';
}
// @public (undocumented)
export interface StatusError {
// (undocumented)
errorType: KubernetesErrorTypes;
// (undocumented)
resourcePath?: string;
// (undocumented)
statusCode?: number;
}
// @public (undocumented)
export interface WorkloadsByEntityRequest {
// (undocumented)
+10 -1
View File
@@ -223,12 +223,21 @@ export interface PodStatusFetchResponse {
}
/** @public */
export interface KubernetesFetchError {
export type KubernetesFetchError = StatusError | RawFetchError;
/** @public */
export interface StatusError {
errorType: KubernetesErrorTypes;
statusCode?: number;
resourcePath?: string;
}
/** @public */
export interface RawFetchError {
errorType: 'FETCH_ERROR';
message: string;
}
/** @public */
export type KubernetesErrorTypes =
| 'BAD_REQUEST'
@@ -29,7 +29,8 @@ const clustersWithErrorsToErrorMessage = (
{c.errors.map((e, j) => {
return (
<Typography variant="body2" key={j}>
{`Error fetching Kubernetes resource: '${e.resourcePath}', error: ${e.errorType}, status code: ${e.statusCode}`}
{e.errorType !== 'FETCH_ERROR' &&
`Error fetching Kubernetes resource: '${e.resourcePath}', error: ${e.errorType}, status code: ${e.statusCode}`}
</Typography>
);
})}