replace kubernetes fanout request function with class
This commit is contained in:
+53
-55
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -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<KubernetesObjectTypes>([
|
||||
'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<KubernetesObjectTypes> = 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>[] = 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 }));
|
||||
}
|
||||
}
|
||||
@@ -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<KubernetesObjectTypes>,
|
||||
) => Promise<ObjectsByEntityResponse>;
|
||||
|
||||
const DEFAULT_OBJECTS = new Set<KubernetesObjectTypes>([
|
||||
'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>[] = 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 }));
|
||||
};
|
||||
@@ -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<KubernetesFetcher>;
|
||||
let kubernetesServiceLocator: jest.Mocked<KubernetesServiceLocator>;
|
||||
let handleGetByServiceId: jest.Mock<Promise<ObjectsByEntityResponse>>;
|
||||
let kubernetesFanOutHandler: jest.Mocked<KubernetesFanOutHandler>;
|
||||
|
||||
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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user