diff --git a/.changeset/silver-bears-wave.md b/.changeset/silver-bears-wave.md new file mode 100644 index 0000000000..9c98220fdc --- /dev/null +++ b/.changeset/silver-bears-wave.md @@ -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. 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; }