diff --git a/.changeset/silver-bears-wave.md b/.changeset/silver-bears-wave.md new file mode 100644 index 0000000000..9836949e11 --- /dev/null +++ b/.changeset/silver-bears-wave.md @@ -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 diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 527cf94a6a..241df1c6f0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -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; @@ -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: [], + }); + }); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 209d01b703..b4536c5880 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -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; }