when ServiceAccount Strategy is used and no serviceAccountToken has been provided, the Kubernetes proxy is able to get the token and CA from the pod instance

Signed-off-by: Andres Mauricio Gomez P <andmagom@outlook.com>
This commit is contained in:
Andres Mauricio Gomez P
2024-02-22 10:50:28 -05:00
parent b01dace087
commit b9a08881c3
3 changed files with 115 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Fixed bug at Kubernetes proxy, now when ServiceAccount Strategy is used and no serviceAccountToken has been provided, the Kubernetes Proxy assumes backstage is running on k8s, so it is able to get the token 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;
}