Merge pull request #18167 from luchillo17/feat/BCKSTG-208-backstage-kubernetes-cluster-

HEADER_KUBERNETES_CLUSTER optional in single cluster setup
This commit is contained in:
Jamie Klassen
2023-06-09 10:47:40 -04:00
committed by GitHub
3 changed files with 119 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
K8s proxy HEADER_KUBERNETES_CLUSTER is now optional in single-cluster setups.
@@ -104,6 +104,56 @@ describe('KubernetesProxy', () => {
).rejects.toThrow(NotFoundError);
});
it('should return a ERROR_NOT_FOUND if multi-cluster & no cluster selected', async () => {
clusterSupplier.getClusters.mockResolvedValue([
{
name: 'local',
url: 'http:/localhost:8001',
authProvider: 'localKubectlProxy',
skipMetricsLookup: true,
} as ClusterDetails,
{
name: 'cluster1',
url: 'https://localhost:9999',
serviceAccountToken: 'tokenA',
authProvider: 'googleServiceAccount',
} as ClusterDetails,
]);
permissionApi.authorize.mockReturnValue(
Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
);
const req = buildMockRequest(undefined, 'api');
const { res, next } = getMockRes();
await expect(
proxy.createRequestHandler({ permissionApi })(req, res, next),
).rejects.toThrow(NotFoundError);
});
it('should return a ERROR_NOT_FOUND if selected cluster not in config', async () => {
clusterSupplier.getClusters.mockResolvedValue([
{
name: 'cluster1',
url: 'https://localhost:9999',
serviceAccountToken: 'tokenA',
authProvider: 'googleServiceAccount',
} as ClusterDetails,
]);
permissionApi.authorize.mockReturnValue(
Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
);
const req = buildMockRequest('test', 'api');
const { res, next } = getMockRes();
await expect(
proxy.createRequestHandler({ permissionApi })(req, res, next),
).rejects.toThrow(NotFoundError);
});
it('should pass the exact response from Kubernetes', async () => {
const apiResponse = {
kind: 'APIVersions',
@@ -155,6 +205,55 @@ describe('KubernetesProxy', () => {
expect(response.body).toStrictEqual(apiResponse);
});
it('should pass the exact response from Kubernetes default cluster & no cluster selected in single cluster setup', async () => {
const apiResponse = {
kind: 'APIVersions',
versions: ['v1'],
serverAddressByClientCIDRs: [
{
clientCIDR: '0.0.0.0/0',
serverAddress: '192.168.0.1:3333',
},
],
};
clusterSupplier.getClusters.mockResolvedValue([
{
name: 'cluster1',
url: 'https://localhost:9999',
serviceAccountToken: '',
authProvider: 'serviceAccount',
},
] as ClusterDetails[]);
permissionApi.authorize.mockReturnValue(
Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
);
authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
serviceAccountToken: '',
authProvider: 'serviceAccount',
} as ClusterDetails);
const router = Router();
router.use('/mountpath', proxy.createRequestHandler({ permissionApi }));
const app = express().use(router);
const requestPromise = request(app).get('/mountpath/api');
worker.use(
rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) =>
res(ctx.status(299), ctx.json(apiResponse)),
),
rest.all(requestPromise.url, (req: any) => req.passthrough()),
);
const response = await requestPromise;
expect(response.status).toEqual(299);
expect(response.body).toStrictEqual(apiResponse);
});
it('sets host header to support clusters behind name-based virtual hosts', async () => {
worker.use(
rest.get(
@@ -481,55 +580,6 @@ describe('KubernetesProxy', () => {
});
});
it('should return a 400 error if Backstage-Kubernetes-Cluster field isnt provided in request', async () => {
worker.use(
rest.get('https://localhost:9999/api/v1/namespaces', (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
kind: 'NamespaceList',
apiVersion: 'v1',
items: [],
}),
);
}),
);
permissionApi.authorize.mockReturnValue(
Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
);
clusterSupplier.getClusters.mockResolvedValue([
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
},
] as ClusterDetails[]);
authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
serviceAccountToken: 'tokenA',
authProvider: 'googleServiceAccount',
} as ClusterDetails);
const router = Router();
router.use('/mountpath', proxy.createRequestHandler({ permissionApi }));
router.use(errorHandler());
const app = express().use(router);
const requestPromise = request(app)
.get('/mountpath/api/v1/namespaces')
.set(HEADER_KUBERNETES_AUTH, 'tokenB');
worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough()));
const response = await requestPromise;
expect(response.status).toEqual(400);
});
it('returns a 500 error if authTranslator errors out and Backstage-Kubernetes-Authorization field is not provided', async () => {
worker.use(
rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => {
@@ -17,7 +17,6 @@
import {
ErrorResponseBody,
ForwardedError,
InputError,
NotAllowedError,
NotFoundError,
serializeError,
@@ -175,13 +174,23 @@ export class KubernetesProxy {
private async getClusterForRequest(req: Request): Promise<ClusterDetails> {
const clusterName = req.header(HEADER_KUBERNETES_CLUSTER);
if (!clusterName) {
throw new InputError(`Missing '${HEADER_KUBERNETES_CLUSTER}' header.`);
const clusters = await this.clusterSupplier.getClusters();
if (!clusters || clusters.length <= 0) {
throw new NotFoundError(`No Clusters configured`);
}
const hasClusterNameHeader =
typeof clusterName === 'string' && clusterName.length > 0;
let cluster: ClusterDetails | undefined;
if (hasClusterNameHeader) {
cluster = clusters.find(c => c.name === clusterName);
} else if (clusters.length === 1) {
cluster = clusters.at(0);
}
const cluster = await this.clusterSupplier
.getClusters()
.then(clusters => clusters.find(c => c.name === clusterName));
if (!cluster) {
throw new NotFoundError(`Cluster '${clusterName}' not found`);
}