move in-cluster token handling to strategy

Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
Jamie Klassen
2023-09-11 15:01:39 -04:00
parent 1df3afb21e
commit e30bd8709d
5 changed files with 99 additions and 20 deletions
@@ -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',
});
});
});
});
});
@@ -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<KubernetesCredential> {
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() {}
@@ -203,6 +203,7 @@ describe('KubernetesBuilder', () => {
url: 'https://localhost:1234',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'serviceAccount',
serviceAccountToken: 'placeholder-token',
},
};
const clusters: ClusterDetails[] = [
@@ -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<ObjectToFetch>([
{
group: '',
@@ -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({