feat: Remove multi-cluster support per latest RFC version
Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com>
This commit is contained in:
committed by
Fredrik Adelöw
parent
e862980c57
commit
9fb0d696b8
@@ -22,10 +22,12 @@ 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 type { RequestHandler } from 'express';
|
||||
import { TokenCredential } from '@azure/identity';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const APPLICATION_JSON: string;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface AWSClusterDetails extends ClusterDetails {
|
||||
// (undocumented)
|
||||
@@ -144,6 +146,9 @@ export class GoogleServiceAccountAuthTranslator
|
||||
): Promise<GKEClusterDetails>;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const HEADER_KUBERNETES_CLUSTER: string;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface KubernetesAuthTranslator {
|
||||
// (undocumented)
|
||||
@@ -227,11 +232,6 @@ export class KubernetesBuilder {
|
||||
// (undocumented)
|
||||
protected getServiceLocatorMethod(): ServiceLocatorMethod;
|
||||
// (undocumented)
|
||||
protected makeProxyRequest(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): Promise<void>;
|
||||
// (undocumented)
|
||||
setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this;
|
||||
// (undocumented)
|
||||
setDefaultClusterRefreshInterval(refreshInterval: Duration): this;
|
||||
@@ -251,6 +251,7 @@ export type KubernetesBuilderReturn = Promise<{
|
||||
clusterSupplier: KubernetesClustersSupplier;
|
||||
customResources: CustomResource[];
|
||||
fetcher: KubernetesFetcher;
|
||||
proxy: KubernetesProxy;
|
||||
objectsProvider: KubernetesObjectsProvider;
|
||||
serviceLocator: KubernetesServiceLocator;
|
||||
}>;
|
||||
@@ -349,22 +350,14 @@ export type KubernetesObjectTypes =
|
||||
export class KubernetesProxy {
|
||||
constructor(logger: Logger);
|
||||
// (undocumented)
|
||||
handleProxyRequest(
|
||||
req: Request_2,
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
): Promise<KubernetesProxyResponse>;
|
||||
get clustersSupplier(): KubernetesClustersSupplier;
|
||||
set clustersSupplier(clustersSupplier: KubernetesClustersSupplier);
|
||||
// (undocumented)
|
||||
protected readonly logger: Logger;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface KubernetesProxyResponse {
|
||||
// (undocumented)
|
||||
cluster?: string;
|
||||
static readonly PROXY_PATH: string;
|
||||
// (undocumented)
|
||||
code: number;
|
||||
// (undocumented)
|
||||
data: any;
|
||||
proxyRequestHandler: RequestHandler;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-kubernetes-common": "workspace:^",
|
||||
"@google-cloud/container": "^4.0.0",
|
||||
"@jest-mock/express": "^2.0.1",
|
||||
"@kubernetes/client-node": "0.17.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/luxon": "^3.0.0",
|
||||
@@ -58,7 +59,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"stream-buffers": "^3.0.2",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
KubernetesFanOutHandler,
|
||||
} from './KubernetesFanOutHandler';
|
||||
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
|
||||
import { KubernetesProxy, KubernetesProxyResponse } from './KubernetesProxy';
|
||||
import { KubernetesProxy } from './KubernetesProxy';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -62,6 +62,7 @@ export type KubernetesBuilderReturn = Promise<{
|
||||
clusterSupplier: KubernetesClustersSupplier;
|
||||
customResources: CustomResource[];
|
||||
fetcher: KubernetesFetcher;
|
||||
proxy: KubernetesProxy;
|
||||
objectsProvider: KubernetesObjectsProvider;
|
||||
serviceLocator: KubernetesServiceLocator;
|
||||
}>;
|
||||
@@ -107,8 +108,12 @@ export class KubernetesBuilder {
|
||||
|
||||
const fetcher = this.getFetcher();
|
||||
|
||||
const proxy = this.getProxy();
|
||||
|
||||
const clusterSupplier = this.getClusterSupplier();
|
||||
|
||||
proxy.clustersSupplier = clusterSupplier;
|
||||
|
||||
const serviceLocator = this.getServiceLocator();
|
||||
|
||||
const objectsProvider = this.getObjectsProvider({
|
||||
@@ -129,6 +134,7 @@ export class KubernetesBuilder {
|
||||
clusterSupplier,
|
||||
customResources,
|
||||
fetcher,
|
||||
proxy,
|
||||
objectsProvider,
|
||||
router,
|
||||
serviceLocator,
|
||||
@@ -292,12 +298,12 @@ export class KubernetesBuilder {
|
||||
});
|
||||
});
|
||||
|
||||
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));
|
||||
if (typeof proxy?.proxyRequestHandler === 'function') {
|
||||
router.get(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler);
|
||||
router.post(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler);
|
||||
router.put(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler);
|
||||
router.patch(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler);
|
||||
router.delete(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler);
|
||||
}
|
||||
|
||||
addResourceRoutesToRouter(router, catalogApi, objectsProvider);
|
||||
@@ -381,24 +387,4 @@ export class KubernetesBuilder {
|
||||
protected getProxy() {
|
||||
return this.proxy ?? this.buildProxy();
|
||||
}
|
||||
|
||||
protected async makeProxyRequest(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
) {
|
||||
const supplier = this.getClustersSupplier();
|
||||
const proxy = this.getProxy();
|
||||
|
||||
const proxyResponse: KubernetesProxyResponse =
|
||||
await proxy.handleProxyRequest(req, supplier);
|
||||
|
||||
res.status(proxyResponse.code).json(proxyResponse.data);
|
||||
}
|
||||
|
||||
private getClustersSupplier(): KubernetesClustersSupplier {
|
||||
return (
|
||||
this.clusterSupplier ??
|
||||
this.buildClusterSupplier(this.defaultClusterRefreshInterval)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,50 +22,38 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
|
||||
import { KubernetesProxy } from './KubernetesProxy';
|
||||
import {
|
||||
APPLICATION_JSON,
|
||||
HEADER_KUBERNETES_CLUSTER,
|
||||
KubernetesProxy,
|
||||
} from './KubernetesProxy';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { getMockReq, getMockRes } from '@jest-mock/express';
|
||||
|
||||
describe('KubernetesProxy', () => {
|
||||
let proxy: KubernetesProxy;
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
const buildEncodedRequest = (
|
||||
clustersHeader: any,
|
||||
query: string,
|
||||
body?: unknown,
|
||||
): Request => {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const encodedClusters = Buffer.from(
|
||||
JSON.stringify(clustersHeader),
|
||||
).toString('base64');
|
||||
|
||||
const req = {
|
||||
const buildMockRequest = (clusterName: any, path: string): Request => {
|
||||
const req = getMockReq({
|
||||
params: {
|
||||
encodedQuery,
|
||||
path,
|
||||
},
|
||||
header: (key: string) => {
|
||||
let value: string = '';
|
||||
header: jest.fn((key: string) => {
|
||||
switch (key) {
|
||||
case 'Content-Type': {
|
||||
value = 'application/json';
|
||||
break;
|
||||
return APPLICATION_JSON;
|
||||
}
|
||||
case 'X-Kubernetes-Clusters': {
|
||||
value = encodedClusters;
|
||||
break;
|
||||
case HEADER_KUBERNETES_CLUSTER: {
|
||||
return clusterName;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
if (body) {
|
||||
req.body = body;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
return req;
|
||||
};
|
||||
@@ -83,15 +71,17 @@ describe('KubernetesProxy', () => {
|
||||
});
|
||||
|
||||
it('should return a ERROR_NOT_FOUND if no clusters are found', async () => {
|
||||
const clustersSupplier = buildClustersSupplierWithClusters([]);
|
||||
const req = buildEncodedRequest({}, 'api');
|
||||
proxy.clustersSupplier = buildClustersSupplierWithClusters([]);
|
||||
|
||||
await expect(
|
||||
proxy.handleProxyRequest(req, clustersSupplier),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
const req = buildMockRequest('test', 'api');
|
||||
const { res, next } = getMockRes();
|
||||
|
||||
await expect(proxy.proxyRequestHandler(req, res, next)).rejects.toThrow(
|
||||
NotFoundError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should match the response code of the Kubernetes response (single cluster)', async () => {
|
||||
it('should match the response code of the Kubernetes response', async () => {
|
||||
const clusters: ClusterDetails[] = [
|
||||
{
|
||||
name: 'cluster1',
|
||||
@@ -102,8 +92,10 @@ describe('KubernetesProxy', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
|
||||
const req = buildEncodedRequest({ cluster1: 'token' }, 'api');
|
||||
proxy.clustersSupplier = buildClustersSupplierWithClusters(clusters);
|
||||
|
||||
const req = buildMockRequest('cluster1', 'api');
|
||||
const { res: response, next } = getMockRes();
|
||||
|
||||
const apiResponse = {
|
||||
kind: 'APIVersions',
|
||||
@@ -117,76 +109,18 @@ describe('KubernetesProxy', () => {
|
||||
};
|
||||
|
||||
worker.use(
|
||||
rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
|
||||
rest.get(`${clusters[0].url}/${req.params.path}`, (_, res, ctx) =>
|
||||
res(ctx.status(299), ctx.body(JSON.stringify(apiResponse))),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await proxy.handleProxyRequest(req, clustersSupplier);
|
||||
await proxy.proxyRequestHandler(req, response, next);
|
||||
|
||||
expect(result.code).toEqual(299);
|
||||
expect(response.status).toHaveBeenCalledWith(299);
|
||||
expect(response.json).toHaveBeenCalledWith(apiResponse);
|
||||
});
|
||||
|
||||
it('should match the response code of the best Kubernetes response (multi cluster)', async () => {
|
||||
const clusters: ClusterDetails[] = [
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9998',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
];
|
||||
|
||||
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
|
||||
const req = buildEncodedRequest(
|
||||
{ cluster1: 'token', cluster2: 'token' },
|
||||
'api',
|
||||
);
|
||||
|
||||
const apiResponse1 = {
|
||||
kind: 'APIVersions',
|
||||
versions: ['v1'],
|
||||
serverAddressByClientCIDRs: [
|
||||
{
|
||||
clientCIDR: '0.0.0.0/0',
|
||||
serverAddress: '192.168.0.1:3333',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const apiResponse2 = {
|
||||
kind: 'Status',
|
||||
apiVersion: 'v1',
|
||||
metadata: {},
|
||||
status: 'Failure',
|
||||
message: 'Unauthorized',
|
||||
reason: 'Unauthorized',
|
||||
code: 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 proxy.handleProxyRequest(req, clustersSupplier);
|
||||
|
||||
expect(result.code).toEqual(200);
|
||||
});
|
||||
|
||||
it('should pass the exact response data from Kubernetes (single cluster)', async () => {
|
||||
it('should pass the exact response data from Kubernetes', async () => {
|
||||
const clusters: ClusterDetails[] = [
|
||||
{
|
||||
name: 'cluster1',
|
||||
@@ -197,8 +131,10 @@ describe('KubernetesProxy', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
|
||||
const req = buildEncodedRequest({ cluster1: 'token' }, 'api');
|
||||
proxy.clustersSupplier = buildClustersSupplierWithClusters(clusters);
|
||||
|
||||
const req = buildMockRequest('cluster1', 'api');
|
||||
const { res: response, next } = getMockRes();
|
||||
|
||||
const apiResponse = {
|
||||
kind: 'APIVersions',
|
||||
@@ -212,109 +148,14 @@ describe('KubernetesProxy', () => {
|
||||
};
|
||||
|
||||
worker.use(
|
||||
rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) =>
|
||||
rest.get(`${clusters[0].url}/${req.params.path}`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.body(JSON.stringify(apiResponse))),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await proxy.handleProxyRequest(req, clustersSupplier);
|
||||
await proxy.proxyRequestHandler(req, response, next);
|
||||
|
||||
const resultString = JSON.stringify(result.data);
|
||||
const expectedString = JSON.stringify({
|
||||
cluster1: {
|
||||
kind: 'APIVersions',
|
||||
versions: ['v1'],
|
||||
serverAddressByClientCIDRs: [
|
||||
{
|
||||
clientCIDR: '0.0.0.0/0',
|
||||
serverAddress: '192.168.0.1:3333',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(resultString).toEqual(expectedString);
|
||||
});
|
||||
|
||||
it('should pass the exact response data from Kubernetes (multi cluster)', async () => {
|
||||
const clusters: ClusterDetails[] = [
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9998',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
];
|
||||
|
||||
const clustersSupplier = buildClustersSupplierWithClusters(clusters);
|
||||
const req = buildEncodedRequest(
|
||||
{ cluster1: 'token', cluster2: 'token' },
|
||||
'api',
|
||||
);
|
||||
|
||||
const apiResponse1 = {
|
||||
kind: 'APIVersions',
|
||||
versions: ['v1'],
|
||||
serverAddressByClientCIDRs: [
|
||||
{
|
||||
clientCIDR: '0.0.0.0/0',
|
||||
serverAddress: '192.168.0.1:3333',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const apiResponse2 = {
|
||||
kind: 'Status',
|
||||
apiVersion: 'v1',
|
||||
metadata: {},
|
||||
status: 'Failure',
|
||||
message: 'Unauthorized',
|
||||
reason: 'Unauthorized',
|
||||
code: 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 proxy.handleProxyRequest(req, clustersSupplier);
|
||||
|
||||
const resultString = JSON.stringify(result.data);
|
||||
const expectedString = JSON.stringify({
|
||||
cluster1: {
|
||||
kind: 'APIVersions',
|
||||
versions: ['v1'],
|
||||
serverAddressByClientCIDRs: [
|
||||
{
|
||||
clientCIDR: '0.0.0.0/0',
|
||||
serverAddress: '192.168.0.1:3333',
|
||||
},
|
||||
],
|
||||
},
|
||||
cluster2: {
|
||||
kind: 'Status',
|
||||
apiVersion: 'v1',
|
||||
metadata: {},
|
||||
status: 'Failure',
|
||||
message: 'Unauthorized',
|
||||
reason: 'Unauthorized',
|
||||
code: 401,
|
||||
},
|
||||
});
|
||||
|
||||
expect(resultString).toEqual(expectedString);
|
||||
expect(response.status).toHaveBeenCalledWith(200);
|
||||
expect(response.json).toHaveBeenCalledWith(apiResponse);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,151 +19,105 @@ import {
|
||||
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 fetch, { RequestInit, Response } from 'node-fetch';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
|
||||
|
||||
import type { Request } from 'express';
|
||||
import type { Request as ExpressRequest, RequestHandler } from 'express';
|
||||
|
||||
/**
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const APPLICATION_JSON: string = 'application/json';
|
||||
|
||||
/**
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster';
|
||||
|
||||
const HEADER_CONTENT_TYPE: string = 'Content-Type';
|
||||
const APPLICATION_JSON: string = 'application/json';
|
||||
|
||||
const HEADER_KUBERNETES_CLUSTERS: string = 'X-Kubernetes-Clusters';
|
||||
|
||||
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 {
|
||||
private _clustersSupplier?: KubernetesClustersSupplier;
|
||||
|
||||
static readonly PROXY_PATH: string = '/proxy/:path(*)';
|
||||
|
||||
constructor(protected readonly logger: Logger) {}
|
||||
|
||||
public async handleProxyRequest(
|
||||
req: Request,
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
): Promise<KubernetesProxyResponse> {
|
||||
const requestedClusters = this.getKubernetesRequestedClusters(req);
|
||||
public proxyRequestHandler: RequestHandler = async (req, res) => {
|
||||
const requestedCluster = this.getKubernetesRequestedCluster(req);
|
||||
|
||||
if (Object.keys(requestedClusters).length < 1) {
|
||||
this.logger.error(`No clusters found`);
|
||||
throw new NotFoundError('No clusters found!');
|
||||
const clusterDetails = await this.getClusterDetails(requestedCluster);
|
||||
|
||||
const response = await this.makeRequestToCluster(clusterDetails, req);
|
||||
|
||||
const contentType = req.header(HEADER_CONTENT_TYPE) || APPLICATION_JSON;
|
||||
|
||||
const data = contentType.includes(APPLICATION_JSON)
|
||||
? await response.json()
|
||||
: await response.text();
|
||||
|
||||
res.status(response.status).json(data);
|
||||
};
|
||||
|
||||
public get clustersSupplier(): KubernetesClustersSupplier {
|
||||
if (this._clustersSupplier ? false : this._clustersSupplier ?? true) {
|
||||
throw new ConflictError("Missing Proxy's Clusters Supplier");
|
||||
}
|
||||
|
||||
const clusterDetails = await this.getClusterDetails(
|
||||
clusterSupplier,
|
||||
requestedClusters,
|
||||
);
|
||||
|
||||
if (clusterDetails.length < 1) {
|
||||
this.logger.error(`No clusters found`);
|
||||
throw new NotFoundError('No clusters found!');
|
||||
}
|
||||
|
||||
const responses = await Promise.all(
|
||||
clusterDetails.map(async clusterDetail => {
|
||||
const response = await this.makeRequestToCluster(clusterDetail, req);
|
||||
return response;
|
||||
}),
|
||||
);
|
||||
|
||||
const data: { [key: string]: any } = {};
|
||||
const codes: number[] = [];
|
||||
|
||||
responses.forEach(kpr => {
|
||||
if (kpr.cluster) {
|
||||
data[kpr.cluster] = kpr.data;
|
||||
codes.push(kpr.code);
|
||||
}
|
||||
});
|
||||
|
||||
const code = this.getBestResponseCode(codes);
|
||||
|
||||
const res: KubernetesProxyResponse = {
|
||||
code,
|
||||
data,
|
||||
};
|
||||
|
||||
return res;
|
||||
return this._clustersSupplier as KubernetesClustersSupplier;
|
||||
}
|
||||
|
||||
private getKubernetesRequestedClusters(
|
||||
req: Request,
|
||||
): KubernetesProxyClusters {
|
||||
const encodedClusters: string =
|
||||
req.header(HEADER_KUBERNETES_CLUSTERS) ?? '';
|
||||
public set clustersSupplier(clustersSupplier) {
|
||||
this._clustersSupplier = clustersSupplier;
|
||||
}
|
||||
|
||||
if (!encodedClusters) {
|
||||
return {};
|
||||
private getKubernetesRequestedCluster(req: ExpressRequest): string {
|
||||
const requestedClusterName: string =
|
||||
req.header(HEADER_KUBERNETES_CLUSTER) ?? '';
|
||||
|
||||
if (!requestedClusterName) {
|
||||
this.logger.error(`Malformed ${HEADER_KUBERNETES_CLUSTER} header.`);
|
||||
throw new InputError(`Malformed ${HEADER_KUBERNETES_CLUSTER} header.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedClusters = Buffer.from(encodedClusters, 'base64').toString();
|
||||
const clusters: KubernetesProxyClusters = JSON.parse(decodedClusters);
|
||||
return clusters;
|
||||
} catch (e: any) {
|
||||
this.logger.debug(
|
||||
`error with encoded cluster header: ${stringifyError(e)}`,
|
||||
);
|
||||
}
|
||||
return {};
|
||||
return requestedClusterName;
|
||||
}
|
||||
|
||||
private async getClusterDetails(
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
requestedClusters: KubernetesProxyClusters,
|
||||
): Promise<ClusterDetails[]> {
|
||||
const clusters = await clusterSupplier.getClusters();
|
||||
requestedCluster: string,
|
||||
): Promise<ClusterDetails> {
|
||||
const clusters = await this.clustersSupplier.getClusters();
|
||||
|
||||
const clusterNames = Object.keys(requestedClusters);
|
||||
const clusterDetail = clusters.find(cluster =>
|
||||
requestedCluster.includes(cluster.name),
|
||||
);
|
||||
|
||||
const clusterDetails = clusters.filter(c => clusterNames.includes(c.name));
|
||||
if (clusterDetail ? false : clusterDetail ?? true) {
|
||||
this.logger.error(
|
||||
`Cluster ${requestedCluster} details not found in config`,
|
||||
);
|
||||
|
||||
const clusterDetailsAuth = clusterDetails.map(c => {
|
||||
const cAuth: ClusterDetails = Object.assign(c, {
|
||||
serviceAccountToken: requestedClusters[c.name],
|
||||
});
|
||||
return cAuth;
|
||||
});
|
||||
throw new NotFoundError("Cluster's detail not found");
|
||||
}
|
||||
|
||||
return clusterDetailsAuth;
|
||||
return clusterDetail as ClusterDetails;
|
||||
}
|
||||
|
||||
private getClusterURI(details: ClusterDetails): string {
|
||||
const client = this.getKubeConfig(details);
|
||||
return client.getCurrentCluster()?.server || '';
|
||||
}
|
||||
|
||||
private async makeRequestToCluster(
|
||||
details: ClusterDetails,
|
||||
req: Request,
|
||||
): Promise<KubernetesProxyResponse> {
|
||||
const serverURI = this.getClusterURI(details);
|
||||
const serverURI = this.getKubeConfig(details)?.getCurrentCluster()?.server;
|
||||
|
||||
if (!serverURI) {
|
||||
this.logger.error(`Cluster ${details.name} details IP error`);
|
||||
@@ -171,27 +125,26 @@ export class KubernetesProxy {
|
||||
throw new ConflictError('Cluster detail error');
|
||||
}
|
||||
|
||||
const query = decodeURIComponent(req.params.encodedQuery) || '';
|
||||
const uri = `${serverURI}/${query}`;
|
||||
return serverURI;
|
||||
}
|
||||
|
||||
const contentType = req.header(HEADER_CONTENT_TYPE) || APPLICATION_JSON;
|
||||
private async makeRequestToCluster(
|
||||
details: ClusterDetails,
|
||||
req: ExpressRequest,
|
||||
): Promise<Response> {
|
||||
const serverURI = this.getClusterURI(details);
|
||||
|
||||
return await this.sendClusterRequest(
|
||||
details,
|
||||
uri,
|
||||
req.method,
|
||||
contentType,
|
||||
req.body,
|
||||
);
|
||||
const path = decodeURIComponent(req.params.path) || '';
|
||||
const uri = `${serverURI}/${path}`;
|
||||
|
||||
return await this.sendClusterRequest(details, uri, req);
|
||||
}
|
||||
|
||||
private async sendClusterRequest(
|
||||
details: ClusterDetails,
|
||||
uri: string,
|
||||
method: string,
|
||||
contentType: string,
|
||||
body?: any,
|
||||
): Promise<KubernetesProxyResponse> {
|
||||
req: ExpressRequest,
|
||||
): Promise<Response> {
|
||||
const bearerToken = details.serviceAccountToken;
|
||||
|
||||
if (!bearerToken) {
|
||||
@@ -200,12 +153,11 @@ export class KubernetesProxy {
|
||||
throw new AuthenticationError('Invalid service account token');
|
||||
}
|
||||
|
||||
const { method, headers, body } = req;
|
||||
|
||||
const reqData: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
headers: headers as { [key: string]: string },
|
||||
};
|
||||
|
||||
if (!details.skipTLSVerify) {
|
||||
@@ -214,6 +166,7 @@ export class KubernetesProxy {
|
||||
reqData.agent = new https.Agent({ ca });
|
||||
} else {
|
||||
this.logger.error('could not find CA certificate!');
|
||||
|
||||
throw new InputError(
|
||||
'Invalid CA certificate configured within Backstage',
|
||||
);
|
||||
@@ -225,23 +178,7 @@ export class KubernetesProxy {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(uri, reqData);
|
||||
|
||||
let data: string | any;
|
||||
|
||||
if (contentType.includes(APPLICATION_JSON)) {
|
||||
data = await res.json();
|
||||
} else {
|
||||
data = await res.text();
|
||||
}
|
||||
|
||||
const proxyResponse: KubernetesProxyResponse = {
|
||||
code: res.status,
|
||||
data,
|
||||
cluster: details.name,
|
||||
};
|
||||
|
||||
return proxyResponse;
|
||||
return fetch(uri, reqData);
|
||||
} catch (e: any) {
|
||||
throw new ForwardedError(`Cluster ${details.name} request error`, e);
|
||||
}
|
||||
@@ -281,9 +218,4 @@ export class KubernetesProxy {
|
||||
|
||||
return kubeConfig;
|
||||
}
|
||||
|
||||
private getBestResponseCode(codes: number[]): number {
|
||||
const sorted = codes.sort();
|
||||
return sorted[0] ?? ERROR_INTERNAL_SERVER;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5973,6 +5973,7 @@ __metadata:
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@backstage/plugin-kubernetes-common": "workspace:^"
|
||||
"@google-cloud/container": ^4.0.0
|
||||
"@jest-mock/express": ^2.0.1
|
||||
"@kubernetes/client-node": 0.17.0
|
||||
"@types/aws4": ^1.5.1
|
||||
"@types/express": ^4.17.6
|
||||
@@ -5990,7 +5991,7 @@ __metadata:
|
||||
luxon: ^3.0.0
|
||||
morgan: ^1.10.0
|
||||
msw: ^0.48.0
|
||||
node-fetch: ^2.6.0
|
||||
node-fetch: ^2.6.7
|
||||
stream-buffers: ^3.0.2
|
||||
supertest: ^6.1.3
|
||||
winston: ^3.2.1
|
||||
@@ -9250,6 +9251,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest-mock/express@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@jest-mock/express@npm:2.0.1"
|
||||
dependencies:
|
||||
"@types/express": ^4.17.13
|
||||
checksum: 999ea0a953b3e911d0b8ecc4cb3b78ac252b3354832db9659be6754094410b10508f1688f3e623ab13fe40c28d89fd5a81699e6acd148dd8128abec4e81f3f14
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/console@npm:^29.0.3":
|
||||
version: 29.0.3
|
||||
resolution: "@jest/console@npm:29.0.3"
|
||||
|
||||
Reference in New Issue
Block a user