gracefully surface FetchErrors
Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user