From cc9749a88e228b44221edca31d198283d3921a54 Mon Sep 17 00:00:00 2001 From: Mengnan Gong Date: Sun, 9 Oct 2022 21:47:13 +0800 Subject: [PATCH] add unit tests fetching pod metrics and clean up code Signed-off-by: Mengnan Gong --- .changeset/wet-cameras-call.md | 6 +- .../service/KubernetesFanOutHandler.test.ts | 76 ++ .../src/service/KubernetesFanOutHandler.ts | 2 +- .../src/service/KubernetesFetcher.test.ts | 910 ++++++++++-------- .../src/service/KubernetesFetcher.ts | 8 +- plugins/kubernetes-common/api-report.md | 1 + plugins/kubernetes-common/src/types.ts | 1 + 7 files changed, 597 insertions(+), 407 deletions(-) diff --git a/.changeset/wet-cameras-call.md b/.changeset/wet-cameras-call.md index 5524e23d15..3d58719269 100644 --- a/.changeset/wet-cameras-call.md +++ b/.changeset/wet-cameras-call.md @@ -1,8 +1,10 @@ --- '@backstage/plugin-kubernetes-backend': minor -'@backstage/plugin-kubernetes-common': minor +'@backstage/plugin-kubernetes-common': patch --- The Kubernetes errors when fetching pod metrics are now captured and returned to the frontend. -Include the `PodStatusFetchResponse` into the `FetchResponse` union type. Now the method `fetchPodMetricsByNamespaces` accepts a set of namespaces and returns a `FetchResponseWrapper`, just like other public fetch methods in the fetcher. +- **BREAKING** The method `fetchPodMetricsByNamespace` in the interface `KubernetesFetcher` is changed to `fetchPodMetricsByNamespaces`. It now accepts a set of namespace strings and returns `Promise`. +- Add the `PodStatusFetchResponse` to the `FetchResponse` union type. +- Add `NOT_FOUND` to the `KubernetesErrorTypes` union type, the HTTP error with status code 404 will be mapped to this error. diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 8627795a7f..1af5371e53 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -675,6 +675,82 @@ describe('getKubernetesObjectsByEntity', () => { ], }); }); + it('retrieve objects for two clusters, one fails to fetch pod metrics', async () => { + getClustersByEntity.mockImplementation(() => + Promise.resolve({ + clusters: [ + { + name: 'test-cluster', + authProvider: 'serviceAccount', + dashboardUrl: 'https://k8s.foo.coom', + }, + { + name: 'other-cluster', + authProvider: 'google', + }, + ], + }), + ); + + mockFetch(fetchObjectsForService); + + // To simulate the partial failure, return a valid response for the first call, + // and an error for the second call. + fetchPodMetricsByNamespaces + .mockImplementationOnce( + (clusterDetails: ClusterDetails, namespaces: Set) => + Promise.resolve(generatePodStatus(clusterDetails.name, namespaces)), + ) + .mockResolvedValueOnce({ + errors: [ + { + errorType: 'NOT_FOUND', + resourcePath: '/some/path', + statusCode: 404, + }, + ], + responses: [], + }); + + const sut = getKubernetesFanOutHandler([]); + + const result = await sut.getKubernetesObjectsByEntity({ + entity, + auth: { + google: 'google_token_123', + }, + }); + + expect(getClustersByEntity.mock.calls.length).toBe(1); + expect(fetchObjectsForService.mock.calls.length).toBe(2); + expect(result).toStrictEqual({ + items: [ + { + cluster: { + dashboardUrl: 'https://k8s.foo.coom', + name: 'test-cluster', + }, + errors: [], + podMetrics: [POD_METRICS_FIXTURE], + resources: resourcesByCluster('test-cluster'), + }, + { + cluster: { + name: 'other-cluster', + }, + errors: [ + { + errorType: 'NOT_FOUND', + resourcePath: '/some/path', + statusCode: 404, + }, + ], + podMetrics: [], + resources: resourcesByCluster('other-cluster'), + }, + ], + }); + }); }); describe('getCustomResourcesByEntity', () => { diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 7de3e70249..330931f580 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -358,7 +358,7 @@ export class KubernetesFanOutHandler { namespaces, ); - result.errors.concat(podMetrics.errors); + result.errors.push(...podMetrics.errors); return [result, podMetrics.responses as PodStatusFetchResponse[]]; } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 25a5c9312b..c2560e58b0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -17,6 +17,12 @@ import { getVoidLogger } from '@backstage/backend-common'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { ObjectToFetch } from '../types/types'; +import { topPods } from '@kubernetes/client-node'; + +jest.mock('@kubernetes/client-node', () => ({ + ...jest.requireActual('@kubernetes/client-node'), + topPods: jest.fn(), +})); const OBJECTS_TO_FETCH = new Set([ { @@ -33,63 +39,52 @@ const OBJECTS_TO_FETCH = new Set([ }, ]); +const POD_METRICS_FIXTURE = { + containers: [], + cpu: { + currentUsage: 100, + limitTotal: 102, + requestTotal: 101, + }, + memory: { + currentUsage: '1000', + limitTotal: '1002', + requestTotal: '1001', + }, + pod: {}, +}; + describe('KubernetesFetcher', () => { - let clientMock: any; - let kubernetesClientProvider: any; - let sut: KubernetesClientBasedFetcher; + describe('fetchObjectsForService', () => { + let clientMock: any; + let kubernetesClientProvider: any; + let sut: KubernetesClientBasedFetcher; - beforeEach(() => { - jest.resetAllMocks(); - clientMock = { - listClusterCustomObject: jest.fn(), - listNamespacedCustomObject: jest.fn(), - addInterceptor: jest.fn(), - }; + beforeEach(() => { + jest.resetAllMocks(); + clientMock = { + listClusterCustomObject: jest.fn(), + listNamespacedCustomObject: jest.fn(), + addInterceptor: jest.fn(), + }; - kubernetesClientProvider = { - getCustomObjectsClient: jest.fn(() => clientMock), - }; + kubernetesClientProvider = { + getCustomObjectsClient: jest.fn(() => clientMock), + }; - sut = new KubernetesClientBasedFetcher({ - kubernetesClientProvider, - logger: getVoidLogger(), - }); - }); - - const testErrorResponse = async (errorResponse: any, expectedResult: any) => { - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ - { - metadata: { - name: 'pod-name', - }, - }, - ], - }, + sut = new KubernetesClientBasedFetcher({ + kubernetesClientProvider, + logger: getVoidLogger(), + }); }); - clientMock.listClusterCustomObject.mockRejectedValue(errorResponse); - - const result = await sut.fetchObjectsForService({ - serviceId: 'some-service', - clusterDetails: { - name: 'cluster1', - url: 'http://localhost:9999', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - }, - objectTypesToFetch: OBJECTS_TO_FETCH, - labelSelector: '', - customResources: [], - }); - - expect(result).toStrictEqual({ - errors: [expectedResult], - responses: [ - { - type: 'pods', - resources: [ + const testErrorResponse = async ( + errorResponse: any, + expectedResult: any, + ) => { + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ { metadata: { name: 'pod-name', @@ -97,82 +92,72 @@ describe('KubernetesFetcher', () => { }, ], }, - ], - }); + }); - expect(clientMock.listClusterCustomObject.mock.calls.length).toBe(2); + clientMock.listClusterCustomObject.mockRejectedValue(errorResponse); - expect(clientMock.listClusterCustomObject.mock.calls[0]).toEqual([ - '', - 'v1', - 'pods', - '', - false, - '', - '', - 'backstage.io/kubernetes-id=some-service', - ]); + const result = await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + objectTypesToFetch: OBJECTS_TO_FETCH, + labelSelector: '', + customResources: [], + }); - expect(clientMock.listClusterCustomObject.mock.calls[1]).toEqual([ - '', - 'v1', - 'services', - '', - false, - '', - '', - 'backstage.io/kubernetes-id=some-service', - ]); - - expect( - kubernetesClientProvider.getCustomObjectsClient.mock.calls.length, - ).toBe(2); - }; - - it('should return pods, services', async () => { - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ + expect(result).toStrictEqual({ + errors: [expectedResult], + responses: [ { - metadata: { - name: 'pod-name', - }, + type: 'pods', + resources: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], }, ], - }, - }); + }); - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ - { - metadata: { - name: 'service-name', - }, - }, - ], - }, - }); + expect(clientMock.listClusterCustomObject.mock.calls.length).toBe(2); - const result = await sut.fetchObjectsForService({ - serviceId: 'some-service', - clusterDetails: { - name: 'cluster1', - url: 'http://localhost:9999', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - }, - objectTypesToFetch: OBJECTS_TO_FETCH, - labelSelector: '', - customResources: [], - }); + expect(clientMock.listClusterCustomObject.mock.calls[0]).toEqual([ + '', + 'v1', + 'pods', + '', + false, + '', + '', + 'backstage.io/kubernetes-id=some-service', + ]); - expect(result).toStrictEqual({ - errors: [], - responses: [ - { - type: 'pods', - resources: [ + expect(clientMock.listClusterCustomObject.mock.calls[1]).toEqual([ + '', + 'v1', + 'services', + '', + false, + '', + '', + 'backstage.io/kubernetes-id=some-service', + ]); + + expect( + kubernetesClientProvider.getCustomObjectsClient.mock.calls.length, + ).toBe(2); + }; + + it('should return pods, services', async () => { + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ { metadata: { name: 'pod-name', @@ -180,9 +165,11 @@ describe('KubernetesFetcher', () => { }, ], }, - { - type: 'services', - resources: [ + }); + + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ { metadata: { name: 'service-name', @@ -190,100 +177,79 @@ describe('KubernetesFetcher', () => { }, ], }, - ], - }); + }); - expect(clientMock.listClusterCustomObject.mock.calls.length).toBe(2); - - expect(clientMock.listClusterCustomObject.mock.calls[0]).toEqual([ - '', - 'v1', - 'pods', - '', - false, - '', - '', - 'backstage.io/kubernetes-id=some-service', - ]); - - expect(clientMock.listClusterCustomObject.mock.calls[1]).toEqual([ - '', - 'v1', - 'services', - '', - false, - '', - '', - 'backstage.io/kubernetes-id=some-service', - ]); - - expect( - kubernetesClientProvider.getCustomObjectsClient.mock.calls.length, - ).toBe(2); - }); - it('should return pods, services and customobjects', async () => { - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ - { - metadata: { - name: 'pod-name', - }, - }, - ], - }, - }); - - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ - { - metadata: { - name: 'service-name', - }, - }, - ], - }, - }); - - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ - { - metadata: { - name: 'something-else', - }, - }, - ], - }, - }); - - const result = await sut.fetchObjectsForService({ - serviceId: 'some-service', - clusterDetails: { - name: 'cluster1', - url: 'http://localhost:9999', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - }, - objectTypesToFetch: OBJECTS_TO_FETCH, - labelSelector: '', - customResources: [ - { - objectType: 'customresources', - group: 'some-group', - apiVersion: 'v2', - plural: 'things', + const result = await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', }, - ], - }); + objectTypesToFetch: OBJECTS_TO_FETCH, + labelSelector: '', + customResources: [], + }); - expect(result).toStrictEqual({ - errors: [], - responses: [ - { - type: 'pods', - resources: [ + expect(result).toStrictEqual({ + errors: [], + responses: [ + { + type: 'pods', + resources: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + { + type: 'services', + resources: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + ], + }); + + expect(clientMock.listClusterCustomObject.mock.calls.length).toBe(2); + + expect(clientMock.listClusterCustomObject.mock.calls[0]).toEqual([ + '', + 'v1', + 'pods', + '', + false, + '', + '', + 'backstage.io/kubernetes-id=some-service', + ]); + + expect(clientMock.listClusterCustomObject.mock.calls[1]).toEqual([ + '', + 'v1', + 'services', + '', + false, + '', + '', + 'backstage.io/kubernetes-id=some-service', + ]); + + expect( + kubernetesClientProvider.getCustomObjectsClient.mock.calls.length, + ).toBe(2); + }); + it('should return pods, services and customobjects', async () => { + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ { metadata: { name: 'pod-name', @@ -291,9 +257,11 @@ describe('KubernetesFetcher', () => { }, ], }, - { - type: 'services', - resources: [ + }); + + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ { metadata: { name: 'service-name', @@ -301,9 +269,11 @@ describe('KubernetesFetcher', () => { }, ], }, - { - type: 'customresources', - resources: [ + }); + + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ { metadata: { name: 'something-else', @@ -311,216 +281,358 @@ describe('KubernetesFetcher', () => { }, ], }, - ], + }); + + const result = await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + objectTypesToFetch: OBJECTS_TO_FETCH, + labelSelector: '', + customResources: [ + { + objectType: 'customresources', + group: 'some-group', + apiVersion: 'v2', + plural: 'things', + }, + ], + }); + + expect(result).toStrictEqual({ + errors: [], + responses: [ + { + type: 'pods', + resources: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + { + type: 'services', + resources: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + { + type: 'customresources', + resources: [ + { + metadata: { + name: 'something-else', + }, + }, + ], + }, + ], + }); + + expect(clientMock.listClusterCustomObject.mock.calls.length).toBe(3); + + expect(clientMock.listClusterCustomObject.mock.calls[0]).toEqual([ + '', + 'v1', + 'pods', + '', + false, + '', + '', + 'backstage.io/kubernetes-id=some-service', + ]); + + expect(clientMock.listClusterCustomObject.mock.calls[1]).toEqual([ + '', + 'v1', + 'services', + '', + false, + '', + '', + 'backstage.io/kubernetes-id=some-service', + ]); + + expect(clientMock.listClusterCustomObject.mock.calls[2]).toEqual([ + 'some-group', + 'v2', + 'things', + '', + false, + '', + '', + 'backstage.io/kubernetes-id=some-service', + ]); + + expect( + kubernetesClientProvider.getCustomObjectsClient.mock.calls.length, + ).toBe(3); }); - - expect(clientMock.listClusterCustomObject.mock.calls.length).toBe(3); - - expect(clientMock.listClusterCustomObject.mock.calls[0]).toEqual([ - '', - 'v1', - 'pods', - '', - false, - '', - '', - 'backstage.io/kubernetes-id=some-service', - ]); - - expect(clientMock.listClusterCustomObject.mock.calls[1]).toEqual([ - '', - 'v1', - 'services', - '', - false, - '', - '', - 'backstage.io/kubernetes-id=some-service', - ]); - - expect(clientMock.listClusterCustomObject.mock.calls[2]).toEqual([ - 'some-group', - 'v2', - 'things', - '', - false, - '', - '', - 'backstage.io/kubernetes-id=some-service', - ]); - - expect( - kubernetesClientProvider.getCustomObjectsClient.mock.calls.length, - ).toBe(3); - }); - // they're in testErrorResponse - // eslint-disable-next-line jest/expect-expect - it('should return pods, bad request error', async () => { - await testErrorResponse( - { - response: { + // they're in testErrorResponse + // eslint-disable-next-line jest/expect-expect + it('should return pods, bad request error', async () => { + await testErrorResponse( + { + response: { + statusCode: 400, + request: { + uri: { + pathname: '/some/path', + }, + }, + }, + }, + { + errorType: 'BAD_REQUEST', + resourcePath: '/some/path', statusCode: 400, - request: { - uri: { - pathname: '/some/path', + }, + ); + }); + // they're in testErrorResponse + // eslint-disable-next-line jest/expect-expect + it('should return pods, unauthorized error', async () => { + await testErrorResponse( + { + response: { + statusCode: 401, + request: { + uri: { + pathname: '/some/path', + }, }, }, }, - }, - { - errorType: 'BAD_REQUEST', - resourcePath: '/some/path', - statusCode: 400, - }, - ); - }); - // they're in testErrorResponse - // eslint-disable-next-line jest/expect-expect - it('should return pods, unauthorized error', async () => { - await testErrorResponse( - { - response: { + { + errorType: 'UNAUTHORIZED_ERROR', + resourcePath: '/some/path', statusCode: 401, - request: { - uri: { - pathname: '/some/path', + }, + ); + }); + // they're in testErrorResponse + // eslint-disable-next-line jest/expect-expect + it('should return pods, system error', async () => { + await testErrorResponse( + { + response: { + statusCode: 500, + request: { + uri: { + pathname: '/some/path', + }, }, }, }, - }, - { - errorType: 'UNAUTHORIZED_ERROR', - resourcePath: '/some/path', - statusCode: 401, - }, - ); - }); - // they're in testErrorResponse - // eslint-disable-next-line jest/expect-expect - it('should return pods, system error', async () => { - await testErrorResponse( - { - response: { + { + errorType: 'SYSTEM_ERROR', + resourcePath: '/some/path', statusCode: 500, - request: { - uri: { - pathname: '/some/path', + }, + ); + }); + // they're in testErrorResponse + // eslint-disable-next-line jest/expect-expect + it('should return pods, unknown error', async () => { + await testErrorResponse( + { + response: { + statusCode: 900, + request: { + uri: { + pathname: '/some/path', + }, }, }, }, - }, - { - errorType: 'SYSTEM_ERROR', - resourcePath: '/some/path', - statusCode: 500, - }, - ); - }); - // they're in testErrorResponse - // eslint-disable-next-line jest/expect-expect - it('should return pods, unknown error', async () => { - await testErrorResponse( - { - response: { + { + errorType: 'UNKNOWN_ERROR', + resourcePath: '/some/path', statusCode: 900, - request: { - uri: { - pathname: '/some/path', - }, - }, }, - }, - { - errorType: 'UNKNOWN_ERROR', - resourcePath: '/some/path', - statusCode: 900, - }, - ); + ); + }); + it('should always add a labelSelector query', async () => { + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + }); + + clientMock.listClusterCustomObject.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + }); + + await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + objectTypesToFetch: OBJECTS_TO_FETCH, + labelSelector: '', + customResources: [], + }); + + const mockCall = clientMock.listClusterCustomObject.mock.calls[0]; + const actualSelector = mockCall[mockCall.length - 1]; + const expectedSelector = 'backstage.io/kubernetes-id=some-service'; + expect(actualSelector).toBe(expectedSelector); + }); + it('should use namespace if provided', async () => { + clientMock.listNamespacedCustomObject.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + }); + + clientMock.listNamespacedCustomObject.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + }); + + await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + objectTypesToFetch: OBJECTS_TO_FETCH, + labelSelector: '', + namespace: 'some-namespace', + customResources: [], + }); + + const mockCall = clientMock.listNamespacedCustomObject.mock.calls[0]; + const namespace = mockCall[2]; + expect(namespace).toBe('some-namespace'); + }); }); - it('should always add a labelSelector query', async () => { - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ + + describe('fetchPodMetricsByNamespaces', () => { + let kubernetesClientProvider: any; + let sut: KubernetesClientBasedFetcher; + + beforeEach(() => { + jest.resetAllMocks(); + + kubernetesClientProvider = { + getMetricsClient: jest.fn(), + getCoreClientByClusterDetails: jest.fn(), + }; + + sut = new KubernetesClientBasedFetcher({ + kubernetesClientProvider, + logger: getVoidLogger(), + }); + }); + + it('should return pod metrics', async () => { + (topPods as jest.Mock).mockResolvedValue(POD_METRICS_FIXTURE); + + const result = await sut.fetchPodMetricsByNamespaces( + { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + new Set(['ns-a', 'ns-b']), + ); + expect(result).toStrictEqual({ + errors: [], + responses: [ { - metadata: { - name: 'pod-name', - }, + type: 'podstatus', + resources: POD_METRICS_FIXTURE, + }, + { + type: 'podstatus', + resources: POD_METRICS_FIXTURE, }, ], - }, + }); }); - - clientMock.listClusterCustomObject.mockResolvedValueOnce({ - body: { - items: [ - { - metadata: { - name: 'service-name', + it('should return pod metrics and error', async () => { + const topPodsMock = topPods as jest.Mock; + topPodsMock + .mockResolvedValueOnce(POD_METRICS_FIXTURE) + .mockRejectedValueOnce({ + response: { + statusCode: 404, + request: { + uri: { + pathname: '/some/path', + }, }, }, - ], - }, - }); + }); - await sut.fetchObjectsForService({ - serviceId: 'some-service', - clusterDetails: { - name: 'cluster1', - url: 'http://localhost:9999', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - }, - objectTypesToFetch: OBJECTS_TO_FETCH, - labelSelector: '', - customResources: [], - }); - - const mockCall = clientMock.listClusterCustomObject.mock.calls[0]; - const actualSelector = mockCall[mockCall.length - 1]; - const expectedSelector = 'backstage.io/kubernetes-id=some-service'; - expect(actualSelector).toBe(expectedSelector); - }); - it('should use namespace if provided', async () => { - clientMock.listNamespacedCustomObject.mockResolvedValueOnce({ - body: { - items: [ + const result = await sut.fetchPodMetricsByNamespaces( + { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + new Set(['ns-a', 'ns-b']), + ); + expect(result).toStrictEqual({ + errors: [ { - metadata: { - name: 'pod-name', - }, + errorType: 'NOT_FOUND', + resourcePath: '/some/path', + statusCode: 404, }, ], - }, - }); - - clientMock.listNamespacedCustomObject.mockResolvedValueOnce({ - body: { - items: [ + responses: [ { - metadata: { - name: 'service-name', - }, + type: 'podstatus', + resources: POD_METRICS_FIXTURE, }, ], - }, + }); }); - - await sut.fetchObjectsForService({ - serviceId: 'some-service', - clusterDetails: { - name: 'cluster1', - url: 'http://localhost:9999', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - }, - objectTypesToFetch: OBJECTS_TO_FETCH, - labelSelector: '', - namespace: 'some-namespace', - customResources: [], - }); - - const mockCall = clientMock.listNamespacedCustomObject.mock.calls[0]; - const namespace = mockCall[2]; - expect(namespace).toBe('some-namespace'); }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 6c61f10700..fd6400529b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CoreV1Api, topPods } from '@kubernetes/client-node'; +import { topPods } from '@kubernetes/client-node'; import lodash, { Dictionary } from 'lodash'; import { Logger } from 'winston'; import { @@ -33,10 +33,6 @@ import { } from '@backstage/plugin-kubernetes-common'; import { KubernetesClientProvider } from './KubernetesClientProvider'; -export interface Clients { - core: CoreV1Api; -} - export interface KubernetesClientBasedFetcherOptions { kubernetesClientProvider: KubernetesClientProvider; logger: Logger; @@ -66,6 +62,8 @@ const statusCodeToErrorType = (statusCode: number): KubernetesErrorTypes => { return 'BAD_REQUEST'; case 401: return 'UNAUTHORIZED_ERROR'; + case 404: + return 'NOT_FOUND'; case 500: return 'SYSTEM_ERROR'; default: diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 3ed89d2a56..8509fa97cc 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -179,6 +179,7 @@ export interface JobsFetchResponse { export type KubernetesErrorTypes = | 'BAD_REQUEST' | 'UNAUTHORIZED_ERROR' + | 'NOT_FOUND' | 'SYSTEM_ERROR' | 'UNKNOWN_ERROR'; diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index 58fc81f3ff..3feddd7f4c 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -233,6 +233,7 @@ export interface KubernetesFetchError { export type KubernetesErrorTypes = | 'BAD_REQUEST' | 'UNAUTHORIZED_ERROR' + | 'NOT_FOUND' | 'SYSTEM_ERROR' | 'UNKNOWN_ERROR';