feat: Liam Rathke pre RFC update commit squash
Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Copy of proxy plugin work Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Adds B64 encoding, TLS verify check Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Sets up test scaffolding Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Finishes proxy implementation tests Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Removes deprecated buffer method Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Adds additional HTTP verbs Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Removes fallback Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Adds some content-agnosticness, removes nitpick Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Adds changeset Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Fixes TSC errors? Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Adds API Report docs Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Removes unnecessary KubernetesRequestAuth parameter Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Somehow adds new docs??? Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Fixes some code inline with review comments Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Updates API report Signed-off-by: Liam Rathke <liam.rathke@gmail.com> Fixes tests Signed-off-by: Liam Rathke <liam.rathke@gmail.com>
This commit is contained in:
committed by
Fredrik Adelöw
parent
5bb2d0f32b
commit
b585179770
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
'@backstage/plugin-kubernetes-common': patch
|
||||
---
|
||||
|
||||
Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided).
|
||||
@@ -209,8 +209,15 @@ export class KubernetesBuilder {
|
||||
// (undocumented)
|
||||
protected getObjectTypesToFetch(): ObjectToFetch[] | undefined;
|
||||
// (undocumented)
|
||||
protected getProxyServices(): KubernetesProxyServices;
|
||||
// (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;
|
||||
@@ -322,6 +329,12 @@ export type KubernetesObjectTypes =
|
||||
| 'statefulsets'
|
||||
| 'daemonsets';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface KubernetesProxyServices {
|
||||
// (undocumented)
|
||||
kcs: KubernetesClustersSupplier;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface KubernetesServiceLocator {
|
||||
// (undocumented)
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"helmet": "^6.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.0.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"morgan": "^1.10.0",
|
||||
"stream-buffers": "^3.0.2",
|
||||
"winston": "^3.2.1",
|
||||
|
||||
@@ -30,8 +30,12 @@ import {
|
||||
KubernetesFetcher,
|
||||
KubernetesServiceLocator,
|
||||
KubernetesObjectsProviderOptions,
|
||||
KubernetesProxyServices,
|
||||
} from '../types/types';
|
||||
import { KubernetesClientProvider } from './KubernetesClientProvider';
|
||||
|
||||
import { KubernetesProxy, KubernetesProxyResponse } from './KubernetesProxy';
|
||||
|
||||
import {
|
||||
DEFAULT_OBJECTS,
|
||||
KubernetesFanOutHandler,
|
||||
@@ -76,12 +80,15 @@ export class KubernetesBuilder {
|
||||
private objectsProvider?: KubernetesObjectsProvider;
|
||||
private fetcher?: KubernetesFetcher;
|
||||
private serviceLocator?: KubernetesServiceLocator;
|
||||
private proxy: KubernetesProxy;
|
||||
|
||||
static createBuilder(env: KubernetesEnvironment) {
|
||||
return new KubernetesBuilder(env);
|
||||
}
|
||||
|
||||
constructor(protected readonly env: KubernetesEnvironment) {}
|
||||
constructor(protected readonly env: KubernetesEnvironment) {
|
||||
this.proxy = new KubernetesProxy(env.logger);
|
||||
}
|
||||
|
||||
public async build(): KubernetesBuilderReturn {
|
||||
const logger = this.env.logger;
|
||||
@@ -273,6 +280,12 @@ 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));
|
||||
|
||||
addResourceRoutesToRouter(router, catalogApi, objectsProvider);
|
||||
|
||||
return router;
|
||||
@@ -325,4 +338,28 @@ export class KubernetesBuilder {
|
||||
|
||||
return objectTypesToFetch;
|
||||
}
|
||||
|
||||
protected async makeProxyRequest(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
) {
|
||||
const services = this.getProxyServices();
|
||||
const proxyResponse: KubernetesProxyResponse =
|
||||
await this.proxy.handleProxyRequest(services, req);
|
||||
res.status(proxyResponse.code).json(proxyResponse.data);
|
||||
}
|
||||
|
||||
protected getProxyServices(): KubernetesProxyServices {
|
||||
const kcs =
|
||||
this.clusterSupplier ??
|
||||
this.buildClusterSupplier(this.defaultClusterRefreshInterval);
|
||||
|
||||
if (!kcs) {
|
||||
this.env.logger.error('could not find cluster supplier!');
|
||||
}
|
||||
|
||||
return {
|
||||
kcs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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 { 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 fetch from 'node-fetch';
|
||||
|
||||
describe('KubernetesProxy', () => {
|
||||
let _clientMock: any;
|
||||
let sut: KubernetesProxy;
|
||||
|
||||
const buildEncodedRequest = (
|
||||
clustersHeader: any,
|
||||
query: string,
|
||||
body?: any,
|
||||
): Request => {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const encodedClusters = Buffer.from(
|
||||
JSON.stringify(clustersHeader),
|
||||
).toString('base64');
|
||||
|
||||
const req = {
|
||||
params: {
|
||||
encodedQuery,
|
||||
},
|
||||
header: (key: string) => {
|
||||
let value: string = '';
|
||||
switch (key) {
|
||||
case 'Content-Type': {
|
||||
value = 'application/json';
|
||||
break;
|
||||
}
|
||||
case 'X-Kubernetes-Clusters': {
|
||||
value = encodedClusters;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
if (body) {
|
||||
req.body = body;
|
||||
}
|
||||
|
||||
return req;
|
||||
};
|
||||
|
||||
const buildProxyServicesWithClusters = (
|
||||
clusters: ClusterDetails[],
|
||||
): KubernetesProxyServices => {
|
||||
const kcs: KubernetesClustersSupplier = {
|
||||
getClusters: async () => {
|
||||
return clusters;
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
kcs,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
_clientMock = {
|
||||
handleProxyRequest: jest.fn(),
|
||||
};
|
||||
|
||||
sut = new KubernetesProxy(getVoidLogger());
|
||||
});
|
||||
|
||||
it('should return a 404 if no clusters are found', async () => {
|
||||
const services = buildProxyServicesWithClusters([]);
|
||||
const req = buildEncodedRequest({}, 'api');
|
||||
|
||||
const result = await sut.handleProxyRequest(services, req);
|
||||
|
||||
expect(result.code).toEqual(404);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should match the response code of the Kubernetes response (single cluster)', async () => {
|
||||
const services = buildProxyServicesWithClusters([
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
]);
|
||||
const req = buildEncodedRequest({ cluster1: 'token' }, 'api');
|
||||
|
||||
const apiResponse = {
|
||||
kind: 'APIVersions',
|
||||
versions: ['v1'],
|
||||
serverAddressByClientCIDRs: [
|
||||
{
|
||||
clientCIDR: '0.0.0.0/0',
|
||||
serverAddress: '192.168.0.1:3333',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-ignore-next-line
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
|
||||
new Response(JSON.stringify(apiResponse), {
|
||||
status: 299,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await sut.handleProxyRequest(services, req);
|
||||
|
||||
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([
|
||||
{
|
||||
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 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,
|
||||
};
|
||||
|
||||
(fetch as jest.MockedFunction<typeof fetch>)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(apiResponse1), {
|
||||
status: 200,
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(apiResponse2), {
|
||||
status: 401,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await sut.handleProxyRequest(services, req);
|
||||
|
||||
expect(fetch).toBeCalledTimes(2);
|
||||
expect(result.code).toEqual(200);
|
||||
});
|
||||
|
||||
it('should pass the exact response data from Kubernetes (single cluster)', async () => {
|
||||
const services = buildProxyServicesWithClusters([
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
]);
|
||||
const req = buildEncodedRequest({ cluster1: 'token' }, 'api');
|
||||
|
||||
const apiResponse = {
|
||||
kind: 'APIVersions',
|
||||
versions: ['v1'],
|
||||
serverAddressByClientCIDRs: [
|
||||
{
|
||||
clientCIDR: '0.0.0.0/0',
|
||||
serverAddress: '192.168.0.1:3333',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-ignore-next-line
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
|
||||
new Response(JSON.stringify(apiResponse), {
|
||||
status: 200,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await sut.handleProxyRequest(services, req);
|
||||
|
||||
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(fetch).toBeCalledTimes(1);
|
||||
expect(resultString).toEqual(expectedString);
|
||||
});
|
||||
|
||||
it('should pass the exact response data from Kubernetes (multi cluster)', async () => {
|
||||
const services = buildProxyServicesWithClusters([
|
||||
{
|
||||
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 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,
|
||||
};
|
||||
|
||||
// @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,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await sut.handleProxyRequest(services, req);
|
||||
|
||||
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(fetch).toBeCalledTimes(2);
|
||||
expect(resultString).toEqual(expectedString);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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 { KubeConfig, bufferFromFileOrString } from '@kubernetes/client-node';
|
||||
import { Logger } from 'winston';
|
||||
import fetch from 'node-fetch';
|
||||
import * as https from 'https';
|
||||
|
||||
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';
|
||||
|
||||
export interface KubernetesProxyResponse {
|
||||
code: number;
|
||||
data: any;
|
||||
cluster?: string;
|
||||
}
|
||||
|
||||
interface KubernetesProxyClusters {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export class KubernetesProxy {
|
||||
constructor(protected readonly logger: Logger) {}
|
||||
|
||||
public async handleProxyRequest(
|
||||
services: KubernetesProxyServices,
|
||||
req: Request,
|
||||
): Promise<KubernetesProxyResponse> {
|
||||
const krc = this.getKubernetesRequestedClusters(req);
|
||||
|
||||
if (Object.keys(krc).length < 1) {
|
||||
return {
|
||||
code: ERROR_NOT_FOUND,
|
||||
data: 'No clusters found!',
|
||||
};
|
||||
}
|
||||
|
||||
const details = await this.getClusterDetails(services.kcs, krc);
|
||||
|
||||
if (details.length < 1) {
|
||||
return {
|
||||
code: ERROR_NOT_FOUND,
|
||||
data: 'No clusters found!',
|
||||
};
|
||||
}
|
||||
|
||||
const responses = await Promise.all(
|
||||
details.map(async d => {
|
||||
const response = await this.makeRequestToCluster(d, 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;
|
||||
}
|
||||
|
||||
private getKubernetesRequestedClusters(
|
||||
req: Request,
|
||||
): KubernetesProxyClusters {
|
||||
const encodedClusters: string =
|
||||
req.header(HEADER_KUBERNETES_CLUSTERS) ?? '';
|
||||
|
||||
if (!encodedClusters) {
|
||||
return {};
|
||||
}
|
||||
|
||||
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: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private async getClusterDetails(
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
krc: KubernetesProxyClusters,
|
||||
): Promise<ClusterDetails[]> {
|
||||
const clusters = await clusterSupplier.getClusters();
|
||||
|
||||
const clusterNames = Object.keys(krc);
|
||||
|
||||
const clusterDetails = clusters.filter(c => clusterNames.includes(c.name));
|
||||
|
||||
const clusterDetailsAuth = clusterDetails.map(c => {
|
||||
const cAuth: ClusterDetails = Object.assign(c, {
|
||||
serviceAccountToken: krc[c.name],
|
||||
});
|
||||
return cAuth;
|
||||
});
|
||||
|
||||
return clusterDetailsAuth;
|
||||
}
|
||||
|
||||
private getClusterURI(details: ClusterDetails): string {
|
||||
const client = this.getKubeConfig(details);
|
||||
return client.getCurrentCluster()?.server || '';
|
||||
}
|
||||
|
||||
private async makeRequestToCluster(
|
||||
details: ClusterDetails,
|
||||
req: Request,
|
||||
): Promise<KubernetesProxyResponse> {
|
||||
const serverIP = this.getClusterURI(details);
|
||||
if (!serverIP) {
|
||||
return {
|
||||
code: ERROR_INTERNAL_SERVER,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const query = decodeURIComponent(req.params.encodedQuery) || '';
|
||||
const uri = `${serverIP}/${query}`;
|
||||
|
||||
const contentType = req.header(HEADER_CONTENT_TYPE) || APPLICATION_JSON;
|
||||
|
||||
const res = await this.sendClusterRequest(
|
||||
details,
|
||||
uri,
|
||||
req.method,
|
||||
contentType,
|
||||
req.body,
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private async sendClusterRequest(
|
||||
details: ClusterDetails,
|
||||
uri: string,
|
||||
method: string,
|
||||
contentType: string,
|
||||
body?: any,
|
||||
): Promise<KubernetesProxyResponse> {
|
||||
const bearerToken = details.serviceAccountToken;
|
||||
if (!bearerToken) {
|
||||
return {
|
||||
code: ERROR_BAD_REQUEST,
|
||||
data: {
|
||||
error: 'Invalid service account token',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const reqData: any = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
if (!details.skipTLSVerify) {
|
||||
if (details.caData) {
|
||||
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',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (body && Object.keys(body).length > 0) {
|
||||
reqData.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const req = await fetch(uri, reqData);
|
||||
|
||||
let res;
|
||||
if (contentType.includes(APPLICATION_JSON)) {
|
||||
res = await req.json();
|
||||
} else {
|
||||
res = await req.text();
|
||||
}
|
||||
|
||||
const proxyResponse: KubernetesProxyResponse = {
|
||||
code: req.status,
|
||||
data: res,
|
||||
cluster: details.name,
|
||||
};
|
||||
|
||||
return proxyResponse;
|
||||
} catch (e: any) {
|
||||
return {
|
||||
code: ERROR_INTERNAL_SERVER,
|
||||
data: e,
|
||||
cluster: details.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getKubeConfig(clusterDetails: ClusterDetails): KubeConfig {
|
||||
const cluster = {
|
||||
name: clusterDetails.name,
|
||||
server: clusterDetails.url,
|
||||
skipTLSVerify: clusterDetails.skipTLSVerify,
|
||||
caData: clusterDetails.caData,
|
||||
};
|
||||
|
||||
const user = {
|
||||
name: CLUSTER_USER_NAME,
|
||||
token: clusterDetails.serviceAccountToken,
|
||||
};
|
||||
|
||||
const context = {
|
||||
name: clusterDetails.name,
|
||||
user: user.name,
|
||||
cluster: cluster.name,
|
||||
};
|
||||
|
||||
const kc = new KubeConfig();
|
||||
if (clusterDetails.serviceAccountToken) {
|
||||
kc.loadFromOptions({
|
||||
clusters: [cluster],
|
||||
users: [user],
|
||||
contexts: [context],
|
||||
currentContext: context.name,
|
||||
});
|
||||
} else {
|
||||
kc.loadFromDefault();
|
||||
}
|
||||
|
||||
return kc;
|
||||
}
|
||||
|
||||
private getBestResponseCode(codes: number[]): number {
|
||||
const sorted = codes.sort();
|
||||
return sorted[0] ?? ERROR_INTERNAL_SERVER;
|
||||
}
|
||||
}
|
||||
@@ -280,3 +280,11 @@ export interface KubernetesObjectsProvider {
|
||||
customResourcesByEntity: CustomResourcesByEntity,
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface KubernetesProxyServices {
|
||||
kcs: KubernetesClustersSupplier;
|
||||
}
|
||||
|
||||
@@ -193,6 +193,16 @@ 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)
|
||||
|
||||
@@ -258,3 +258,7 @@ export interface ClientPodStatus {
|
||||
memory: ClientCurrentResourceUsage;
|
||||
containers: ClientContainerStatus[];
|
||||
}
|
||||
|
||||
export interface KubernetesProxyClusters {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user