From 888497b55cb6067f7a866db4ecd0cc7edae2e5d7 Mon Sep 17 00:00:00 2001 From: mclarke Date: Wed, 10 Feb 2021 11:16:37 +0000 Subject: [PATCH] replace kubernetes fanout request function with class --- ...est.ts => KubernetesFanOutHandler.test.ts} | 108 +++++++++-------- .../src/service/KubernetesFanOutHandler.ts | 109 ++++++++++++++++++ .../getKubernetesObjectsForServiceHandler.ts | 107 ----------------- .../src/service/router.test.ts | 41 +++---- .../kubernetes-backend/src/service/router.ts | 21 +--- 5 files changed, 183 insertions(+), 203 deletions(-) rename plugins/kubernetes-backend/src/service/{getKubernetesObjectsByServiceIdHandler.test.ts => KubernetesFanOutHandler.test.ts} (73%) create mode 100644 plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts delete mode 100644 plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts similarity index 73% rename from plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts rename to plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 5c6513e57c..95ec4f0aba 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; import { ObjectFetchParams } from '..'; - -const TEST_SERVICE_ID = 'my-service'; +import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; const fetchObjectsForService = jest.fn(); @@ -81,34 +79,34 @@ describe('handleGetKubernetesObjectsForService', () => { mockFetch(fetchObjectsForService); - const result = await handleGetKubernetesObjectsForService( - TEST_SERVICE_ID, + const sut = new KubernetesFanOutHandler( + getVoidLogger(), { - fetchObjectsForService: fetchObjectsForService, + fetchObjectsForService, }, { getClustersByServiceId, }, - getVoidLogger(), - { - entity: { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { - name: 'test-component', - annotations: { - 'backstage.io/kubernetes-labels-selector': - 'backstage.io/test-label=test-component', - }, - }, - spec: { - type: 'service', - lifecycle: 'production', - owner: 'joe', + ); + + const result = await sut.getKubernetesObjectsByEntity({ + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'backstage.io/kubernetes-labels-selector': + 'backstage.io/test-label=test-component', }, }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', + }, }, - ); + }); expect(getClustersByServiceId.mock.calls.length).toBe(1); expect(fetchObjectsForService.mock.calls.length).toBe(1); @@ -124,7 +122,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-pods-my-service-test-cluster', + name: 'my-pods-test-component-test-cluster', }, }, ], @@ -134,7 +132,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-configmaps-my-service-test-cluster', + name: 'my-configmaps-test-component-test-cluster', }, }, ], @@ -144,7 +142,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-services-my-service-test-cluster', + name: 'my-services-test-component-test-cluster', }, }, ], @@ -172,37 +170,37 @@ describe('handleGetKubernetesObjectsForService', () => { mockFetch(fetchObjectsForService); - const result = await handleGetKubernetesObjectsForService( - TEST_SERVICE_ID, + const sut = new KubernetesFanOutHandler( + getVoidLogger(), { - fetchObjectsForService: fetchObjectsForService, + fetchObjectsForService, }, { getClustersByServiceId, }, - getVoidLogger(), - { - auth: { - google: 'google_token_123', + ); + + const result = await sut.getKubernetesObjectsByEntity({ + auth: { + google: 'google_token_123', + }, + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'backstage.io/kubernetes-labels-selector': + 'backstage.io/test-label=test-component', + }, }, - entity: { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { - name: 'test-component', - annotations: { - 'backstage.io/kubernetes-labels-selector': - 'backstage.io/test-label=test-component', - }, - }, - spec: { - type: 'service', - lifecycle: 'production', - owner: 'joe', - }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', }, }, - ); + }); expect(getClustersByServiceId.mock.calls.length).toBe(1); expect(fetchObjectsForService.mock.calls.length).toBe(2); @@ -218,7 +216,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-pods-my-service-test-cluster', + name: 'my-pods-test-component-test-cluster', }, }, ], @@ -228,7 +226,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-configmaps-my-service-test-cluster', + name: 'my-configmaps-test-component-test-cluster', }, }, ], @@ -238,7 +236,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-services-my-service-test-cluster', + name: 'my-services-test-component-test-cluster', }, }, ], @@ -256,7 +254,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-pods-my-service-other-cluster', + name: 'my-pods-test-component-other-cluster', }, }, ], @@ -266,7 +264,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-configmaps-my-service-other-cluster', + name: 'my-configmaps-test-component-other-cluster', }, }, ], @@ -276,7 +274,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-services-my-service-other-cluster', + name: 'my-services-test-component-other-cluster', }, }, ], diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts new file mode 100644 index 0000000000..0a2fc1488a --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2021 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 { Logger } from 'winston'; +import { + ClusterDetails, + KubernetesFetcher, + KubernetesObjectTypes, + KubernetesRequestBody, + KubernetesServiceLocator, +} from '../types/types'; +import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; +import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; + +const DEFAULT_OBJECTS = new Set([ + 'pods', + 'services', + 'configmaps', + 'deployments', + 'replicasets', + 'horizontalpodautoscalers', + 'ingresses', +]); + +export class KubernetesFanOutHandler { + private readonly logger: Logger; + private readonly fetcher: KubernetesFetcher; + private readonly serviceLocator: KubernetesServiceLocator; + + constructor( + logger: Logger, + fetcher: KubernetesFetcher, + serviceLocator: KubernetesServiceLocator, + ) { + this.logger = logger; + this.fetcher = fetcher; + this.serviceLocator = serviceLocator; + } + + async getKubernetesObjectsByEntity( + requestBody: KubernetesRequestBody, + objectTypesToFetch: Set = DEFAULT_OBJECTS, + ) { + const entityName = requestBody.entity.metadata.name; + + const clusterDetails: ClusterDetails[] = await this.serviceLocator.getClustersByServiceId( + entityName, + ); + + // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them + const promises: Promise[] = clusterDetails.map(cd => { + const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance( + cd.authProvider, + ); + return kubernetesAuthTranslator.decorateClusterDetailsWithAuth( + cd, + requestBody, + ); + }); + const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all( + promises, + ); + + this.logger.info( + `entity.metadata.name=${entityName} clusterDetails=[${clusterDetailsDecoratedForAuth + .map(c => c.name) + .join(', ')}]`, + ); + + const labelSelector: string = + requestBody.entity?.metadata?.annotations?.[ + 'backstage.io/kubernetes-label-selector' + ] || `backstage.io/kubernetes-id=${entityName}`; + + return Promise.all( + clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { + return this.fetcher + .fetchObjectsForService({ + serviceId: entityName, + clusterDetails: clusterDetailsItem, + objectTypesToFetch, + labelSelector, + }) + .then(result => { + return { + cluster: { + name: clusterDetailsItem.name, + }, + resources: result.responses, + errors: result.errors, + }; + }); + }), + ).then(r => ({ items: r })); + } +} diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts deleted file mode 100644 index 811a3c8acb..0000000000 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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 { Logger } from 'winston'; -import { - KubernetesRequestBody, - ClusterDetails, - KubernetesServiceLocator, - KubernetesFetcher, - KubernetesObjectTypes, - ObjectsByEntityResponse, - ObjectFetchParams, -} from '../types/types'; -import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; -import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; - -export type GetKubernetesObjectsForServiceHandler = ( - serviceId: string, - fetcher: KubernetesFetcher, - serviceLocator: KubernetesServiceLocator, - logger: Logger, - requestBody: KubernetesRequestBody, - objectTypesToFetch?: Set, -) => Promise; - -const DEFAULT_OBJECTS = new Set([ - 'pods', - 'services', - 'configmaps', - 'deployments', - 'replicasets', - 'horizontalpodautoscalers', - 'ingresses', -]); - -// Fans out the request to all clusters that the service lives in, aggregates their responses together -export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServiceHandler = async ( - serviceId, - fetcher, - serviceLocator, - logger, - requestBody, - objectTypesToFetch = DEFAULT_OBJECTS, -) => { - const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId( - serviceId, - ); - - // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them - const promises: Promise[] = clusterDetails.map(cd => { - const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance( - cd.authProvider, - ); - return kubernetesAuthTranslator.decorateClusterDetailsWithAuth( - cd, - requestBody, - ); - }); - const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all( - promises, - ); - - logger.info( - `serviceId=${serviceId} clusterDetails=[${clusterDetailsDecoratedForAuth - .map(c => c.name) - .join(', ')}]`, - ); - - const labelSelector: string = - requestBody.entity?.metadata?.annotations?.[ - 'backstage.io/kubernetes-label-selector' - ] || `backstage.io/kubernetes-id=${requestBody.entity.metadata.name}`; - - return Promise.all( - clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { - return fetcher - .fetchObjectsForService({ - serviceId, - clusterDetails: clusterDetailsItem, - objectTypesToFetch, - labelSelector, - } as ObjectFetchParams) - .then(result => { - return { - cluster: { - name: clusterDetailsItem.name, - }, - resources: result.responses, - errors: result.errors, - }; - }); - }), - ).then(r => ({ items: r })); -}; diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts index a407ce9693..c91cae27df 100644 --- a/plugins/kubernetes-backend/src/service/router.test.ts +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -18,35 +18,18 @@ import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; import { makeRouter } from './router'; -import { - KubernetesServiceLocator, - KubernetesFetcher, - ObjectsByEntityResponse, -} from '..'; +import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; describe('router', () => { let app: express.Express; - let kubernetesFetcher: jest.Mocked; - let kubernetesServiceLocator: jest.Mocked; - let handleGetByServiceId: jest.Mock>; + let kubernetesFanOutHandler: jest.Mocked; beforeAll(async () => { - kubernetesFetcher = { - fetchObjectsForService: jest.fn(), - }; + kubernetesFanOutHandler = { + getKubernetesObjectsByEntity: jest.fn(), + } as any; - kubernetesServiceLocator = { - getClustersByServiceId: jest.fn(), - }; - - handleGetByServiceId = jest.fn(); - - const router = makeRouter( - getVoidLogger(), - kubernetesFetcher, - kubernetesServiceLocator, - handleGetByServiceId as any, - ); + const router = makeRouter(getVoidLogger(), kubernetesFanOutHandler); app = express().use(router); }); @@ -67,7 +50,9 @@ describe('router', () => { ], }, } as any; - handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result)); + kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockReturnValueOnce( + Promise.resolve(result), + ); const response = await request(app).post('/services/test-service'); @@ -87,7 +72,9 @@ describe('router', () => { ], }, } as any; - handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result)); + kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockReturnValueOnce( + Promise.resolve(result), + ); const response = await request(app) .post('/services/test-service') @@ -103,7 +90,9 @@ describe('router', () => { }); it('internal error: lists kubernetes objects', async () => { - handleGetByServiceId.mockRejectedValue(Error('some internal error')); + kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockRejectedValue( + Error('some internal error'), + ); const response = await request(app).post('/services/test-service'); diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 38a02ad970..32b6dfb832 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -21,19 +21,15 @@ import { Config } from '@backstage/config'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesClientProvider } from './KubernetesClientProvider'; -import { - GetKubernetesObjectsForServiceHandler, - handleGetKubernetesObjectsForService, -} from './getKubernetesObjectsForServiceHandler'; import { KubernetesRequestBody, KubernetesServiceLocator, - KubernetesFetcher, ServiceLocatorMethod, ClusterLocatorMethod, ClusterDetails, } from '..'; import { getCombinedClusterDetails } from '../cluster-locator'; +import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; export interface RouterOptions { logger: Logger; @@ -62,9 +58,7 @@ const getServiceLocator = ( export const makeRouter = ( logger: Logger, - fetcher: KubernetesFetcher, - serviceLocator: KubernetesServiceLocator, - handleGetByEntity: GetKubernetesObjectsForServiceHandler, + kubernetesFanOutHandler: KubernetesFanOutHandler, ): express.Router => { const router = Router(); router.use(express.json()); @@ -73,11 +67,7 @@ export const makeRouter = ( const serviceId = req.params.serviceId; const requestBody: KubernetesRequestBody = req.body; try { - const response = await handleGetByEntity( - serviceId, - fetcher, - serviceLocator, - logger, + const response = await kubernetesFanOutHandler.getKubernetesObjectsByEntity( requestBody, ); res.send(response); @@ -115,10 +105,11 @@ export async function createRouter( const serviceLocator = getServiceLocator(options.config, clusterDetails); - return makeRouter( + const kubernetesFanOutHandler = new KubernetesFanOutHandler( logger, fetcher, serviceLocator, - handleGetKubernetesObjectsForService, ); + + return makeRouter(logger, kubernetesFanOutHandler); }