From e30bd8709d43d22315f41aa81a0b5d18a80bf0f5 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Mon, 11 Sep 2023 15:01:39 -0400 Subject: [PATCH] move in-cluster token handling to strategy Signed-off-by: Jamie Klassen --- .../src/auth/ServiceAccountStrategy.test.ts | 60 +++++++++++++++++++ .../src/auth/ServiceAccountStrategy.ts | 15 ++++- .../src/service/KubernetesBuilder.test.ts | 1 + .../src/service/KubernetesFetcher.test.ts | 8 +-- .../src/service/KubernetesFetcher.ts | 35 ++++++----- 5 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts diff --git a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts new file mode 100644 index 0000000000..b6b9d0efb8 --- /dev/null +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ServiceAccountStrategy } from './ServiceAccountStrategy'; +import mockFs from 'mock-fs'; + +describe('ServiceAccountStrategy', () => { + describe('#getCredential', () => { + it('reads bearer token from config', async () => { + const strategy = new ServiceAccountStrategy(); + + const credential = await strategy.getCredential({ + name: '', + url: '', + authMetadata: { serviceAccountToken: 'from config' }, + }); + + expect(credential).toStrictEqual({ + type: 'bearer token', + token: 'from config', + }); + }); + describe('when serviceAccountToken is absent from config', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('reads in-cluster token', async () => { + mockFs({ + '/var/run/secrets/kubernetes.io/serviceaccount/token': + 'in-cluster-token', + }); + const strategy = new ServiceAccountStrategy(); + + const credential = await strategy.getCredential({ + name: '', + url: '', + authMetadata: {}, + }); + + expect(credential).toStrictEqual({ + type: 'bearer token', + token: 'in-cluster-token', + }); + }); + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts index 24ade91e48..9658b1d8eb 100644 --- a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { KubeConfig, User } from '@kubernetes/client-node'; +import fs from 'fs-extra'; import { AuthenticationStrategy, KubernetesCredential } from './types'; import { ClusterDetails } from '../types/types'; @@ -26,7 +28,18 @@ export class ServiceAccountStrategy implements AuthenticationStrategy { clusterDetails: ClusterDetails, ): Promise { const token = clusterDetails.authMetadata.serviceAccountToken; - return token ? { type: 'bearer token', token } : { type: 'anonymous' }; + if (token) { + return { type: 'bearer token', token }; + } + const kc = new KubeConfig(); + kc.loadFromCluster(); + // loadFromCluster is guaranteed to populate the user + const user = kc.getCurrentUser() as User; + + return { + type: 'bearer token', + token: fs.readFileSync(user.authProvider.config.tokenFile).toString(), + }; } public validate() {} diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index a428c7ca0e..dbcb0659b7 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -203,6 +203,7 @@ describe('KubernetesBuilder', () => { url: 'https://localhost:1234', authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + serviceAccountToken: 'placeholder-token', }, }; const clusters: ClusterDetails[] = [ diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 9f751ff8e1..2cecbbeaec 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -909,8 +909,6 @@ describe('KubernetesFetcher', () => { process.env.KUBERNETES_SERVICE_PORT = '443'; mockFs({ '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt': '', - '/var/run/secrets/kubernetes.io/serviceaccount/token': - 'allowed-token', }); worker.use( rest.get('https://10.10.10.10/api/v1/pods', (req, res, ctx) => @@ -928,9 +926,11 @@ describe('KubernetesFetcher', () => { clusterDetails: { name: 'overridden-to-in-cluster', url: 'http://ignored', - authMetadata: {}, + authMetadata: { + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount', + }, }, - credential: { type: 'anonymous' }, + credential: { type: 'bearer token', token: 'allowed-token' }, objectTypesToFetch: new Set([ { group: '', diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 42556c8473..e514f5fe0b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -20,7 +20,6 @@ import { CoreV1Api, KubeConfig, Metrics, - User, bufferFromFileOrString, topPods, } from '@kubernetes/client-node'; @@ -214,14 +213,19 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { let url: URL; let requestInit: RequestInit; + const authProvider = + clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; if ( + authProvider === 'serviceAccount' && + !clusterDetails.authMetadata.serviceAccountToken && + fs.pathExistsSync(Config.SERVICEACCOUNT_CA_PATH) + ) { + [url, requestInit] = this.fetchArgsInCluster(credential); + } else if ( credential.type === 'bearer token' || - clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER] === - 'localKubectlProxy' + authProvider === 'localKubectlProxy' ) { [url, requestInit] = this.fetchArgs(clusterDetails, credential); - } else if (fs.pathExistsSync(Config.SERVICEACCOUNT_TOKEN_PATH)) { - [url, requestInit] = this.fetchArgsInCluster(); } else { return Promise.reject( new Error( @@ -271,24 +275,25 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } return [url, requestInit]; } - private fetchArgsInCluster(): [URL, RequestInit] { - const kc = new KubeConfig(); - kc.loadFromCluster(); - // loadFromCluster is guaranteed to populate the cluster/user/context - const cluster = kc.getCurrentCluster() as Cluster; - const user = kc.getCurrentUser() as User; - - const token = fs.readFileSync(user.authProvider.config.tokenFile); - + private fetchArgsInCluster( + credential: KubernetesCredential, + ): [URL, RequestInit] { const requestInit: RequestInit = { method: 'GET', headers: { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, + ...(credential.type === 'bearer token' && { + Authorization: `Bearer ${credential.token}`, + }), }, }; + const kc = new KubeConfig(); + kc.loadFromCluster(); + // loadFromCluster is guaranteed to populate the cluster/user/context + const cluster = kc.getCurrentCluster() as Cluster; + const url = new URL(cluster.server); if (url.protocol === 'https:') { requestInit.agent = new https.Agent({