feat: Carlos Lopez pre RFC update commit squash

docs: Remove unnecessary types
test: Use MSW instead of mocking fetch
fix: Updated api-report.md & error stringify from @backstage/errors
feat: Kubernetes builder getters/setters update
fix: Error handling with fix: Error handling with`@backstage/errors`
fix: Use same MSW version as other plugins
fix: Sort handleProxyRequest inputs
fix: Small type & comment fixes
fix: Change serverIP to serverURI
fix: Variable name improvements

Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com>
This commit is contained in:
Carlos Esteban Lopez
2022-10-28 18:58:13 -05:00
committed by Fredrik Adelöw
parent b585179770
commit e862980c57
10 changed files with 280 additions and 233 deletions
+35 -3
View File
@@ -22,6 +22,8 @@ import { Logger } from 'winston';
import { Metrics } from '@kubernetes/client-node';
import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { PodStatus } from '@kubernetes/client-node/dist/top';
import type { Request as Request_2 } from 'express';
import { TokenCredential } from '@azure/identity';
// @alpha (undocumented)
@@ -188,6 +190,8 @@ export class KubernetesBuilder {
options: KubernetesObjectsProviderOptions,
): KubernetesObjectsProvider;
// (undocumented)
protected buildProxy(): KubernetesProxy;
// (undocumented)
protected buildRouter(
objectsProvider: KubernetesObjectsProvider,
clusterSupplier: KubernetesClustersSupplier,
@@ -207,9 +211,19 @@ export class KubernetesBuilder {
clusterSupplier: KubernetesClustersSupplier,
): Promise<ClusterDetails[]>;
// (undocumented)
protected getClusterSupplier(): KubernetesClustersSupplier;
// (undocumented)
protected getFetcher(): KubernetesFetcher;
// (undocumented)
protected getObjectsProvider(
options: KubernetesObjectsProviderOptions,
): KubernetesObjectsProvider;
// (undocumented)
protected getObjectTypesToFetch(): ObjectToFetch[] | undefined;
// (undocumented)
protected getProxyServices(): KubernetesProxyServices;
protected getProxy(): KubernetesProxy;
// (undocumented)
protected getServiceLocator(): KubernetesServiceLocator;
// (undocumented)
protected getServiceLocatorMethod(): ServiceLocatorMethod;
// (undocumented)
@@ -226,6 +240,8 @@ export class KubernetesBuilder {
// (undocumented)
setObjectsProvider(objectsProvider?: KubernetesObjectsProvider): this;
// (undocumented)
setProxy(proxy?: KubernetesProxy): this;
// (undocumented)
setServiceLocator(serviceLocator?: KubernetesServiceLocator): this;
}
@@ -330,9 +346,25 @@ export type KubernetesObjectTypes =
| 'daemonsets';
// @alpha (undocumented)
export interface KubernetesProxyServices {
export class KubernetesProxy {
constructor(logger: Logger);
// (undocumented)
kcs: KubernetesClustersSupplier;
handleProxyRequest(
req: Request_2,
clusterSupplier: KubernetesClustersSupplier,
): Promise<KubernetesProxyResponse>;
// (undocumented)
protected readonly logger: Logger;
}
// @alpha (undocumented)
export interface KubernetesProxyResponse {
// (undocumented)
cluster?: string;
// (undocumented)
code: number;
// (undocumented)
data: any;
}
// @alpha
+3 -1
View File
@@ -36,6 +36,7 @@
"dependencies": {
"@azure/identity": "^2.0.4",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
@@ -56,8 +57,8 @@
"helmet": "^6.0.0",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"node-fetch": "^2.6.0",
"morgan": "^1.10.0",
"node-fetch": "^2.6.0",
"stream-buffers": "^3.0.2",
"winston": "^3.2.1",
"yn": "^4.0.0"
@@ -66,6 +67,7 @@
"@backstage/cli": "workspace:^",
"@types/aws4": "^1.5.1",
"aws-sdk-mock": "^5.2.1",
"msw": "^0.48.0",
"supertest": "^6.1.3"
},
"files": [
+1
View File
@@ -32,6 +32,7 @@ export * from './kubernetes-auth-translator/types';
export * from './service/router';
export * from './service/KubernetesBuilder';
export * from './service/KubernetesClientProvider';
export * from './service/KubernetesProxy';
export * from './types/types';
@@ -13,36 +13,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { Duration } from 'luxon';
import { Logger } from 'winston';
import { getCombinedClusterSupplier } from '../cluster-locator';
import { addResourceRoutesToRouter } from '../routes/resourcesRoutes';
import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator';
import {
KubernetesObjectTypes,
ServiceLocatorMethod,
CustomResource,
KubernetesObjectsProvider,
ObjectsByEntityRequest,
KubernetesClustersSupplier,
KubernetesFetcher,
KubernetesServiceLocator,
KubernetesObjectsProvider,
KubernetesObjectsProviderOptions,
KubernetesProxyServices,
KubernetesObjectTypes,
KubernetesServiceLocator,
ObjectsByEntityRequest,
ServiceLocatorMethod,
} from '../types/types';
import { KubernetesClientProvider } from './KubernetesClientProvider';
import { KubernetesProxy, KubernetesProxyResponse } from './KubernetesProxy';
import {
DEFAULT_OBJECTS,
KubernetesFanOutHandler,
} from './KubernetesFanOutHandler';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
import { addResourceRoutesToRouter } from '../routes/resourcesRoutes';
import { CatalogApi } from '@backstage/catalog-client';
import { KubernetesProxy, KubernetesProxyResponse } from './KubernetesProxy';
/**
*
@@ -80,15 +78,13 @@ export class KubernetesBuilder {
private objectsProvider?: KubernetesObjectsProvider;
private fetcher?: KubernetesFetcher;
private serviceLocator?: KubernetesServiceLocator;
private proxy: KubernetesProxy;
private proxy?: KubernetesProxy;
static createBuilder(env: KubernetesEnvironment) {
return new KubernetesBuilder(env);
}
constructor(protected readonly env: KubernetesEnvironment) {
this.proxy = new KubernetesProxy(env.logger);
}
constructor(protected readonly env: KubernetesEnvironment) {}
public async build(): KubernetesBuilderReturn {
const logger = this.env.logger;
@@ -109,25 +105,19 @@ export class KubernetesBuilder {
}
const customResources = this.buildCustomResources();
const fetcher = this.fetcher ?? this.buildFetcher();
const fetcher = this.getFetcher();
const clusterSupplier =
this.clusterSupplier ??
this.buildClusterSupplier(this.defaultClusterRefreshInterval);
const clusterSupplier = this.getClusterSupplier();
const serviceLocator =
this.serviceLocator ??
this.buildServiceLocator(this.getServiceLocatorMethod(), clusterSupplier);
const serviceLocator = this.getServiceLocator();
const objectsProvider =
this.objectsProvider ??
this.buildObjectsProvider({
logger,
fetcher,
serviceLocator,
customResources,
objectTypesToFetch: this.getObjectTypesToFetch(),
});
const objectsProvider = this.getObjectsProvider({
logger,
fetcher,
serviceLocator,
customResources,
objectTypesToFetch: this.getObjectTypesToFetch(),
});
const router = this.buildRouter(
objectsProvider,
@@ -170,6 +160,11 @@ export class KubernetesBuilder {
return this;
}
public setProxy(proxy?: KubernetesProxy) {
this.proxy = proxy;
return this;
}
protected buildCustomResources() {
const customResources: CustomResource[] = (
this.env.config.getOptionalConfigArray('kubernetes.customResources') ?? []
@@ -193,24 +188,29 @@ export class KubernetesBuilder {
refreshInterval: Duration,
): KubernetesClustersSupplier {
const config = this.env.config;
return getCombinedClusterSupplier(
this.clusterSupplier = getCombinedClusterSupplier(
config,
this.env.catalogApi,
refreshInterval,
);
return this.clusterSupplier;
}
protected buildObjectsProvider(
options: KubernetesObjectsProviderOptions,
): KubernetesObjectsProvider {
return new KubernetesFanOutHandler(options);
this.objectsProvider = new KubernetesFanOutHandler(options);
return this.objectsProvider;
}
protected buildFetcher(): KubernetesFetcher {
return new KubernetesClientBasedFetcher({
this.fetcher = new KubernetesClientBasedFetcher({
kubernetesClientProvider: new KubernetesClientProvider(),
logger: this.env.logger,
});
return this.fetcher;
}
protected buildServiceLocator(
@@ -219,14 +219,19 @@ export class KubernetesBuilder {
): KubernetesServiceLocator {
switch (method) {
case 'multiTenant':
return this.buildMultiTenantServiceLocator(clusterSupplier);
this.serviceLocator =
this.buildMultiTenantServiceLocator(clusterSupplier);
break;
case 'http':
return this.buildHttpServiceLocator(clusterSupplier);
this.serviceLocator = this.buildHttpServiceLocator(clusterSupplier);
break;
default:
throw new Error(
`Unsupported kubernetes.clusterLocatorMethod "${method}"`,
);
}
return this.serviceLocator;
}
protected buildMultiTenantServiceLocator(
@@ -241,6 +246,11 @@ export class KubernetesBuilder {
throw new Error('not implemented');
}
protected buildProxy(): KubernetesProxy {
this.proxy = new KubernetesProxy(this.env.logger);
return this.proxy;
}
protected buildRouter(
objectsProvider: KubernetesObjectsProvider,
clusterSupplier: KubernetesClustersSupplier,
@@ -250,6 +260,8 @@ export class KubernetesBuilder {
const router = Router();
router.use(express.json());
const proxy = this.getProxy();
// @deprecated
router.post('/services/:serviceId', async (req, res) => {
const serviceId = req.params.serviceId;
@@ -280,11 +292,13 @@ export class KubernetesBuilder {
});
});
router.get('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.post('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.put('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.patch('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.delete('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
if (typeof proxy?.handleProxyRequest === 'function') {
router.get('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.post('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.put('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.patch('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
router.delete('/proxy/:encodedQuery', this.makeProxyRequest.bind(this));
}
addResourceRoutesToRouter(router, catalogApi, objectsProvider);
@@ -309,6 +323,31 @@ export class KubernetesBuilder {
) as ServiceLocatorMethod;
}
protected getFetcher(): KubernetesFetcher {
return this.fetcher ?? this.buildFetcher();
}
protected getClusterSupplier() {
return (
this.clusterSupplier ??
this.buildClusterSupplier(this.defaultClusterRefreshInterval)
);
}
protected getServiceLocator(): KubernetesServiceLocator {
return (
this.serviceLocator ??
this.buildServiceLocator(
this.getServiceLocatorMethod(),
this.getClusterSupplier(),
)
);
}
protected getObjectsProvider(options: KubernetesObjectsProviderOptions) {
return this.objectsProvider ?? this.buildObjectsProvider(options);
}
protected getObjectTypesToFetch() {
const objectTypesToFetchStrings = this.env.config.getOptionalStringArray(
'kubernetes.objectTypes',
@@ -339,27 +378,27 @@ export class KubernetesBuilder {
return objectTypesToFetch;
}
protected getProxy() {
return this.proxy ?? this.buildProxy();
}
protected async makeProxyRequest(
req: express.Request,
res: express.Response,
) {
const services = this.getProxyServices();
const supplier = this.getClustersSupplier();
const proxy = this.getProxy();
const proxyResponse: KubernetesProxyResponse =
await this.proxy.handleProxyRequest(services, req);
await proxy.handleProxyRequest(req, supplier);
res.status(proxyResponse.code).json(proxyResponse.data);
}
protected getProxyServices(): KubernetesProxyServices {
const kcs =
private getClustersSupplier(): KubernetesClustersSupplier {
return (
this.clusterSupplier ??
this.buildClusterSupplier(this.defaultClusterRefreshInterval);
if (!kcs) {
this.env.logger.error('could not find cluster supplier!');
}
return {
kcs,
};
this.buildClusterSupplier(this.defaultClusterRefreshInterval)
);
}
}
@@ -13,32 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import {
ClusterDetails,
KubernetesClustersSupplier,
KubernetesProxyServices,
} from '../types/types';
import { KubernetesProxy } from './KubernetesProxy';
import { Request } from 'express';
import 'buffer';
jest.mock('node-fetch');
const { Response } = jest.requireActual('node-fetch');
import { getVoidLogger } from '@backstage/backend-common';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { Request } from 'express';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import fetch from 'node-fetch';
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
import { KubernetesProxy } from './KubernetesProxy';
import { NotFoundError } from '@backstage/errors';
describe('KubernetesProxy', () => {
let _clientMock: any;
let sut: KubernetesProxy;
let proxy: KubernetesProxy;
const worker = setupServer();
setupRequestMockHandlers(worker);
const buildEncodedRequest = (
clustersHeader: any,
query: string,
body?: any,
body?: unknown,
): Request => {
const encodedQuery = encodeURIComponent(query);
const encodedClusters = Buffer.from(
@@ -75,41 +70,29 @@ describe('KubernetesProxy', () => {
return req;
};
const buildProxyServicesWithClusters = (
const buildClustersSupplierWithClusters = (
clusters: ClusterDetails[],
): KubernetesProxyServices => {
const kcs: KubernetesClustersSupplier = {
getClusters: async () => {
return clusters;
},
};
return {
kcs,
};
};
beforeEach(() => {
jest.resetAllMocks();
_clientMock = {
handleProxyRequest: jest.fn(),
};
sut = new KubernetesProxy(getVoidLogger());
): KubernetesClustersSupplier => ({
getClusters: async () => {
return clusters;
},
});
it('should return a 404 if no clusters are found', async () => {
const services = buildProxyServicesWithClusters([]);
beforeEach(() => {
proxy = new KubernetesProxy(getVoidLogger());
});
it('should return a ERROR_NOT_FOUND if no clusters are found', async () => {
const clustersSupplier = buildClustersSupplierWithClusters([]);
const req = buildEncodedRequest({}, 'api');
const result = await sut.handleProxyRequest(services, req);
expect(result.code).toEqual(404);
expect(fetch).not.toHaveBeenCalled();
await expect(
proxy.handleProxyRequest(req, clustersSupplier),
).rejects.toThrow(NotFoundError);
});
it('should match the response code of the Kubernetes response (single cluster)', async () => {
const services = buildProxyServicesWithClusters([
const clusters: ClusterDetails[] = [
{
name: 'cluster1',
url: 'http://localhost:9999',
@@ -117,7 +100,9 @@ describe('KubernetesProxy', () => {
authProvider: 'serviceAccount',
skipTLSVerify: true,
},
]);
];
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
const req = buildEncodedRequest({ cluster1: 'token' }, 'api');
const apiResponse = {
@@ -131,21 +116,19 @@ describe('KubernetesProxy', () => {
],
};
// @ts-ignore-next-line
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(apiResponse), {
status: 299,
}),
worker.use(
rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
res(ctx.status(299), ctx.body(JSON.stringify(apiResponse))),
),
);
const result = await sut.handleProxyRequest(services, req);
const result = await proxy.handleProxyRequest(req, clustersSupplier);
expect(fetch).toBeCalledTimes(1);
expect(result.code).toEqual(299);
});
it('should match the response code of the best Kubernetes response (multi cluster)', async () => {
const services = buildProxyServicesWithClusters([
const clusters: ClusterDetails[] = [
{
name: 'cluster1',
url: 'http://localhost:9998',
@@ -160,7 +143,9 @@ describe('KubernetesProxy', () => {
authProvider: 'serviceAccount',
skipTLSVerify: true,
},
]);
];
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
const req = buildEncodedRequest(
{ cluster1: 'token', cluster2: 'token' },
'api',
@@ -187,26 +172,22 @@ describe('KubernetesProxy', () => {
code: 401,
};
(fetch as jest.MockedFunction<typeof fetch>)
.mockResolvedValueOnce(
new Response(JSON.stringify(apiResponse1), {
status: 200,
}),
)
.mockResolvedValueOnce(
new Response(JSON.stringify(apiResponse2), {
status: 401,
}),
);
worker.use(
rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
res(ctx.status(200), ctx.body(JSON.stringify(apiResponse1))),
),
rest.get(`${clusters[1].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
res(ctx.status(401), ctx.body(JSON.stringify(apiResponse2))),
),
);
const result = await sut.handleProxyRequest(services, req);
const result = await proxy.handleProxyRequest(req, clustersSupplier);
expect(fetch).toBeCalledTimes(2);
expect(result.code).toEqual(200);
});
it('should pass the exact response data from Kubernetes (single cluster)', async () => {
const services = buildProxyServicesWithClusters([
const clusters: ClusterDetails[] = [
{
name: 'cluster1',
url: 'http://localhost:9999',
@@ -214,7 +195,9 @@ describe('KubernetesProxy', () => {
authProvider: 'serviceAccount',
skipTLSVerify: true,
},
]);
];
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
const req = buildEncodedRequest({ cluster1: 'token' }, 'api');
const apiResponse = {
@@ -228,14 +211,13 @@ describe('KubernetesProxy', () => {
],
};
// @ts-ignore-next-line
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(apiResponse), {
status: 200,
}),
worker.use(
rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
res(ctx.status(200), ctx.body(JSON.stringify(apiResponse))),
),
);
const result = await sut.handleProxyRequest(services, req);
const result = await proxy.handleProxyRequest(req, clustersSupplier);
const resultString = JSON.stringify(result.data);
const expectedString = JSON.stringify({
@@ -251,12 +233,11 @@ describe('KubernetesProxy', () => {
},
});
expect(fetch).toBeCalledTimes(1);
expect(resultString).toEqual(expectedString);
});
it('should pass the exact response data from Kubernetes (multi cluster)', async () => {
const services = buildProxyServicesWithClusters([
const clusters: ClusterDetails[] = [
{
name: 'cluster1',
url: 'http://localhost:9998',
@@ -271,7 +252,9 @@ describe('KubernetesProxy', () => {
authProvider: 'serviceAccount',
skipTLSVerify: true,
},
]);
];
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
const req = buildEncodedRequest(
{ cluster1: 'token', cluster2: 'token' },
'api',
@@ -298,20 +281,16 @@ describe('KubernetesProxy', () => {
code: 401,
};
// @ts-ignore-next-line
(fetch as jest.MockedFunction<typeof fetch>)
.mockResolvedValueOnce(
new Response(JSON.stringify(apiResponse1), {
status: 200,
}),
)
.mockResolvedValueOnce(
new Response(JSON.stringify(apiResponse2), {
status: 401,
}),
);
worker.use(
rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
res(ctx.status(200), ctx.body(JSON.stringify(apiResponse1))),
),
rest.get(`${clusters[1].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
res(ctx.status(401), ctx.body(JSON.stringify(apiResponse2))),
),
);
const result = await sut.handleProxyRequest(services, req);
const result = await proxy.handleProxyRequest(req, clustersSupplier);
const resultString = JSON.stringify(result.data);
const expectedString = JSON.stringify({
@@ -336,7 +315,6 @@ describe('KubernetesProxy', () => {
},
});
expect(fetch).toBeCalledTimes(2);
expect(resultString).toEqual(expectedString);
});
});
@@ -13,69 +13,81 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { KubeConfig, bufferFromFileOrString } from '@kubernetes/client-node';
import { Logger } from 'winston';
import fetch from 'node-fetch';
import {
AuthenticationError,
ConflictError,
ForwardedError,
InputError,
NotFoundError,
stringifyError,
} from '@backstage/errors';
import { bufferFromFileOrString, KubeConfig } from '@kubernetes/client-node';
import * as https from 'https';
import fetch, { RequestInit } from 'node-fetch';
import { Logger } from 'winston';
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
import type { Request } from 'express';
import {
ClusterDetails,
KubernetesProxyServices,
KubernetesClustersSupplier,
} from '../types/types';
const HEADER_CONTENT_TYPE: string = 'Content-Type';
const APPLICATION_JSON: string = 'application/json';
const HEADER_KUBERNETES_CLUSTERS: string = 'X-Kubernetes-Clusters';
const ERROR_BAD_REQUEST: number = 400;
const ERROR_NOT_FOUND: number = 404;
const ERROR_INTERNAL_SERVER: number = 500;
const CLUSTER_USER_NAME: string = 'backstage';
/**
*
* @alpha
*/
export interface KubernetesProxyResponse {
code: number;
data: any;
cluster?: string;
}
/**
*
* @alpha
*/
interface KubernetesProxyClusters {
[key: string]: string;
}
/**
*
* @alpha
*/
export class KubernetesProxy {
constructor(protected readonly logger: Logger) {}
public async handleProxyRequest(
services: KubernetesProxyServices,
req: Request,
clusterSupplier: KubernetesClustersSupplier,
): Promise<KubernetesProxyResponse> {
const krc = this.getKubernetesRequestedClusters(req);
const requestedClusters = this.getKubernetesRequestedClusters(req);
if (Object.keys(krc).length < 1) {
return {
code: ERROR_NOT_FOUND,
data: 'No clusters found!',
};
if (Object.keys(requestedClusters).length < 1) {
this.logger.error(`No clusters found`);
throw new NotFoundError('No clusters found!');
}
const details = await this.getClusterDetails(services.kcs, krc);
const clusterDetails = await this.getClusterDetails(
clusterSupplier,
requestedClusters,
);
if (details.length < 1) {
return {
code: ERROR_NOT_FOUND,
data: 'No clusters found!',
};
if (clusterDetails.length < 1) {
this.logger.error(`No clusters found`);
throw new NotFoundError('No clusters found!');
}
const responses = await Promise.all(
details.map(async d => {
const response = await this.makeRequestToCluster(d, req);
clusterDetails.map(async clusterDetail => {
const response = await this.makeRequestToCluster(clusterDetail, req);
return response;
}),
);
@@ -116,7 +128,7 @@ export class KubernetesProxy {
return clusters;
} catch (e: any) {
this.logger.debug(
`error with encoded cluster header: ${JSON.stringify(e)}`,
`error with encoded cluster header: ${stringifyError(e)}`,
);
}
return {};
@@ -124,17 +136,17 @@ export class KubernetesProxy {
private async getClusterDetails(
clusterSupplier: KubernetesClustersSupplier,
krc: KubernetesProxyClusters,
requestedClusters: KubernetesProxyClusters,
): Promise<ClusterDetails[]> {
const clusters = await clusterSupplier.getClusters();
const clusterNames = Object.keys(krc);
const clusterNames = Object.keys(requestedClusters);
const clusterDetails = clusters.filter(c => clusterNames.includes(c.name));
const clusterDetailsAuth = clusterDetails.map(c => {
const cAuth: ClusterDetails = Object.assign(c, {
serviceAccountToken: krc[c.name],
serviceAccountToken: requestedClusters[c.name],
});
return cAuth;
});
@@ -151,28 +163,26 @@ export class KubernetesProxy {
details: ClusterDetails,
req: Request,
): Promise<KubernetesProxyResponse> {
const serverIP = this.getClusterURI(details);
if (!serverIP) {
return {
code: ERROR_INTERNAL_SERVER,
data: null,
};
const serverURI = this.getClusterURI(details);
if (!serverURI) {
this.logger.error(`Cluster ${details.name} details IP error`);
throw new ConflictError('Cluster detail error');
}
const query = decodeURIComponent(req.params.encodedQuery) || '';
const uri = `${serverIP}/${query}`;
const uri = `${serverURI}/${query}`;
const contentType = req.header(HEADER_CONTENT_TYPE) || APPLICATION_JSON;
const res = await this.sendClusterRequest(
return await this.sendClusterRequest(
details,
uri,
req.method,
contentType,
req.body,
);
return res;
}
private async sendClusterRequest(
@@ -183,16 +193,14 @@ export class KubernetesProxy {
body?: any,
): Promise<KubernetesProxyResponse> {
const bearerToken = details.serviceAccountToken;
if (!bearerToken) {
return {
code: ERROR_BAD_REQUEST,
data: {
error: 'Invalid service account token',
},
};
this.logger.error('Invalid service account token');
throw new AuthenticationError('Invalid service account token');
}
const reqData: any = {
const reqData: RequestInit = {
method,
headers: {
'Content-Type': contentType,
@@ -205,13 +213,10 @@ export class KubernetesProxy {
const ca = bufferFromFileOrString('', details.caData)?.toString() || '';
reqData.agent = new https.Agent({ ca });
} else {
this.logger.info('could not find CA certificate!');
return {
code: ERROR_INTERNAL_SERVER,
data: {
error: 'Invalid CA certificate configured within Backstage',
},
};
this.logger.error('could not find CA certificate!');
throw new InputError(
'Invalid CA certificate configured within Backstage',
);
}
}
@@ -220,28 +225,25 @@ export class KubernetesProxy {
}
try {
const req = await fetch(uri, reqData);
const res = await fetch(uri, reqData);
let data: string | any;
let res;
if (contentType.includes(APPLICATION_JSON)) {
res = await req.json();
data = await res.json();
} else {
res = await req.text();
data = await res.text();
}
const proxyResponse: KubernetesProxyResponse = {
code: req.status,
data: res,
code: res.status,
data,
cluster: details.name,
};
return proxyResponse;
} catch (e: any) {
return {
code: ERROR_INTERNAL_SERVER,
data: e,
cluster: details.name,
};
throw new ForwardedError(`Cluster ${details.name} request error`, e);
}
}
@@ -264,19 +266,20 @@ export class KubernetesProxy {
cluster: cluster.name,
};
const kc = new KubeConfig();
const kubeConfig = new KubeConfig();
if (clusterDetails.serviceAccountToken) {
kc.loadFromOptions({
kubeConfig.loadFromOptions({
clusters: [cluster],
users: [user],
contexts: [context],
currentContext: context.name,
});
} else {
kc.loadFromDefault();
kubeConfig.loadFromDefault();
}
return kc;
return kubeConfig;
}
private getBestResponseCode(codes: number[]): number {
@@ -280,11 +280,3 @@ export interface KubernetesObjectsProvider {
customResourcesByEntity: CustomResourcesByEntity,
): Promise<ObjectsByEntityResponse>;
}
/**
*
* @alpha
*/
export interface KubernetesProxyServices {
kcs: KubernetesClustersSupplier;
}
-4
View File
@@ -193,16 +193,12 @@ export interface KubernetesFetchError {
statusCode?: number;
}
// Warning: (ae-missing-release-tag) "KubernetesProxyClusters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface KubernetesProxyClusters {
// (undocumented)
[key: string]: string;
}
// Warning: (ae-missing-release-tag) "KubernetesRequestAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface KubernetesRequestAuth {
// (undocumented)
+1
View File
@@ -259,6 +259,7 @@ export interface ClientPodStatus {
containers: ClientContainerStatus[];
}
/** @public */
export interface KubernetesProxyClusters {
[key: string]: string;
}
+3
View File
@@ -5964,6 +5964,7 @@ __metadata:
dependencies:
"@azure/identity": ^2.0.4
"@backstage/backend-common": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
@@ -5988,6 +5989,8 @@ __metadata:
lodash: ^4.17.21
luxon: ^3.0.0
morgan: ^1.10.0
msw: ^0.48.0
node-fetch: ^2.6.0
stream-buffers: ^3.0.2
supertest: ^6.1.3
winston: ^3.2.1