Merge pull request #23177 from andmagom/kubernetes/FixProxyServiceAccount

Fixed bug at Kubernetes proxy when Backstage is running on K8S
This commit is contained in:
Ben Lambert
2024-03-12 11:20:54 +01:00
committed by GitHub
3 changed files with 115 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Fixed a bug in the proxy endpoint. Now when the `serviceAccount` strategy is used and no `serviceAccountToken` has been provided, the proxy endpoint assumes backstage is running on Kubernetes and gets the URL and CA from the Pod instance.
@@ -17,13 +17,19 @@
import 'buffer';
import { resolve as resolvePath } from 'path';
import { errorHandler, getVoidLogger } from '@backstage/backend-common';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import {
createMockDirectory,
setupRequestMockHandlers,
} from '@backstage/backend-test-utils';
import { NotFoundError } from '@backstage/errors';
import {
AuthorizeResult,
PermissionEvaluator,
} from '@backstage/plugin-permission-common';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
import {
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
KubernetesRequestAuth,
} from '@backstage/plugin-kubernetes-common';
import { getMockReq, getMockRes } from '@jest-mock/express';
import express from 'express';
import Router from 'express-promise-router';
@@ -32,6 +38,7 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import request from 'supertest';
import { AddressInfo, WebSocket, WebSocketServer } from 'ws';
import { Config } from '@kubernetes/client-node';
import { LocalKubectlProxyClusterLocator } from '../cluster-locator/LocalKubectlProxyLocator';
import {
@@ -49,6 +56,12 @@ import {
import type { Request } from 'express';
const mockCertDir = createMockDirectory({
content: {
'ca.crt': 'MOCKCA',
},
});
describe('KubernetesProxy', () => {
let proxy: KubernetesProxy;
let authStrategy: jest.Mocked<AuthenticationStrategy>;
@@ -977,4 +990,72 @@ describe('KubernetesProxy', () => {
await closePromise;
});
});
describe('Backstage running on k8s', () => {
const initialHost = process.env.KUBERNETES_SERVICE_HOST;
const initialPort = process.env.KUBERNETES_SERVICE_PORT;
const initialCaPath = Config.SERVICEACCOUNT_CA_PATH;
afterEach(() => {
process.env.KUBERNETES_SERVICE_HOST = initialHost;
process.env.KUBERNETES_SERVICE_PORT = initialPort;
Config.SERVICEACCOUNT_CA_PATH = initialCaPath;
});
it('makes in-cluster requests when cluster details has no token', async () => {
process.env.KUBERNETES_SERVICE_HOST = '10.10.10.10';
process.env.KUBERNETES_SERVICE_PORT = '443';
Config.SERVICEACCOUNT_CA_PATH = mockCertDir.resolve('ca.crt');
clusterSupplier.getClusters.mockResolvedValue([
{
name: 'cluster1',
url: 'http://ignored',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
},
},
] as ClusterDetails[]);
authStrategy.getCredential.mockResolvedValue({
type: 'bearer token',
token: 'SA_token',
});
worker.use(
rest.get(
'https://10.10.10.10/api/v1/namespaces',
(req: any, res: any, ctx: any) => {
if (req.headers.get('Authorization') === 'Bearer SA_token') {
return res(
ctx.status(200),
ctx.json({
kind: 'NamespaceList',
apiVersion: 'v1',
items: [],
}),
);
}
return res(ctx.status(403));
},
),
);
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
requestPath: '/api/v1/namespaces',
headers: {
[HEADER_KUBERNETES_CLUSTER]: 'cluster1',
},
});
const response = await requestPromise;
expect(response.body).toStrictEqual({
kind: 'NamespaceList',
apiVersion: 'v1',
items: [],
});
});
});
});
@@ -22,6 +22,7 @@ import {
} from '@backstage/errors';
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
import {
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
KubernetesRequestAuth,
kubernetesProxyPermission,
} from '@backstage/plugin-kubernetes-common';
@@ -29,9 +30,15 @@ import {
AuthorizeResult,
PermissionEvaluator,
} from '@backstage/plugin-permission-common';
import { bufferFromFileOrString } from '@kubernetes/client-node';
import {
Cluster,
KubeConfig,
bufferFromFileOrString,
} from '@kubernetes/client-node';
import { createProxyMiddleware, RequestHandler } from 'http-proxy-middleware';
import { Logger } from 'winston';
import fs from 'fs-extra';
import { Config } from '@kubernetes/client-node';
import { AuthenticationStrategy } from '../auth';
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
@@ -234,6 +241,25 @@ export class KubernetesProxy {
throw new NotFoundError(`Cluster '${clusterName}' not found`);
}
const authProvider =
cluster.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];
if (
authProvider === 'serviceAccount' &&
fs.pathExistsSync(Config.SERVICEACCOUNT_CA_PATH) &&
!cluster.authMetadata.serviceAccountToken
) {
const kc = new KubeConfig();
kc.loadFromCluster();
const clusterFromKubeConfig = kc.getCurrentCluster() as Cluster;
const url = new URL(clusterFromKubeConfig.server);
cluster.url = clusterFromKubeConfig.server;
if (url.protocol === 'https:') {
cluster.caFile = clusterFromKubeConfig.caFile;
}
}
return cluster;
}