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:
@@ -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`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user